2022-07-17 12:39:18 +00:00
|
|
|
-- Copyright (c) 2019-2022 David Vogel
|
2019-10-18 20:35:51 +00:00
|
|
|
--
|
|
|
|
-- This software is released under the MIT License.
|
|
|
|
-- https://opensource.org/licenses/MIT
|
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio.
|
|
|
|
CAPTURE_GRID_SIZE = 512 -- in virtual (world) pixels. There will always be exactly 4 images overlapping if the virtual resolution is 1024x1024.
|
2019-11-01 01:40:21 +00:00
|
|
|
CAPTURE_FORCE_HP = 4 -- * 25HP
|
2019-10-18 20:35:51 +00:00
|
|
|
|
2020-10-20 13:29:28 +00:00
|
|
|
-- "Base layout" (Base layout. Every part outside this is based on a similar layout, but uses different materials/seeds)
|
|
|
|
CAPTURE_AREA_BASE_LAYOUT = {
|
2022-07-17 12:39:18 +00:00
|
|
|
Left = -17920, -- in virtual (world) pixels.
|
|
|
|
Top = -7168, -- in virtual (world) pixels.
|
|
|
|
Right = 17920, -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
|
|
|
Bottom = 17408 -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
2020-10-20 13:29:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
-- "Main world" (The main world with 3 parts: sky, normal and hell)
|
|
|
|
CAPTURE_AREA_MAIN_WORLD = {
|
2022-07-17 12:39:18 +00:00
|
|
|
Left = -17920, -- in virtual (world) pixels.
|
|
|
|
Top = -31744, -- in virtual (world) pixels.
|
|
|
|
Right = 17920, -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
|
|
|
Bottom = 41984 -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
2020-10-20 13:29:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
-- "Extended" (Main world + a fraction of the parallel worlds to the left and right)
|
|
|
|
CAPTURE_AREA_EXTENDED = {
|
2022-07-17 12:39:18 +00:00
|
|
|
Left = -25600, -- in virtual (world) pixels.
|
|
|
|
Top = -31744, -- in virtual (world) pixels.
|
|
|
|
Right = 25600, -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
|
|
|
Bottom = 41984 -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
2020-10-20 13:29:28 +00:00
|
|
|
}
|
2019-11-02 20:37:10 +00:00
|
|
|
|
2022-07-17 14:54:59 +00:00
|
|
|
-- Set of already captured entities.
|
|
|
|
local capturedEntities = {}
|
|
|
|
|
2019-10-18 20:35:51 +00:00
|
|
|
local function preparePlayer()
|
|
|
|
local playerEntity = getPlayer()
|
|
|
|
addEffectToEntity(playerEntity, "PROTECTION_ALL")
|
|
|
|
|
2019-11-28 20:05:50 +00:00
|
|
|
--addPerkToPlayer("BREATH_UNDERWATER")
|
|
|
|
--addPerkToPlayer("INVISIBILITY")
|
|
|
|
--addPerkToPlayer("REMOVE_FOG_OF_WAR")
|
|
|
|
--addPerkToPlayer("REPELLING_CAPE")
|
|
|
|
--addPerkToPlayer("WORM_DETRACTOR")
|
2019-10-18 20:35:51 +00:00
|
|
|
setPlayerHP(CAPTURE_FORCE_HP)
|
|
|
|
end
|
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
--- Captures a screenshot at the given coordinates.
|
|
|
|
--- This will block until all chunks in the given area are loaded.
|
|
|
|
---
|
|
|
|
--- @param x number -- Virtual x coordinate (World pixels) of the screen center.
|
|
|
|
--- @param y number -- Virtual y coordinate (World pixels) of the screen center.
|
|
|
|
--- @param rx number -- Screen x coordinate of the top left corner of the screenshot rectangle.
|
|
|
|
--- @param ry number -- Screen y coordinate of the top left corner of the screenshot rectangle.
|
|
|
|
--- @param entityFile file*
|
|
|
|
local function captureScreenshot(x, y, rx, ry, entityFile)
|
2020-06-01 20:39:00 +00:00
|
|
|
local virtualWidth, virtualHeight =
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
|
|
|
|
|
|
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
|
|
|
|
local xMin, yMin = x - virtualHalfWidth, y - virtualHalfHeight
|
|
|
|
local xMax, yMax = xMin + virtualWidth, yMin + virtualHeight
|
|
|
|
|
|
|
|
UiCaptureDelay = 0
|
2019-11-02 23:58:03 +00:00
|
|
|
GameSetCameraPos(x, y)
|
2020-06-01 20:39:00 +00:00
|
|
|
repeat
|
|
|
|
if UiCaptureDelay > 100 then
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Wiggle the screen a bit, as chunks sometimes don't want to load.
|
2020-10-17 15:27:26 +00:00
|
|
|
GameSetCameraPos(x + math.random(-100, 100), y + math.random(-100, 100))
|
2020-06-01 20:39:00 +00:00
|
|
|
DrawUI()
|
|
|
|
wait(0)
|
|
|
|
UiCaptureDelay = UiCaptureDelay + 1
|
|
|
|
GameSetCameraPos(x, y)
|
|
|
|
end
|
2020-10-17 15:27:26 +00:00
|
|
|
|
2020-06-01 20:39:00 +00:00
|
|
|
DrawUI()
|
|
|
|
wait(0)
|
|
|
|
UiCaptureDelay = UiCaptureDelay + 1
|
2022-07-17 12:39:18 +00:00
|
|
|
until DoesWorldExistAt(xMin, yMin, xMax, yMax) -- Chunks will be drawn on the *next* frame.
|
2020-06-01 20:39:00 +00:00
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
wait(0) -- Without this line empty chunks may still appear, also it's needed for the UI to disappear.
|
2019-11-02 23:58:03 +00:00
|
|
|
if not TriggerCapture(rx, ry) then
|
|
|
|
UiCaptureProblem = "Screen capture failed. Please restart Noita."
|
|
|
|
end
|
2019-11-03 22:13:55 +00:00
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Capture entities right after capturing the screenshot.
|
|
|
|
if entityFile then
|
|
|
|
local radius = math.sqrt(virtualHalfWidth^2 + virtualHalfHeight^2) + 1
|
|
|
|
local entities = EntityGetInRadius(x, y, radius)
|
2022-07-17 14:54:59 +00:00
|
|
|
for _, entityID in ipairs(entities) do
|
|
|
|
-- Make sure to only export entities when they are encountered the first time.
|
|
|
|
if not capturedEntities[entityID] then
|
|
|
|
capturedEntities[entityID] = true
|
|
|
|
local x, y, rotation, scaleX, scaleY = EntityGetTransform(entityID)
|
|
|
|
local entityName = EntityGetName(entityID)
|
|
|
|
local entityTags = EntityGetTags(entityID)
|
|
|
|
entityFile:write(string.format("%d, %s, %f, %f, %f, %f, %f, %q\n", entityID, entityName, x, y, rotation, scaleX, scaleY, entityTags))
|
|
|
|
-- TODO: Correctly escape CSV data
|
|
|
|
end
|
2022-07-17 12:39:18 +00:00
|
|
|
end
|
2022-07-17 14:54:59 +00:00
|
|
|
entityFile:flush() -- Ensure everything is written to disk before noita decides to crash.
|
2022-07-17 12:39:18 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Reset monitor and PC standby each screenshot.
|
2019-11-03 22:13:55 +00:00
|
|
|
ResetStandbyTimer()
|
2019-11-02 23:58:03 +00:00
|
|
|
end
|
|
|
|
|
2022-07-17 14:54:59 +00:00
|
|
|
local function createOrOpenEntityCaptureFile()
|
|
|
|
local file = io.open("mods/noita-mapcap/output/entities.csv", "r")
|
|
|
|
if file then
|
|
|
|
local _ = file:read() -- Skip first line.
|
|
|
|
for line in file:lines() do
|
|
|
|
for field in string.gmatch(line, "([^,]+)") do
|
|
|
|
local entityID = tonumber(field)
|
|
|
|
if entityID then
|
|
|
|
capturedEntities[entityID] = true
|
|
|
|
end
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
file:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Create or reopen entities CSV file.
|
|
|
|
local file = io.open("mods/noita-mapcap/output/entities.csv", "a+")
|
|
|
|
if file == nil then return nil end
|
|
|
|
|
|
|
|
if file:seek("end") == 0 then
|
|
|
|
-- Empty file: Create header.
|
|
|
|
file:write("entityID, entityName, x, y, rotation, scaleX, scaleY, tags\n")
|
|
|
|
file:flush()
|
|
|
|
end
|
|
|
|
|
|
|
|
return file
|
|
|
|
end
|
|
|
|
|
2019-11-02 20:37:10 +00:00
|
|
|
function startCapturingSpiral()
|
2022-07-17 14:54:59 +00:00
|
|
|
local entityFile = createOrOpenEntityCaptureFile()
|
2022-07-17 12:39:18 +00:00
|
|
|
|
|
|
|
local ox, oy = GameGetCameraPos() -- Returns the virtual coordinates of the screen center.
|
2019-10-20 14:28:17 +00:00
|
|
|
ox, oy = math.floor(ox / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE, math.floor(oy / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE
|
2022-07-17 12:39:18 +00:00
|
|
|
ox, oy = ox + 256, oy + 256 -- Align screen with ingame chunk grid that is 512x512.
|
2019-10-18 20:35:51 +00:00
|
|
|
local x, y = ox, oy
|
|
|
|
|
2019-11-02 23:58:03 +00:00
|
|
|
local virtualWidth, virtualHeight =
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
|
|
|
|
|
|
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
|
|
|
|
|
2019-10-18 20:35:51 +00:00
|
|
|
preparePlayer()
|
|
|
|
|
2019-10-20 14:28:17 +00:00
|
|
|
GameSetCameraFree(true)
|
2019-10-18 20:35:51 +00:00
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Coroutine to calculate next coordinate, and trigger screenshots.
|
2019-10-18 20:35:51 +00:00
|
|
|
local i = 1
|
|
|
|
async_loop(
|
|
|
|
function()
|
|
|
|
-- +x
|
|
|
|
for i = 1, i, 1 do
|
2022-07-17 12:39:18 +00:00
|
|
|
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
2019-10-23 18:03:03 +00:00
|
|
|
if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
2022-07-17 14:54:59 +00:00
|
|
|
captureScreenshot(x, y, rx, ry, entityFile)
|
2019-10-23 18:03:03 +00:00
|
|
|
end
|
2019-10-18 20:35:51 +00:00
|
|
|
x, y = x + CAPTURE_GRID_SIZE, y
|
|
|
|
end
|
|
|
|
-- +y
|
|
|
|
for i = 1, i, 1 do
|
2022-07-17 12:39:18 +00:00
|
|
|
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
2019-10-23 18:03:03 +00:00
|
|
|
if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
2022-07-17 14:54:59 +00:00
|
|
|
captureScreenshot(x, y, rx, ry, entityFile)
|
2019-10-23 18:03:03 +00:00
|
|
|
end
|
2019-10-18 20:35:51 +00:00
|
|
|
x, y = x, y + CAPTURE_GRID_SIZE
|
|
|
|
end
|
|
|
|
i = i + 1
|
|
|
|
-- -x
|
|
|
|
for i = 1, i, 1 do
|
2022-07-17 12:39:18 +00:00
|
|
|
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
2019-10-23 18:03:03 +00:00
|
|
|
if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
2022-07-17 14:54:59 +00:00
|
|
|
captureScreenshot(x, y, rx, ry, entityFile)
|
2019-10-23 18:03:03 +00:00
|
|
|
end
|
2019-10-18 20:35:51 +00:00
|
|
|
x, y = x - CAPTURE_GRID_SIZE, y
|
|
|
|
end
|
|
|
|
-- -y
|
|
|
|
for i = 1, i, 1 do
|
2022-07-17 12:39:18 +00:00
|
|
|
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
2019-10-23 18:03:03 +00:00
|
|
|
if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
2022-07-17 14:54:59 +00:00
|
|
|
captureScreenshot(x, y, rx, ry, entityFile)
|
2019-10-23 18:03:03 +00:00
|
|
|
end
|
2019-10-18 20:35:51 +00:00
|
|
|
x, y = x, y - CAPTURE_GRID_SIZE
|
|
|
|
end
|
|
|
|
i = i + 1
|
|
|
|
end
|
|
|
|
)
|
|
|
|
end
|
2019-11-02 20:37:10 +00:00
|
|
|
|
2020-10-20 13:29:28 +00:00
|
|
|
function startCapturingHilbert(area)
|
2022-07-17 14:54:59 +00:00
|
|
|
local entityFile = createOrOpenEntityCaptureFile()
|
|
|
|
|
2019-11-02 20:37:10 +00:00
|
|
|
local ox, oy = GameGetCameraPos()
|
|
|
|
|
2019-11-02 23:58:03 +00:00
|
|
|
local virtualWidth, virtualHeight =
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
|
|
|
|
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
|
|
|
|
|
|
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
|
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Get size of the rectangle in grid/chunk coordinates.
|
2020-10-20 13:29:28 +00:00
|
|
|
local gridLeft = math.floor(area.Left / CAPTURE_GRID_SIZE)
|
|
|
|
local gridTop = math.floor(area.Top / CAPTURE_GRID_SIZE)
|
2022-07-17 12:39:18 +00:00
|
|
|
local gridRight = math.ceil(area.Right / CAPTURE_GRID_SIZE) -- This grid coordinate is not included.
|
|
|
|
local gridBottom = math.ceil(area.Bottom / CAPTURE_GRID_SIZE) -- This grid coordinate is not included.
|
2020-10-20 13:29:28 +00:00
|
|
|
|
|
|
|
-- Edge case
|
|
|
|
if area.Left == area.Right then
|
|
|
|
gridRight = gridLeft
|
|
|
|
end
|
|
|
|
if area.Top == area.Bottom then
|
|
|
|
gridBottom = gridTop
|
|
|
|
end
|
2019-11-02 20:37:10 +00:00
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Size of the grid in chunks.
|
2019-11-02 20:37:10 +00:00
|
|
|
local gridWidth = gridRight - gridLeft
|
|
|
|
local gridHeight = gridBottom - gridTop
|
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Hilbert curve can only fit into a square, so get the longest side.
|
2019-11-02 20:37:10 +00:00
|
|
|
local gridPOTSize = math.ceil(math.log(math.max(gridWidth, gridHeight)) / math.log(2))
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Max size (Already rounded up to the next power of two).
|
2019-11-02 20:37:10 +00:00
|
|
|
local gridMaxSize = math.pow(2, gridPOTSize)
|
|
|
|
|
|
|
|
local t, tLimit = 0, gridMaxSize * gridMaxSize
|
|
|
|
|
|
|
|
UiProgress = {Progress = 0, Max = gridWidth * gridHeight}
|
|
|
|
|
|
|
|
preparePlayer()
|
|
|
|
|
|
|
|
GameSetCameraFree(true)
|
|
|
|
|
2022-07-17 12:39:18 +00:00
|
|
|
-- Coroutine to calculate next coordinate, and trigger screenshots.
|
2019-11-02 20:37:10 +00:00
|
|
|
async(
|
|
|
|
function()
|
|
|
|
while t < tLimit do
|
|
|
|
local hx, hy = mapHilbert(t, gridPOTSize)
|
|
|
|
if hx < gridWidth and hy < gridHeight then
|
|
|
|
local x, y = (hx + gridLeft) * CAPTURE_GRID_SIZE, (hy + gridTop) * CAPTURE_GRID_SIZE
|
2022-07-17 12:39:18 +00:00
|
|
|
x, y = x + 256, y + 256 -- Align screen with ingame chunk grid that is 512x512.
|
|
|
|
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
2019-11-02 20:37:10 +00:00
|
|
|
if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
2022-07-17 14:54:59 +00:00
|
|
|
captureScreenshot(x, y, rx, ry, entityFile)
|
2019-11-02 20:37:10 +00:00
|
|
|
end
|
|
|
|
UiProgress.Progress = UiProgress.Progress + 1
|
|
|
|
end
|
|
|
|
|
|
|
|
t = t + 1
|
|
|
|
end
|
2020-06-01 20:39:00 +00:00
|
|
|
|
|
|
|
UiProgress.Done = true
|
2019-11-02 20:37:10 +00:00
|
|
|
end
|
|
|
|
)
|
|
|
|
end
|