Add setting to delay screen captures

This is useful to let the world populate and let the physics simulation settle down.
This commit is contained in:
David Vogel 2024-02-05 18:10:10 +01:00
parent 24a1615706
commit 9e51538f3f
2 changed files with 66 additions and 36 deletions

View File

@ -32,7 +32,7 @@ Capture.PlayerPathCapturingCtx = Capture.PlayerPathCapturingCtx or ProcessRunner
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle. ---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates. ---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates.
---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically. ---@param pos Vec2? -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically.
---@return Vec2 topLeftCapture ---@return Vec2 topLeftCapture
---@return Vec2 bottomRightCapture ---@return Vec2 bottomRightCapture
---@return Vec2 topLeftWorld ---@return Vec2 topLeftWorld
@ -53,12 +53,13 @@ end
---This will block until all chunks in the virtual rectangle are loaded. ---This will block until all chunks in the virtual rectangle are loaded.
--- ---
---Don't set `ensureLoaded` to true when `pos` is nil! ---Don't set `ensureLoaded` to true when `pos` is nil!
---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport will not be modified. ---@param pos Vec2? -- Position of the viewport center in world coordinates. If set to nil, the viewport will not be modified.
---@param ensureLoaded boolean|nil -- If true, the function will wait until all chunks in the virtual rectangle are loaded. ---@param ensureLoaded boolean? -- If true, the function will wait until all chunks in the virtual rectangle are loaded.
---@param dontOverwrite boolean|nil -- If true, the function will abort if there is already a file with the same coordinates. ---@param dontOverwrite boolean? -- If true, the function will abort if there is already a file with the same coordinates.
---@param ctx ProcessRunnerCtx|nil -- The process runner context this runs in. ---@param ctx ProcessRunnerCtx? -- The process runner context this runs in.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPixelScale) ---@param captureDelay number? -- The number of additional frames to wait before a screen capture.
local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPixelScale, captureDelay)
if outputPixelScale == 0 or outputPixelScale == nil then if outputPixelScale == 0 or outputPixelScale == nil then
outputPixelScale = Coords:PixelScale() outputPixelScale = Coords:PixelScale()
end end
@ -80,9 +81,20 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end end
if pos then CameraAPI.SetPos(pos) end if pos then CameraAPI.SetPos(pos) end
-- Reset the count for the "Waiting for x frames." message in the UI.
if ctx then ctx.state.WaitFrames = 0 end
-- Wait some additional frames.
if captureDelay and captureDelay > 0 then
for _ = 1, captureDelay do
wait(0)
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end
end
end
if ensureLoaded then if ensureLoaded then
local delayFrames = 0 local delayFrames = 0
if ctx then ctx.state.WaitFrames = delayFrames end
repeat repeat
-- Prematurely stop capturing if that is requested by the context. -- Prematurely stop capturing if that is requested by the context.
if ctx and ctx:IsStopping() then return end if ctx and ctx:IsStopping() then return end
@ -92,19 +104,21 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-10, 10), math.random(-10, 10))) end if pos then CameraAPI.SetPos(pos + Vec2(math.random(-10, 10), math.random(-10, 10))) end
wait(0) wait(0)
delayFrames = delayFrames + 1 delayFrames = delayFrames + 1
if ctx then ctx.state.WaitFrames = delayFrames end if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end
if pos then CameraAPI.SetPos(pos) end if pos then CameraAPI.SetPos(pos) end
end end
wait(0) wait(0)
delayFrames = delayFrames + 1 delayFrames = delayFrames + 1
if ctx then ctx.state.WaitFrames = delayFrames end if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end
local topLeftBounds, bottomRightBounds = CameraAPI:Bounds() local topLeftBounds, bottomRightBounds = CameraAPI:Bounds()
until DoesWorldExistAt(topLeftBounds.x, topLeftBounds.y, bottomRightBounds.x, bottomRightBounds.y) until DoesWorldExistAt(topLeftBounds.x, topLeftBounds.y, bottomRightBounds.x, bottomRightBounds.y)
-- Chunks are loaded and will be drawn on the *next* frame. -- Chunks are loaded and will be drawn on the *next* frame.
end end
if ctx then ctx.state.WaitFrames = 0 end
-- Suspend UI drawing for 1 frame. -- Suspend UI drawing for 1 frame.
UI:SuspendDrawing(1) UI:SuspendDrawing(1)
@ -150,8 +164,9 @@ end
---Use `Capture.MapCapturingCtx` to stop, control or view the progress. ---Use `Capture.MapCapturingCtx` to stop, control or view the progress.
---@param origin Vec2 -- Center of the spiral in world pixels. ---@param origin Vec2 -- Center of the spiral in world pixels.
---@param captureGridSize number -- The grid size in world pixels. ---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale) ---@param captureDelay number? -- The number of additional frames to wait before a screen capture.
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory. -- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a") local file = io.open("mods/noita-mapcap/output/nonempty", "a")
@ -175,23 +190,23 @@ function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
repeat repeat
-- +x -- +x
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
pos:Add(Vec2(captureGridSize, 0)) pos:Add(Vec2(captureGridSize, 0))
end end
-- +y -- +y
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
pos:Add(Vec2(0, captureGridSize)) pos:Add(Vec2(0, captureGridSize))
end end
i = i + 1 i = i + 1
-- -x -- -x
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
pos:Add(Vec2(-captureGridSize, 0)) pos:Add(Vec2(-captureGridSize, 0))
end end
-- -y -- -y
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
pos:Add(Vec2(0, -captureGridSize)) pos:Add(Vec2(0, -captureGridSize))
end end
i = i + 1 i = i + 1
@ -213,8 +228,9 @@ end
---@param topLeft Vec2 -- Top left of the to be captured rectangle. ---@param topLeft Vec2 -- Top left of the to be captured rectangle.
---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle. ---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle.
---@param captureGridSize number -- The grid size in world pixels. ---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize, outputPixelScale) ---@param captureDelay number? -- The number of additional frames to wait before a screen capture.
function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory. -- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a") local file = io.open("mods/noita-mapcap/output/nonempty", "a")
@ -258,7 +274,7 @@ function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize
---@type Vec2 ---@type Vec2
local pos = (hilbertPos + gridTopLeft) * captureGridSize local pos = (hilbertPos + gridTopLeft) * captureGridSize
pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell. pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
ctx.state.Current = ctx.state.Current + 1 ctx.state.Current = ctx.state.Current + 1
end end
@ -281,8 +297,9 @@ end
---@param topLeft Vec2 -- Top left of the to be captured rectangle. ---@param topLeft Vec2 -- Top left of the to be captured rectangle.
---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle. ---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle.
---@param captureGridSize number -- The grid size in world pixels. ---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale) ---@param captureDelay number? -- The number of additional frames to wait before a screen capture.
function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory. -- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a") local file = io.open("mods/noita-mapcap/output/nonempty", "a")
@ -315,7 +332,7 @@ function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, o
---@type Vec2 ---@type Vec2
local pos = gridPos * captureGridSize local pos = gridPos * captureGridSize
pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell. pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
captureScreenshot(pos, true, true, ctx, outputPixelScale) captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
ctx.state.Current = ctx.state.Current + 1 ctx.state.Current = ctx.state.Current + 1
end end
end end
@ -333,7 +350,7 @@ end
---Starts the live capturing process. ---Starts the live capturing process.
---Use `Capture.MapCapturingCtx` to stop, control or view the process. ---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingLive(outputPixelScale) function Capture:StartCapturingLive(outputPixelScale)
---Queries the mod settings for the live capture parameters. ---Queries the mod settings for the live capture parameters.
@ -371,7 +388,7 @@ function Capture:StartCapturingLive(outputPixelScale)
if oldPos then distanceSqr = CameraAPI.GetPos():DistanceSqr(oldPos) else distanceSqr = math.huge end if oldPos then distanceSqr = CameraAPI.GetPos():DistanceSqr(oldPos) else distanceSqr = math.huge end
until ctx:IsStopping() or ((delayFrames >= interval or distanceSqr >= maxDistanceSqr) and distanceSqr >= minDistanceSqr) until ctx:IsStopping() or ((delayFrames >= interval or distanceSqr >= maxDistanceSqr) and distanceSqr >= minDistanceSqr)
captureScreenshot(nil, false, false, ctx, outputPixelScale) captureScreenshot(nil, false, false, ctx, outputPixelScale, nil)
oldPos = CameraAPI.GetPos() oldPos = CameraAPI.GetPos()
until ctx:IsStopping() until ctx:IsStopping()
end end
@ -387,7 +404,7 @@ function Capture:StartCapturingLive(outputPixelScale)
end end
---Gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and/or modifies those entities. ---Gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and/or modifies those entities.
---@param file file*|nil ---@param file file*?
---@param modify boolean ---@param modify boolean
---@param x number ---@param x number
---@param y number ---@param y number
@ -510,7 +527,7 @@ local function captureModifyEntities(file, modify, x, y, radius)
end end
--- ---
---@return file*|nil ---@return file*?
local function createOrOpenEntityCaptureFile() local function createOrOpenEntityCaptureFile()
-- Make sure the file exists. -- Make sure the file exists.
local file = io.open("mods/noita-mapcap/output/entities.json", "a") local file = io.open("mods/noita-mapcap/output/entities.json", "a")
@ -570,7 +587,7 @@ function Capture:StartCapturingEntities(store, modify)
end end
---Writes the current player position and other stats onto disk. ---Writes the current player position and other stats onto disk.
---@param file file*|nil ---@param file file*?
---@param pos Vec2 ---@param pos Vec2
---@param oldPos Vec2 ---@param oldPos Vec2
---@param hp number ---@param hp number
@ -603,7 +620,7 @@ local function writePlayerPathEntry(file, pos, oldPos, hp, maxHP, polymorphed)
end end
--- ---
---@return file*|nil ---@return file*?
local function createOrOpenPlayerPathCaptureFile() local function createOrOpenPlayerPathCaptureFile()
-- Make sure the file exists. -- Make sure the file exists.
local file = io.open("mods/noita-mapcap/output/player-path.json", "a") local file = io.open("mods/noita-mapcap/output/player-path.json", "a")
@ -618,8 +635,8 @@ end
---Starts capturing the player path. ---Starts capturing the player path.
---Use `Capture.PlayerPathCapturingCtx` to stop, control or view the progress. ---Use `Capture.PlayerPathCapturingCtx` to stop, control or view the progress.
---@param interval integer|nil -- Wait time between captures in frames. ---@param interval integer? -- Wait time between captures in frames.
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingPlayerPath(interval, outputPixelScale) function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
interval = interval or 20 interval = interval or 20
@ -647,7 +664,7 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
-- It seems that the entity can still be found by the tag, but its components/values can't be accessed anymore. -- It seems that the entity can still be found by the tag, but its components/values can't be accessed anymore.
-- Solution: Don't do that. -- Solution: Don't do that.
---@type NoitaEntity|nil ---@type NoitaEntity?
local playerEntity local playerEntity
-- Try to find the regular player entity. -- Try to find the regular player entity.
@ -714,6 +731,7 @@ function Capture:StartCapturing()
local mode = ModSettingGet("noita-mapcap.capture-mode") local mode = ModSettingGet("noita-mapcap.capture-mode")
local outputPixelScale = ModSettingGet("noita-mapcap.pixel-scale") local outputPixelScale = ModSettingGet("noita-mapcap.pixel-scale")
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size")) local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
local captureDelay = tonumber(ModSettingGet("noita-mapcap.capture-delay"))
if mode == "live" then if mode == "live" then
self:StartCapturingLive(outputPixelScale) self:StartCapturingLive(outputPixelScale)
@ -724,11 +742,11 @@ function Capture:StartCapturing()
local topLeft = Vec2(ModSettingGet("noita-mapcap.area-top-left")) local topLeft = Vec2(ModSettingGet("noita-mapcap.area-top-left"))
local bottomRight = Vec2(ModSettingGet("noita-mapcap.area-bottom-right")) local bottomRight = Vec2(ModSettingGet("noita-mapcap.area-bottom-right"))
self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale) self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
else else
local predefinedArea = Config.CaptureArea[area] local predefinedArea = Config.CaptureArea[area]
if predefinedArea then if predefinedArea then
self:StartCapturingAreaScan(predefinedArea.TopLeft, predefinedArea.BottomRight, captureGridSize, outputPixelScale) self:StartCapturingAreaScan(predefinedArea.TopLeft, predefinedArea.BottomRight, captureGridSize, outputPixelScale, captureDelay)
else else
Message:ShowRuntimeError("PredefinedArea", string.format("Unknown predefined capturing area %q", tostring(area))) Message:ShowRuntimeError("PredefinedArea", string.format("Unknown predefined capturing area %q", tostring(area)))
end end
@ -737,13 +755,13 @@ function Capture:StartCapturing()
local origin = ModSettingGet("noita-mapcap.capture-mode-spiral-origin") local origin = ModSettingGet("noita-mapcap.capture-mode-spiral-origin")
if origin == "custom" then if origin == "custom" then
local originVec = Vec2(ModSettingGet("noita-mapcap.capture-mode-spiral-origin-vector")) local originVec = Vec2(ModSettingGet("noita-mapcap.capture-mode-spiral-origin-vector"))
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay)
elseif origin == "0" then elseif origin == "0" then
local originVec = Vec2(0, 0) local originVec = Vec2(0, 0)
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay)
elseif origin == "current" then elseif origin == "current" then
local originVec = CameraAPI:GetPos() local originVec = CameraAPI:GetPos()
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay)
else else
Message:ShowRuntimeError("SpiralOrigin", string.format("Unknown spiral origin %q", tostring(origin))) Message:ShowRuntimeError("SpiralOrigin", string.format("Unknown spiral origin %q", tostring(origin)))
end end

View File

@ -157,6 +157,18 @@ modSettings = {
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
change_fn = roundChange, change_fn = roundChange,
}, },
{
id = "capture-delay",
ui_name = "Capture delay",
ui_description = "Additional delay before a screen capture is taken.\nThis can help the world to be populated, and the physics simulation to settle down.\nA setting of 0 means that the screenshot is taken as soon as possible.\n \nUse a value of 10 for a good result without slowing the capture process down too much.",
value_default = 0,
value_min = 0,
value_max = 60,
value_display_multiplier = 1,
value_display_formatting = " $0 frames",
scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
},
{ {
id = "custom-resolution-live", id = "custom-resolution-live",
ui_name = "Use custom resolution", ui_name = "Use custom resolution",