mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-18 17:17:31 +00:00
Rewrite capturing process
- Add process runner library that handles any processes - Add global namespaces for main files - Add config.lua and move capture area definitions into there - Remove CAPTURE_PIXEL_SIZE and CAPTURE_GRID_SIZE variables - Rename topLeftWorld in screen-capture.lua to topLeftOutput - Rewrite all capturing processes and let them use the process runner - Put UI redrawing outside of coroutine - Clean up not needed stuff and get rid of most global variables - Change how the UI is suspended when taking screenshots - Start rewriting UI stuff - Reformat ui.lua - Fix comments
This commit is contained in:
parent
635085f923
commit
a2f5efc9e6
@ -12,108 +12,296 @@ local Coords = require("coordinates")
|
|||||||
local EntityAPI = require("noita-api.entity")
|
local EntityAPI = require("noita-api.entity")
|
||||||
local Hilbert = require("hilbert-curve")
|
local Hilbert = require("hilbert-curve")
|
||||||
local JSON = require("noita-api.json")
|
local JSON = require("noita-api.json")
|
||||||
local ScreenCapture = require("screen-capture")
|
|
||||||
local Utils = require("noita-api.utils")
|
|
||||||
local Vec2 = require("noita-api.vec2")
|
|
||||||
local MonitorStandby = require("monitor-standby")
|
local MonitorStandby = require("monitor-standby")
|
||||||
|
local ProcessRunner = require("process-runner")
|
||||||
|
local ScreenCapture = require("screen-capture")
|
||||||
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
local Utils = require("noita-api.utils")
|
||||||
|
|
||||||
|
------------------
|
||||||
|
-- Global stuff --
|
||||||
|
------------------
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Code --
|
-- Code --
|
||||||
----------
|
----------
|
||||||
|
|
||||||
CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio.
|
Capture.MapCapturingCtx = Capture.MapCapturingCtx or ProcessRunner.New()
|
||||||
CAPTURE_GRID_SIZE = 512 -- in virtual (world) pixels. There will always be exactly 4 images overlapping if the virtual resolution is 1024x1024.
|
Capture.EntityCapturingCtx = Capture.EntityCapturingCtx or ProcessRunner.New()
|
||||||
|
|
||||||
-- "Base layout" (Base layout. Every part outside this is based on a similar layout, but uses different materials/seeds)
|
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
|
||||||
CAPTURE_AREA_BASE_LAYOUT = {
|
---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates.
|
||||||
Left = -17920, -- in virtual (world) pixels.
|
---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically.
|
||||||
Top = -7168, -- in virtual (world) pixels.
|
---@return Vec2 topLeftCapture
|
||||||
Right = 17920, -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
---@return Vec2 bottomRightCapture
|
||||||
Bottom = 17408 -- in virtual (world) pixels. (Coordinate is not included in the rectangle)
|
---@return Vec2 topLeftWorld
|
||||||
}
|
---@return Vec2 bottomRightWorld
|
||||||
|
local function calculateCaptureRectangle(pos)
|
||||||
|
local topLeft, bottomRight = Coords:ValidRenderingRect()
|
||||||
|
|
||||||
-- "Main world" (The main world with 3 parts: sky, normal and hell)
|
-- Convert valid rendering rectangle into world coordinates, and round it towards the window center.
|
||||||
CAPTURE_AREA_MAIN_WORLD = {
|
local topLeftWorld, bottomRightWorld = Coords:ToWorld(topLeft, pos):Rounded("ceil"), Coords:ToWorld(bottomRight, pos):Rounded("floor")
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- "Extended" (Main world + a fraction of the parallel worlds to the left and right)
|
-- Convert back into window coordinates, and round to nearest.
|
||||||
CAPTURE_AREA_EXTENDED = {
|
local topLeftCapture, bottomRightCapture = Coords:ToWindow(topLeftWorld, pos):Rounded(), Coords:ToWindow(bottomRightWorld, pos):Rounded()
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
local componentTypeNamesToDisable = {
|
return topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld
|
||||||
"AnimalAIComponent",
|
|
||||||
"SimplePhysicsComponent",
|
|
||||||
"CharacterPlatformingComponent",
|
|
||||||
"WormComponent",
|
|
||||||
"WormAIComponent",
|
|
||||||
"CameraBoundComponent", -- Disabling this component will prevent entites from being killed/reset when they go offscreen. If they are reset, the "MapCaptured" tag will be gone and we capture these entities multiple times. This has some side effects, like longleg.xml and zombie_weak.xml will respawn every revisit, as the spawner doesn't get deleted.
|
|
||||||
--"PhysicsBodyCollisionDamageComponent",
|
|
||||||
--"ExplodeOnDamageComponent",
|
|
||||||
--"DamageModelComponent",
|
|
||||||
--"SpriteOffsetAnimatorComponent",
|
|
||||||
--"MaterialInventoryComponent",
|
|
||||||
--"LuaComponent",
|
|
||||||
--"PhysicsBody2Component", -- Disabling will hide barrels and similar stuff, also triggers an assertion.
|
|
||||||
--"PhysicsBodyComponent",
|
|
||||||
--"VelocityComponent", -- Disabling this component may cause a "...\component_updators\advancedfishai_system.cpp at line 107" exception.
|
|
||||||
--"SpriteComponent",
|
|
||||||
--"AudioComponent",
|
|
||||||
}
|
|
||||||
|
|
||||||
---
|
|
||||||
---@return file*|nil
|
|
||||||
local function createOrOpenEntityCaptureFile()
|
|
||||||
-- Make sure the file exists.
|
|
||||||
local file = io.open("mods/noita-mapcap/output/entities.json", "a")
|
|
||||||
if file ~= nil then file:close() end
|
|
||||||
|
|
||||||
-- Create or reopen entities CSV file.
|
|
||||||
file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0.
|
|
||||||
if file == nil then return nil end
|
|
||||||
|
|
||||||
return file
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---captureEntities gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and modifies those entities.
|
---Captures a screenshot at the given position in world coordinates.
|
||||||
---@param entityFile file*|nil
|
---This will block until all chunks in the virtual rectangle are loaded.
|
||||||
|
---
|
||||||
|
---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 ensureLoaded boolean|nil -- 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 ctx ProcessRunnerCtx|nil -- The process runner context this runs in.
|
||||||
|
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
|
||||||
|
local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPixelScale)
|
||||||
|
outputPixelScale = outputPixelScale or 0
|
||||||
|
|
||||||
|
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
|
||||||
|
|
||||||
|
---Top left in output coordinates.
|
||||||
|
---@type Vec2
|
||||||
|
local outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
||||||
|
|
||||||
|
-- Check if the file exists, and if we are allowed to overwrite it.
|
||||||
|
if dontOverwrite and Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", outputTopLeft.x, outputTopLeft.y)) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if pos then CameraAPI.SetPos(pos) end
|
||||||
|
if ensureLoaded then
|
||||||
|
local delayFrames = 0
|
||||||
|
repeat
|
||||||
|
-- Prematurely stop capturing if that is requested by the context.
|
||||||
|
if ctx and ctx:IsStopping() then return end
|
||||||
|
|
||||||
|
if delayFrames > 100 then
|
||||||
|
-- Wiggle the screen a bit, as chunks sometimes don't want to load.
|
||||||
|
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-100, 100), math.random(-100, 100))) end
|
||||||
|
wait(0)
|
||||||
|
delayFrames = delayFrames + 1
|
||||||
|
if pos then CameraAPI.SetPos(pos) end
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(0)
|
||||||
|
delayFrames = delayFrames + 1
|
||||||
|
|
||||||
|
until DoesWorldExistAt(topLeftWorld.x, topLeftWorld.y, bottomRightWorld.x, bottomRightWorld.y)
|
||||||
|
-- Chunks are loaded and will be drawn on the *next* frame.
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Suspend UI drawing for 1 frame.
|
||||||
|
UI.SuspendDrawing(1)
|
||||||
|
|
||||||
|
wait(0)
|
||||||
|
|
||||||
|
-- Fetch coordinates again, as they may have changed.
|
||||||
|
if not pos then
|
||||||
|
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
|
||||||
|
outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The top left world position needs to be upscaled by the pixel scale.
|
||||||
|
-- Otherwise it's not possible to stitch the images correctly.
|
||||||
|
if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then
|
||||||
|
error(string.format("failed to capture screenshot"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset monitor and PC standby every screenshot.
|
||||||
|
MonitorStandby.ResetTimer()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Map capture process runner context error handler callback. Just rolls off the tongue.
|
||||||
|
---@param err string
|
||||||
|
---@param scope "init"|"do"|"end"
|
||||||
|
local function mapCapturingCtxErrHandler(err, scope)
|
||||||
|
print(string.format("Failed to capture map: %s", err))
|
||||||
|
-- TODO: Forward error to user interface
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts the capturing process in a spiral around origin.
|
||||||
|
---Use `Capture.MapCapturingCtx` to stop, control or view the progress.
|
||||||
|
---@param origin Vec2 -- Center of the spiral in world pixels.
|
||||||
|
---@param captureGridSize number -- The grid size in world pixels.
|
||||||
|
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
|
||||||
|
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
|
||||||
|
---Origin rounded to capture grid.
|
||||||
|
---@type Vec2
|
||||||
|
local origin = (origin / captureGridSize):Rounded("Floor") * captureGridSize
|
||||||
|
|
||||||
|
---The position in world coordinates.
|
||||||
|
---Centered to chunks.
|
||||||
|
---@type Vec2
|
||||||
|
local pos = origin + Vec2(256, 256) -- TODO: Align chunks with top left pixel
|
||||||
|
|
||||||
|
---Process main callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleDo(ctx)
|
||||||
|
CameraAPI.SetCameraFree(true)
|
||||||
|
|
||||||
|
local i = 1
|
||||||
|
repeat
|
||||||
|
-- +x
|
||||||
|
for _ = 1, i, 1 do
|
||||||
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
|
pos:Add(Vec2(captureGridSize, 0))
|
||||||
|
end
|
||||||
|
-- +y
|
||||||
|
for _ = 1, i, 1 do
|
||||||
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
|
pos:Add(Vec2(0, captureGridSize))
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
-- -x
|
||||||
|
for _ = 1, i, 1 do
|
||||||
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
|
pos:Add(Vec2(-captureGridSize, 0))
|
||||||
|
end
|
||||||
|
-- -y
|
||||||
|
for _ = 1, i, 1 do
|
||||||
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
|
pos:Add(Vec2(0, -captureGridSize))
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
until ctx:IsStopping()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run process, if there is no other running right now.
|
||||||
|
self.MapCapturingCtx:Run(nil, handleDo, nil, mapCapturingCtxErrHandler)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts the capturing process of the given area.
|
||||||
|
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
|
||||||
|
---@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 captureGridSize number -- The grid size in world pixels.
|
||||||
|
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
|
||||||
|
function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outputPixelScale)
|
||||||
|
---The rectangle in grid coordinates.
|
||||||
|
---@type Vec2, Vec2
|
||||||
|
local gridTopLeft, gridBottomRight = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
|
||||||
|
|
||||||
|
-- Handle edge cases.
|
||||||
|
if topLeft.x == bottomRight.x then gridBottomRight.x = gridTopLeft.x end
|
||||||
|
if topLeft.y == bottomRight.y then gridBottomRight.y = gridTopLeft.y end
|
||||||
|
|
||||||
|
---Size of the rectangle in grid coordinates.
|
||||||
|
---@type Vec2
|
||||||
|
local gridSize = gridBottomRight - gridTopLeft
|
||||||
|
|
||||||
|
-- Hilbert curve can only fit into a square, so get the longest side.
|
||||||
|
local gridPOTSize = math.ceil(math.log(math.max(gridSize.x, gridSize.y)) / math.log(2))
|
||||||
|
|
||||||
|
-- Max size (Already rounded up to the next power of two).
|
||||||
|
local gridMaxSize = math.pow(2, gridPOTSize)
|
||||||
|
|
||||||
|
local t, tLimit = 0, gridMaxSize * gridMaxSize
|
||||||
|
|
||||||
|
---Process main callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleDo(ctx)
|
||||||
|
CameraAPI.SetCameraFree(true)
|
||||||
|
ctx.progressEnd = gridSize.x * gridSize.y
|
||||||
|
|
||||||
|
while t < tLimit do
|
||||||
|
-- Prematurely stop capturing if that is requested by the context.
|
||||||
|
if ctx:IsStopping() then return end
|
||||||
|
|
||||||
|
---Position in grid coordinates.
|
||||||
|
---@type Vec2
|
||||||
|
local hilbertPos = Vec2(Hilbert.Map(t, gridPOTSize))
|
||||||
|
if hilbertPos.x < gridSize.x and hilbertPos.y < gridSize.y then
|
||||||
|
---Position in world coordinates.
|
||||||
|
---@type Vec2
|
||||||
|
local pos = (hilbertPos + gridTopLeft) * captureGridSize
|
||||||
|
pos:Add(Vec2(256, 256)) -- Move to chunk center -- TODO: Align chunks with top left pixel
|
||||||
|
captureScreenshot(pos, true, true, ctx, outputPixelScale)
|
||||||
|
ctx.progressCurrent = ctx.progressCurrent + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
t = t + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run process, if there is no other running right now.
|
||||||
|
self.MapCapturingCtx:Run(nil, handleDo, nil, mapCapturingCtxErrHandler)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts the live capturing process.
|
||||||
|
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
|
||||||
|
---@param interval integer|nil -- The interval length in frames. Defaults to 60.
|
||||||
|
---@param minDistance number|nil -- The minimum distance between screenshots. This will prevent screenshots if the player doesn't move much.
|
||||||
|
---@param maxDistance number|nil -- The maximum distance between screenshots. This will allow more screenshots per interval if the player moves fast.
|
||||||
|
---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
|
||||||
|
function Capture:StartCapturingLive(interval, minDistance, maxDistance, outputPixelScale)
|
||||||
|
interval = interval or 60
|
||||||
|
minDistance = minDistance or 10
|
||||||
|
maxDistance = maxDistance or 50
|
||||||
|
|
||||||
|
---Process main callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleDo(ctx)
|
||||||
|
local oldPos
|
||||||
|
local minDistanceSqr, maxDistanceSqr = minDistance ^ 2, maxDistance ^ 2
|
||||||
|
|
||||||
|
repeat
|
||||||
|
-- Wait until we are allowed to take a new screenshot.
|
||||||
|
local delayFrames = 0
|
||||||
|
repeat
|
||||||
|
wait(0)
|
||||||
|
delayFrames = delayFrames + 1
|
||||||
|
|
||||||
|
local distanceSqr
|
||||||
|
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)
|
||||||
|
|
||||||
|
captureScreenshot(nil, false, false, ctx, outputPixelScale)
|
||||||
|
oldPos = CameraAPI.GetPos()
|
||||||
|
until ctx:IsStopping()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run process, if there is no other running right now.
|
||||||
|
self.MapCapturingCtx:Run(nil, handleDo, nil, mapCapturingCtxErrHandler)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and modifies those entities.
|
||||||
|
---@param file file*|nil
|
||||||
|
---@param modify boolean
|
||||||
---@param x number
|
---@param x number
|
||||||
---@param y number
|
---@param y number
|
||||||
---@param radius number
|
---@param radius number
|
||||||
local function captureEntities(entityFile, x, y, radius)
|
local function captureModifyEntities(file, modify, x, y, radius)
|
||||||
if not entityFile then return end
|
|
||||||
|
|
||||||
local entities = EntityAPI.GetInRadius(x, y, radius)
|
local entities = EntityAPI.GetInRadius(x, y, radius)
|
||||||
for _, entity in ipairs(entities) do
|
for _, entity in ipairs(entities) do
|
||||||
-- Get to the root entity, as we are exporting entire entity trees.
|
-- Get to the root entity, as we are exporting entire entity trees.
|
||||||
local rootEntity = entity:GetRootEntity()
|
local rootEntity = entity:GetRootEntity() or entity
|
||||||
|
|
||||||
-- Make sure to only export entities when they are encountered the first time.
|
-- Make sure to only export entities when they are encountered the first time.
|
||||||
if not rootEntity:HasTag("MapCaptured") then
|
if file and not rootEntity:HasTag("MapCaptured") then
|
||||||
--print(rootEntity:GetFilename(), "got captured!")
|
--print(rootEntity:GetFilename(), "got captured!")
|
||||||
|
|
||||||
-- Some hacky way to generate valid JSON that doesn't break when the game crashes.
|
-- Some hacky way to generate valid JSON that doesn't break when the game crashes.
|
||||||
-- Well, as long as it does not crash between write and flush.
|
-- Well, as long as it does not crash between write and flush.
|
||||||
if entityFile:seek("end") == 0 then
|
if file:seek("end") == 0 then
|
||||||
-- First line.
|
-- First line.
|
||||||
entityFile:write("[\n\t", JSON.Marshal(rootEntity), "\n", "]")
|
file:write("[\n\t", JSON.Marshal(rootEntity), "\n", "]")
|
||||||
else
|
else
|
||||||
-- Following lines.
|
-- Following lines.
|
||||||
entityFile:seek("end", -2) -- Seek a few bytes back, so we can overwrite some stuff.
|
file:seek("end", -2) -- Seek a few bytes back, so we can overwrite some stuff.
|
||||||
entityFile:write(",\n\t", JSON.Marshal(rootEntity), "\n", "]")
|
file:write(",\n\t", JSON.Marshal(rootEntity), "\n", "]")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Prevent recapturing.
|
-- Prevent recapturing.
|
||||||
rootEntity:AddTag("MapCaptured")
|
rootEntity:AddTag("MapCaptured")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make sure to only modify entities when they are encountered the first time.
|
||||||
|
if modify and not rootEntity:HasTag("MapModified") then
|
||||||
-- Disable some components.
|
-- Disable some components.
|
||||||
for _, componentTypeName in ipairs(componentTypeNamesToDisable) do
|
for _, componentTypeName in ipairs(Config.ComponentsToDisable) do
|
||||||
local components = rootEntity:GetComponents(componentTypeName)
|
local components = rootEntity:GetComponents(componentTypeName)
|
||||||
for _, component in ipairs(components) do
|
for _, component in ipairs(components) do
|
||||||
rootEntity:SetComponentsEnabled(component, false)
|
rootEntity:SetComponentsEnabled(component, false)
|
||||||
@ -177,271 +365,72 @@ local function captureEntities(entityFile, x, y, radius)
|
|||||||
component:SetValue("kill_when_empty", false)
|
component:SetValue("kill_when_empty", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Prevent it from being modified again.
|
||||||
|
rootEntity:AddTag("MapModified")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Ensure everything is written to disk before noita decides to crash.
|
-- Ensure everything is written to disk before noita decides to crash.
|
||||||
entityFile:flush()
|
if file then
|
||||||
|
file:flush()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function DebugEntityCapture()
|
|
||||||
local entityFile = createOrOpenEntityCaptureFile()
|
|
||||||
|
|
||||||
-- Coroutine to capture all entities around the viewport every frame.
|
|
||||||
async_loop(function()
|
|
||||||
local x, y = GameGetCameraPos() -- Returns the virtual coordinates of the screen center.
|
|
||||||
-- Call the protected function and catch any errors.
|
|
||||||
local ok, err = pcall(captureEntities, entityFile, x, y, 5000)
|
|
||||||
if not ok then
|
|
||||||
print(string.format("Entity capture error: %s", err))
|
|
||||||
end
|
|
||||||
wait(0)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
|
|
||||||
---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically.
|
|
||||||
---@return Vec2 topLeftCapture
|
|
||||||
---@return Vec2 bottomRightCapture
|
|
||||||
---@return Vec2 topLeftWorld
|
|
||||||
---@return Vec2 bottomRightWorld
|
|
||||||
local function GenerateCaptureRectangle(pos)
|
|
||||||
local topLeft, bottomRight = Coords:ValidRenderingRect()
|
|
||||||
|
|
||||||
-- Convert valid rendering rectangle into world coordinates, and round it towards the window center.
|
|
||||||
local topLeftWorld, bottomRightWorld = Coords:ToWorld(topLeft, pos):Rounded("ceil"), Coords:ToWorld(bottomRight, pos):Rounded("floor")
|
|
||||||
|
|
||||||
-- Convert back into window coordinates, and round to nearest.
|
|
||||||
local topLeftCapture, bottomRightCapture = Coords:ToWindow(topLeftWorld, pos):Rounded(), Coords:ToWindow(bottomRightWorld, pos):Rounded()
|
|
||||||
|
|
||||||
return topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld
|
|
||||||
end
|
|
||||||
|
|
||||||
---Captures a screenshot at the given position in world coordinates.
|
|
||||||
---This will block until all chunks in the virtual rectangle are loaded.
|
|
||||||
---
|
---
|
||||||
---Don't set `ensureLoaded` to true when `pos` is nil!
|
---@return file*|nil
|
||||||
---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport will not be modified.
|
local function createOrOpenEntityCaptureFile()
|
||||||
---@param ensureLoaded boolean|nil -- If true, the function will wait until all chunks in the virtual rectangle are loaded.
|
-- Make sure the file exists.
|
||||||
local function captureScreenshot(pos, ensureLoaded)
|
local file = io.open("mods/noita-mapcap/output/entities.json", "a")
|
||||||
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = GenerateCaptureRectangle(pos)
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
UiCaptureDelay = 0
|
-- Create or reopen entities CSV file.
|
||||||
if pos then CameraAPI.SetPos(pos) end
|
file = io.open("mods/noita-mapcap/output/entities.json", "r+b") -- Open for reading (r) and writing (+) in binary mode. r+b will not truncate the file to 0.
|
||||||
if ensureLoaded then
|
if file == nil then return nil end
|
||||||
|
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts entity capturing and modification.
|
||||||
|
---Use `Capture.EntityCapturingCtx` to stop, control or view the progress.
|
||||||
|
---@param store boolean -- Will create a file and write all encountered entities into it.
|
||||||
|
---@param modify boolean -- Will modify all encountered entities.
|
||||||
|
function Capture:StartCapturingEntities(store, modify)
|
||||||
|
-- There is nothing to capture, don't start anything.
|
||||||
|
if not store and not modify then return end
|
||||||
|
|
||||||
|
local file
|
||||||
|
|
||||||
|
---Process initialization callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleInit(ctx)
|
||||||
|
-- Create output file if requested.
|
||||||
|
file = store and createOrOpenEntityCaptureFile() or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Process main callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
|
local function handleDo(ctx)
|
||||||
repeat
|
repeat
|
||||||
if UiCaptureDelay > 100 then
|
local pos, radius = CameraAPI:GetPos(), 5000 -- Returns the virtual coordinates of the screen center.
|
||||||
-- Wiggle the screen a bit, as chunks sometimes don't want to load.
|
captureModifyEntities(file, modify, pos.x, pos.y, radius)
|
||||||
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-100, 100), math.random(-100, 100))) end
|
|
||||||
DrawUI()
|
|
||||||
wait(0)
|
|
||||||
UiCaptureDelay = UiCaptureDelay + 1
|
|
||||||
if pos then CameraAPI.SetPos(pos) end
|
|
||||||
end
|
|
||||||
|
|
||||||
DrawUI()
|
|
||||||
wait(0)
|
wait(0)
|
||||||
UiCaptureDelay = UiCaptureDelay + 1
|
until ctx:IsStopping()
|
||||||
|
|
||||||
until DoesWorldExistAt(topLeftWorld.x, topLeftWorld.y, bottomRightWorld.x, bottomRightWorld.y)
|
|
||||||
-- Chunks are loaded an will be drawn on the *next* frame.
|
|
||||||
end
|
end
|
||||||
|
|
||||||
wait(0) -- Without this line empty chunks may still appear, also it's needed for the UI to disappear.
|
---Process end callback.
|
||||||
|
---@param ctx ProcessRunnerCtx
|
||||||
-- Fetch coordinates again, as they may have changed.
|
local function handleEnd(ctx)
|
||||||
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = GenerateCaptureRectangle(pos)
|
if file then file:close() end
|
||||||
|
|
||||||
local outputPixelScale = 1
|
|
||||||
|
|
||||||
-- The top left world position needs to be upscaled by the pixel scale.
|
|
||||||
-- Otherwise it's not possible to stitch the images correctly.
|
|
||||||
if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, (topLeftWorld * outputPixelScale):Rounded(), (bottomRightWorld - topLeftWorld) * outputPixelScale) then
|
|
||||||
UiCaptureProblem = "Screen capture failed. Please restart Noita."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reset monitor and PC standby every screenshot.
|
---Error handler callback.
|
||||||
MonitorStandby.ResetTimer()
|
---@param err string
|
||||||
end
|
---@param scope "init"|"do"|"end"
|
||||||
|
local function handleErr(err, scope)
|
||||||
function startCapturingSpiral()
|
print(string.format("Failed to capture entities: %s", err))
|
||||||
local entityFile = createOrOpenEntityCaptureFile()
|
|
||||||
|
|
||||||
local ox, oy = GameGetCameraPos() -- Returns the virtual coordinates of the screen center.
|
|
||||||
ox, oy = math.floor(ox / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE, math.floor(oy / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE
|
|
||||||
ox, oy = ox + 256, oy + 256 -- Align screen with ingame chunk grid that is 512x512.
|
|
||||||
local x, y = ox, oy
|
|
||||||
|
|
||||||
local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
||||||
|
|
||||||
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
|
|
||||||
|
|
||||||
GameSetCameraFree(true)
|
|
||||||
|
|
||||||
-- Coroutine to capture all entities around the viewport every frame.
|
|
||||||
async_loop(function()
|
|
||||||
local x, y = GameGetCameraPos() -- Returns the virtual coordinates of the screen center.
|
|
||||||
-- Call the protected function and catch any errors.
|
|
||||||
local ok, err = pcall(captureEntities, entityFile, x, y, 5000)
|
|
||||||
if not ok then
|
|
||||||
print(string.format("Entity capture error: %s", err))
|
|
||||||
end
|
|
||||||
wait(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Coroutine to calculate next coordinate, and trigger screenshots.
|
|
||||||
local i = 1
|
|
||||||
async_loop(
|
|
||||||
function()
|
|
||||||
-- +x
|
|
||||||
for i = 1, i, 1 do
|
|
||||||
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
|
||||||
if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
|
||||||
captureScreenshot(Vec2(x, y), true)
|
|
||||||
end
|
|
||||||
x, y = x + CAPTURE_GRID_SIZE, y
|
|
||||||
end
|
|
||||||
-- +y
|
|
||||||
for i = 1, i, 1 do
|
|
||||||
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
|
||||||
if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
|
||||||
captureScreenshot(Vec2(x, y), true)
|
|
||||||
end
|
|
||||||
x, y = x, y + CAPTURE_GRID_SIZE
|
|
||||||
end
|
|
||||||
i = i + 1
|
|
||||||
-- -x
|
|
||||||
for i = 1, i, 1 do
|
|
||||||
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
|
||||||
if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
|
||||||
captureScreenshot(Vec2(x, y), true)
|
|
||||||
end
|
|
||||||
x, y = x - CAPTURE_GRID_SIZE, y
|
|
||||||
end
|
|
||||||
-- -y
|
|
||||||
for i = 1, i, 1 do
|
|
||||||
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE
|
|
||||||
if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
|
||||||
captureScreenshot(Vec2(x, y), true)
|
|
||||||
end
|
|
||||||
x, y = x, y - CAPTURE_GRID_SIZE
|
|
||||||
end
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
function startCapturingHilbert(area)
|
|
||||||
local entityFile = createOrOpenEntityCaptureFile()
|
|
||||||
|
|
||||||
local ox, oy = GameGetCameraPos()
|
|
||||||
|
|
||||||
local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
||||||
|
|
||||||
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
|
|
||||||
|
|
||||||
-- Get size of the rectangle in grid/chunk coordinates.
|
|
||||||
local gridLeft = math.floor(area.Left / CAPTURE_GRID_SIZE)
|
|
||||||
local gridTop = math.floor(area.Top / CAPTURE_GRID_SIZE)
|
|
||||||
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.
|
|
||||||
|
|
||||||
-- Edge case
|
|
||||||
if area.Left == area.Right then
|
|
||||||
gridRight = gridLeft
|
|
||||||
end
|
|
||||||
if area.Top == area.Bottom then
|
|
||||||
gridBottom = gridTop
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Size of the grid in chunks.
|
-- Run process, if there is no other running right now.
|
||||||
local gridWidth = gridRight - gridLeft
|
self.EntityCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
||||||
local gridHeight = gridBottom - gridTop
|
|
||||||
|
|
||||||
-- Hilbert curve can only fit into a square, so get the longest side.
|
|
||||||
local gridPOTSize = math.ceil(math.log(math.max(gridWidth, gridHeight)) / math.log(2))
|
|
||||||
-- Max size (Already rounded up to the next power of two).
|
|
||||||
local gridMaxSize = math.pow(2, gridPOTSize)
|
|
||||||
|
|
||||||
local t, tLimit = 0, gridMaxSize * gridMaxSize
|
|
||||||
|
|
||||||
UiProgress = { Progress = 0, Max = gridWidth * gridHeight }
|
|
||||||
|
|
||||||
GameSetCameraFree(true)
|
|
||||||
|
|
||||||
-- Coroutine to capture all entities around the viewport every frame.
|
|
||||||
async_loop(function()
|
|
||||||
local x, y = GameGetCameraPos() -- Returns the virtual coordinates of the screen center.
|
|
||||||
-- Call the protected function and catch any errors.
|
|
||||||
local ok, err = pcall(captureEntities, entityFile, x, y, 5000)
|
|
||||||
if not ok then
|
|
||||||
print(string.format("Entity capture error: %s", err))
|
|
||||||
end
|
|
||||||
wait(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Coroutine to calculate next coordinate, and trigger screenshots.
|
|
||||||
async(
|
|
||||||
function()
|
|
||||||
while t < tLimit do
|
|
||||||
local hx, hy = Hilbert.Map(t, gridPOTSize)
|
|
||||||
if hx < gridWidth and hy < gridHeight then
|
|
||||||
local x, y = (hx + gridLeft) * CAPTURE_GRID_SIZE, (hy + gridTop) * CAPTURE_GRID_SIZE
|
|
||||||
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
|
|
||||||
if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
|
|
||||||
captureScreenshot(Vec2(x, y), true)
|
|
||||||
end
|
|
||||||
UiProgress.Progress = UiProgress.Progress + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
t = t + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
UiProgress.Done = true
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
---Starts the capturing screenshots at the given interval.
|
|
||||||
---This will not move the viewport and is meant to capture the player while playing.
|
|
||||||
---@param interval integer|nil -- The interval length in frames. Defaults to 60.
|
|
||||||
---@param minDistance number|nil -- The minimum distance between screenshots. This will prevent screenshots if the player doesn't move much.
|
|
||||||
---@param maxDistance number|nil -- The maximum distance between screenshots. This will allow more screenshots per interval if the player moves fast.
|
|
||||||
function StartCapturingLive(interval, minDistance, maxDistance)
|
|
||||||
interval = interval or 60
|
|
||||||
minDistance = minDistance or 10
|
|
||||||
maxDistance = maxDistance or 50
|
|
||||||
|
|
||||||
local minDistanceSqr, maxDistanceSqr = minDistance ^ 2, maxDistance ^ 2
|
|
||||||
|
|
||||||
--local entityFile = createOrOpenEntityCaptureFile()
|
|
||||||
|
|
||||||
-- Coroutine to capture all entities around the viewport every frame.
|
|
||||||
--[[async_loop(function()
|
|
||||||
local pos = CameraAPI:GetPos() -- Returns the virtual coordinates of the screen center.
|
|
||||||
-- Call the protected function and catch any errors.
|
|
||||||
local ok, err = pcall(captureEntities, entityFile, pos.x, pos.y, 5000)
|
|
||||||
if not ok then
|
|
||||||
print(string.format("Entity capture error: %s", err))
|
|
||||||
end
|
|
||||||
wait(0)
|
|
||||||
end)]]
|
|
||||||
|
|
||||||
local oldPos
|
|
||||||
|
|
||||||
-- Coroutine to calculate next coordinate, and trigger screenshots.
|
|
||||||
async_loop(function()
|
|
||||||
local frames = 0
|
|
||||||
repeat
|
|
||||||
wait(0)
|
|
||||||
frames = frames + 1
|
|
||||||
|
|
||||||
local distanceSqr
|
|
||||||
if oldPos then distanceSqr = CameraAPI.GetPos():DistanceSqr(oldPos) else distanceSqr = math.huge end
|
|
||||||
until (frames >= interval or distanceSqr >= maxDistanceSqr) and distanceSqr >= minDistanceSqr
|
|
||||||
|
|
||||||
captureScreenshot()
|
|
||||||
oldPos = CameraAPI.GetPos()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
46
files/config.lua
Normal file
46
files/config.lua
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
-- Copyright (c) 2022 David Vogel
|
||||||
|
--
|
||||||
|
-- This software is released under the MIT License.
|
||||||
|
-- https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
|
||||||
|
Config.ComponentsToDisable = {
|
||||||
|
"AnimalAIComponent",
|
||||||
|
"SimplePhysicsComponent",
|
||||||
|
"CharacterPlatformingComponent",
|
||||||
|
"WormComponent",
|
||||||
|
"WormAIComponent",
|
||||||
|
"CameraBoundComponent", -- Disabling this component will prevent entites from being killed/reset when they go offscreen. If they are reset, all tags will be reset and we may capture these entities multiple times. This has some side effects, like longleg.xml and zombie_weak.xml will respawn every revisit, as the spawner doesn't get deleted.
|
||||||
|
--"PhysicsBodyCollisionDamageComponent",
|
||||||
|
--"ExplodeOnDamageComponent",
|
||||||
|
--"DamageModelComponent",
|
||||||
|
--"SpriteOffsetAnimatorComponent",
|
||||||
|
--"MaterialInventoryComponent",
|
||||||
|
--"LuaComponent",
|
||||||
|
--"PhysicsBody2Component", -- Disabling will hide barrels and similar stuff, also triggers an assertion.
|
||||||
|
--"PhysicsBodyComponent",
|
||||||
|
--"VelocityComponent", -- Disabling this component may cause a "...\component_updators\advancedfishai_system.cpp at line 107" exception.
|
||||||
|
--"SpriteComponent",
|
||||||
|
--"AudioComponent",
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.CaptureArea = {
|
||||||
|
-- Base layout: Every part outside this is based on a similar layout, but uses different materials/seeds.
|
||||||
|
["1x1"] = {
|
||||||
|
TopLeft = Vec2(-17920, -7168), -- in world coordinates.
|
||||||
|
BottomRight = Vec2(17920, 17408), -- in world coordinates. This pixel is not included in the rectangle.
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Main world: The main world with 3 parts: sky, normal and hell.
|
||||||
|
["1x3"] = {
|
||||||
|
TopLeft = Vec2(-17920, -31744), -- in world coordinates.
|
||||||
|
BottomRight = Vec2(17920, 41984), -- in world coordinates. This pixel is not included in the rectangle.
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Extended: Main world + a fraction of the parallel worlds to the left and right.
|
||||||
|
["1.5x3"] = {
|
||||||
|
TopLeft = Vec2(-25600, -31744), -- in world coordinates.
|
||||||
|
BottomRight = Vec2(25600, 41984), -- in world coordinates. This pixel is not included in the rectangle.
|
||||||
|
},
|
||||||
|
}
|
@ -11,7 +11,7 @@ ffi.cdef([[
|
|||||||
int SetThreadExecutionState(int esFlags);
|
int SetThreadExecutionState(int esFlags);
|
||||||
]])
|
]])
|
||||||
|
|
||||||
-- Reset computer and monitor standby timer
|
-- Reset computer and monitor standby timer.
|
||||||
function MonitorStandby.ResetTimer()
|
function MonitorStandby.ResetTimer()
|
||||||
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
||||||
end
|
end
|
||||||
|
117
files/libraries/process-runner.lua
Normal file
117
files/libraries/process-runner.lua
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
-- Copyright (c) 2022 David Vogel
|
||||||
|
--
|
||||||
|
-- This software is released under the MIT License.
|
||||||
|
-- https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
-- A simple library to run/control processes. Specifically made for the Noita map capture addon.
|
||||||
|
-- This allows only one process to be run at a time in a given context.
|
||||||
|
|
||||||
|
-- No idea if this library has much use outside of this mod.
|
||||||
|
|
||||||
|
if not async then
|
||||||
|
require("coroutines") -- Loads Noita's coroutines library from `data/scripts/lib/coroutines.lua`.
|
||||||
|
end
|
||||||
|
|
||||||
|
-------------
|
||||||
|
-- Classes --
|
||||||
|
-------------
|
||||||
|
|
||||||
|
local ProcessRunner = {}
|
||||||
|
|
||||||
|
---@class ProcessRunnerCtx
|
||||||
|
---@field running boolean|nil
|
||||||
|
---@field stopping boolean|nil
|
||||||
|
---@field progressCurrent number|nil
|
||||||
|
---@field progressEnd number|nil
|
||||||
|
local Context = {}
|
||||||
|
Context.__index = Context
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- Constructor --
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
---Returns a new process runner context.
|
||||||
|
---@return ProcessRunnerCtx
|
||||||
|
function ProcessRunner.New()
|
||||||
|
return setmetatable({}, Context)
|
||||||
|
end
|
||||||
|
|
||||||
|
-------------
|
||||||
|
-- Methods --
|
||||||
|
-------------
|
||||||
|
|
||||||
|
---Returns whether some process is running.
|
||||||
|
---@return boolean
|
||||||
|
function Context:IsRunning()
|
||||||
|
return self.running or false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns whether the process needs to stop as soon as possible.
|
||||||
|
---@return boolean
|
||||||
|
function Context:IsStopping()
|
||||||
|
return self.stopping or false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns the progress of the process.
|
||||||
|
---@return number current
|
||||||
|
---@return number end
|
||||||
|
function Context:GetProgress()
|
||||||
|
return self.progressCurrent or 0, self.progressEnd or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Tells the currently running process to stop.
|
||||||
|
function Context:Stop()
|
||||||
|
self.stopping = true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Starts a process with the three given callback functions.
|
||||||
|
---This will just call the tree callbacks in order.
|
||||||
|
---Everything is called from inside a coroutine, so you can use yield.
|
||||||
|
---
|
||||||
|
---There can only be ever one process at a time.
|
||||||
|
---If there is already a process running, this will just do nothing.
|
||||||
|
---@param initFunc fun(ctx:ProcessRunnerCtx)|nil -- Called first.
|
||||||
|
---@param doFunc fun(ctx:ProcessRunnerCtx)|nil -- Called after `initFunc` has been run.
|
||||||
|
---@param endFunc fun(ctx:ProcessRunnerCtx)|nil -- Called after `doFunc` has been run.
|
||||||
|
---@param errFunc fun(err:string, scope:"init"|"do"|"end") -- Called on any error.
|
||||||
|
function Context:Run(initFunc, doFunc, endFunc, errFunc)
|
||||||
|
if self.running then return end
|
||||||
|
|
||||||
|
async(function()
|
||||||
|
self.running, self.stopping, self.progressCurrent, self.progressEnd = true, false, nil, nil
|
||||||
|
|
||||||
|
-- Init function.
|
||||||
|
if initFunc then
|
||||||
|
local ok, err = pcall(initFunc, self)
|
||||||
|
if not ok then
|
||||||
|
-- Error happened, abort.
|
||||||
|
if endFunc then pcall(endFunc, self) end
|
||||||
|
errFunc(err, "init")
|
||||||
|
self.running, self.stopping = false, false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Do function.
|
||||||
|
if doFunc then
|
||||||
|
local ok, err = pcall(doFunc, self)
|
||||||
|
if not ok then
|
||||||
|
-- Error happened, abort.
|
||||||
|
errFunc(err, "do")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- End function.
|
||||||
|
if endFunc then
|
||||||
|
local ok, err = pcall(endFunc, self)
|
||||||
|
if not ok then
|
||||||
|
-- Error happened, abort.
|
||||||
|
errFunc(err, "end")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.running, self.stopping = false, false
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ProcessRunner
|
@ -31,14 +31,14 @@ ffi.cdef([[
|
|||||||
---Takes a screenshot of the client area of this process' active window.
|
---Takes a screenshot of the client area of this process' active window.
|
||||||
---@param topLeft Vec2 -- Screenshot rectangle's top left coordinate relative to the window's client area in screen pixels.
|
---@param topLeft Vec2 -- Screenshot rectangle's top left coordinate relative to the window's client area in screen pixels.
|
||||||
---@param bottomRight Vec2 -- Screenshot rectangle's bottom right coordinate relative to the window's client area in screen pixels. The pixel is not included in the screenshot area.
|
---@param bottomRight Vec2 -- Screenshot rectangle's bottom right coordinate relative to the window's client area in screen pixels. The pixel is not included in the screenshot area.
|
||||||
---@param topLeftWorld Vec2 -- The corresponding scaled world coordinates of the screenshot rectangles' top left corner.
|
---@param topLeftOutput Vec2 -- The corresponding scaled world coordinates of the screenshot rectangles' top left corner.
|
||||||
---@param finalDimensions Vec2|nil -- The final dimensions that the screenshot will be resized to. If set to zero, no resize will happen.
|
---@param finalDimensions Vec2|nil -- The final dimensions that the screenshot will be resized to. If set to zero, no resize will happen.
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function ScreenCap.Capture(topLeft, bottomRight, topLeftWorld, finalDimensions)
|
function ScreenCap.Capture(topLeft, bottomRight, topLeftOutput, finalDimensions)
|
||||||
finalDimensions = finalDimensions or Vec2(0, 0)
|
finalDimensions = finalDimensions or Vec2(0, 0)
|
||||||
|
|
||||||
local rect = ffi.new("RECT", { math.floor(topLeft.x + 0.5), math.floor(topLeft.y + 0.5), math.floor(bottomRight.x + 0.5), math.floor(bottomRight.y + 0.5) })
|
local rect = ffi.new("RECT", { math.floor(topLeft.x + 0.5), math.floor(topLeft.y + 0.5), math.floor(bottomRight.x + 0.5), math.floor(bottomRight.y + 0.5) })
|
||||||
return res.Capture(rect, math.floor(topLeftWorld.x + 0.5), math.floor(topLeftWorld.y + 0.5), math.floor(finalDimensions.x + 0.5), math.floor(finalDimensions.y + 0.5))
|
return res.Capture(rect, math.floor(topLeftOutput.x + 0.5), math.floor(topLeftOutput.y + 0.5), math.floor(finalDimensions.x + 0.5), math.floor(finalDimensions.y + 0.5))
|
||||||
end
|
end
|
||||||
|
|
||||||
---Returns the client rectangle of the "Main" window of this process in screen coordinates.
|
---Returns the client rectangle of the "Main" window of this process in screen coordinates.
|
||||||
|
72
files/ui.lua
72
files/ui.lua
@ -14,20 +14,28 @@ local ScreenCap = require("screen-capture")
|
|||||||
-- Code --
|
-- Code --
|
||||||
----------
|
----------
|
||||||
|
|
||||||
UiCaptureDelay = 0 -- Waiting time in frames
|
function UI:SuspendDrawing(frames)
|
||||||
UiProgress = nil
|
self.suspendFrames = math.max(self.suspendFrames or 0, frames)
|
||||||
UiCaptureProblem = nil
|
|
||||||
|
|
||||||
local function progressBarString(progress, look)
|
|
||||||
local factor = progress.Progress / progress.Max
|
|
||||||
local count = math.ceil(look.BarLength * factor)
|
|
||||||
local barString = string.rep(look.CharFull, count) .. string.rep(look.CharEmpty, look.BarLength - count)
|
|
||||||
|
|
||||||
return string.format(look.Format, barString, progress.Progress, progress.Max, factor * 100)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function DrawUI()
|
function UI:Draw()
|
||||||
if modGUI ~= nil then
|
self.gui = self.gui or GuiCreate()
|
||||||
|
local gui = self.gui
|
||||||
|
|
||||||
|
-- Skip drawing if we are asked to do so.
|
||||||
|
if self.suspendFrames and self.suspendFrames > 0 then self.suspendFrames = self.suspendFrames - 1 return end
|
||||||
|
self.suspendFrames = nil
|
||||||
|
|
||||||
|
GuiStartFrame(gui)
|
||||||
|
|
||||||
|
GuiLayoutBeginVertical(gui, 50, 20, false, 0, 0)
|
||||||
|
|
||||||
|
GuiTextCentered(gui, 0, 0, "Heyho")
|
||||||
|
|
||||||
|
GuiLayoutEnd(gui)
|
||||||
|
|
||||||
|
if true then return end
|
||||||
|
|
||||||
GuiStartFrame(modGUI)
|
GuiStartFrame(modGUI)
|
||||||
|
|
||||||
GuiLayoutBeginVertical(modGUI, 50, 20)
|
GuiLayoutBeginVertical(modGUI, 50, 20)
|
||||||
@ -53,26 +61,16 @@ function DrawUI()
|
|||||||
if math.abs(ratioX - CAPTURE_PIXEL_SIZE) > 0.0001 or math.abs(ratioY - CAPTURE_PIXEL_SIZE) > 0.0001 then
|
if math.abs(ratioX - CAPTURE_PIXEL_SIZE) > 0.0001 or math.abs(ratioY - CAPTURE_PIXEL_SIZE) > 0.0001 then
|
||||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Screen and virtual resolution differ.")
|
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Screen and virtual resolution differ.")
|
||||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
||||||
GuiTextCentered(
|
GuiTextCentered(modGUI, 0, 0, string.format(
|
||||||
modGUI,
|
"- Change the resolution in the game options to %dx%d",
|
||||||
0,
|
virtualWidth * CAPTURE_PIXEL_SIZE,
|
||||||
0,
|
virtualHeight * CAPTURE_PIXEL_SIZE
|
||||||
string.format(
|
))
|
||||||
"- Change the resolution in the game options to %dx%d",
|
GuiTextCentered(modGUI, 0, 0, string.format(
|
||||||
virtualWidth * CAPTURE_PIXEL_SIZE,
|
"- Change the virtual resolution in the mod to %dx%d",
|
||||||
virtualHeight * CAPTURE_PIXEL_SIZE
|
screenWidth / CAPTURE_PIXEL_SIZE,
|
||||||
)
|
screenHeight / CAPTURE_PIXEL_SIZE
|
||||||
)
|
))
|
||||||
GuiTextCentered(
|
|
||||||
modGUI,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
string.format(
|
|
||||||
"- Change the virtual resolution in the mod to %dx%d",
|
|
||||||
screenWidth / CAPTURE_PIXEL_SIZE,
|
|
||||||
screenHeight / CAPTURE_PIXEL_SIZE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if math.abs(ratioX - ratioY) < 0.0001 then
|
if math.abs(ratioX - ratioY) < 0.0001 then
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format("- Change the CAPTURE_PIXEL_SIZE in the mod to %f", ratioX))
|
GuiTextCentered(modGUI, 0, 0, string.format("- Change the CAPTURE_PIXEL_SIZE in the mod to %f", ratioX))
|
||||||
end
|
end
|
||||||
@ -155,14 +153,4 @@ function DrawUI()
|
|||||||
GuiTextCentered(modGUI, 0, 0, "Done!")
|
GuiTextCentered(modGUI, 0, 0, "Done!")
|
||||||
end
|
end
|
||||||
GuiLayoutEnd(modGUI)
|
GuiLayoutEnd(modGUI)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
async_loop(
|
|
||||||
function()
|
|
||||||
-- When capturing is active, DrawUI is called from a different coroutine
|
|
||||||
-- This ensures that the text is drawn *after* a screenshot has been grabbed
|
|
||||||
if not UiProgress or UiProgress.Done then DrawUI() end
|
|
||||||
wait(0)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
37
init.lua
37
init.lua
@ -25,13 +25,21 @@ local CameraAPI = require("noita-api.camera")
|
|||||||
local DebugAPI = require("noita-api.debug")
|
local DebugAPI = require("noita-api.debug")
|
||||||
local Vec2 = require("noita-api.vec2")
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Global namespaces --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Capture = Capture or {}
|
||||||
|
Config = Config or {}
|
||||||
|
UI = UI or {}
|
||||||
|
|
||||||
-------------------------------
|
-------------------------------
|
||||||
-- Load and run script files --
|
-- Load and run script files --
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
dofile("mods/noita-mapcap/files/capture.lua")
|
dofile("mods/noita-mapcap/files/capture.lua")
|
||||||
|
dofile("mods/noita-mapcap/files/config.lua")
|
||||||
dofile("mods/noita-mapcap/files/ui.lua")
|
dofile("mods/noita-mapcap/files/ui.lua")
|
||||||
--dofile("mods/noita-mapcap/files/blablabla.lua")
|
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
-- Hook callbacks --
|
-- Hook callbacks --
|
||||||
@ -53,25 +61,6 @@ end
|
|||||||
---Ensures chunks around the player have been loaded & created.
|
---Ensures chunks around the player have been loaded & created.
|
||||||
---@param playerEntityID integer
|
---@param playerEntityID integer
|
||||||
function OnPlayerSpawned(playerEntityID)
|
function OnPlayerSpawned(playerEntityID)
|
||||||
modGUI = GuiCreate()
|
|
||||||
|
|
||||||
-- Start entity capturing right when the player spawn.
|
|
||||||
--DebugEntityCapture()
|
|
||||||
|
|
||||||
--[[async(function()
|
|
||||||
wait(0)
|
|
||||||
CameraAPI.SetCameraFree(true)
|
|
||||||
|
|
||||||
local origin = Vec2(512, -512)
|
|
||||||
CameraAPI.SetPos(origin)
|
|
||||||
|
|
||||||
DebugAPI.Mark(origin, "origin")
|
|
||||||
|
|
||||||
local tl, br = Coords:ValidRenderingRect()
|
|
||||||
local tlWorld, brWorld = Coords:ToWorld(tl), Coords:ToWorld(br)
|
|
||||||
DebugAPI.Mark(tlWorld, "tl")
|
|
||||||
DebugAPI.Mark(brWorld, "br")
|
|
||||||
end)]]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---Called when the player dies.
|
---Called when the player dies.
|
||||||
@ -92,6 +81,8 @@ end
|
|||||||
|
|
||||||
---Called *every* time the game has finished updating the world.
|
---Called *every* time the game has finished updating the world.
|
||||||
function OnWorldPostUpdate()
|
function OnWorldPostUpdate()
|
||||||
|
-- Draw UI after coroutines have been resumed.
|
||||||
|
UI:Draw()
|
||||||
end
|
end
|
||||||
|
|
||||||
---Called when the biome config is loaded.
|
---Called when the biome config is loaded.
|
||||||
@ -126,10 +117,10 @@ end
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
-- Override virtual resolution and some other stuff.
|
-- Override virtual resolution and some other stuff.
|
||||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/1024.xml")
|
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/64.xml")
|
||||||
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/fast-cam.xml")
|
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/fast-cam.xml")
|
||||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/no-ui.xml")
|
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/no-ui.xml")
|
||||||
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/offset.xml")
|
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/offset.xml")
|
||||||
|
|
||||||
-- Remove hover animation of newly created perks.
|
-- Remove hover animation of newly created perks.
|
||||||
ModLuaFileAppend("data/scripts/perks/perk.lua", "mods/noita-mapcap/files/overrides/perks/perk.lua")
|
ModLuaFileAppend("data/scripts/perks/perk.lua", "mods/noita-mapcap/files/overrides/perks/perk.lua")
|
||||||
|
Loading…
Reference in New Issue
Block a user