Update capturing stuff

- Add ability to capture while normally playing
- Calculate capture area based on coordinate transformations
- Improve and simplify captureScreenshot function
- Move dynamic library wrappers into libraries folder
- Update capture.dll to support cropping and resizing
- Recompile capture.dll with newer PureBasic compiler that uses C backend
- Increase capture.dll worker threads to 6
- Increase capture.dll queue size by one
- Add Round and Rounded methods to Vec2
- Split magic number XML files for easier debugging
- Fix some EmmyLua annotations
- And and fix some comments
This commit is contained in:
David Vogel 2022-07-24 22:05:34 +02:00
parent f2e582622e
commit 0126e706cb
16 changed files with 331 additions and 140 deletions

View File

@ -1,4 +1,4 @@
; Copyright (c) 2019-2020 David Vogel ; Copyright (c) 2019-2022 David Vogel
; ;
; This software is released under the MIT License. ; This software is released under the MIT License.
; https://opensource.org/licenses/MIT ; https://opensource.org/licenses/MIT
@ -11,6 +11,8 @@ Structure QueueElement
img.i img.i
x.i x.i
y.i y.i
sx.i
sy.i
EndStructure EndStructure
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15 ; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
@ -62,7 +64,7 @@ ProcedureDLL AttachProcess(Instance)
CreateDirectory("mods/noita-mapcap/output/") CreateDirectory("mods/noita-mapcap/output/")
For i = 1 To 4 For i = 1 To 6
CreateThread(@Worker(), #Null) CreateThread(@Worker(), #Null)
Next Next
EndProcedure EndProcedure
@ -78,9 +80,15 @@ Procedure Worker(*Dummy)
img = Queue()\img img = Queue()\img
x = Queue()\x x = Queue()\x
y = Queue()\y y = Queue()\y
sx = Queue()\sx
sy = Queue()\sy
DeleteElement(Queue()) DeleteElement(Queue())
UnlockMutex(Mutex) UnlockMutex(Mutex)
If sx > 0 And sy > 0
ResizeImage(img, sx, sy)
EndIf
SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG) SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG)
;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test ;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test
@ -88,7 +96,11 @@ Procedure Worker(*Dummy)
ForEver ForEver
EndProcedure EndProcedure
ProcedureDLL Capture(px.i, py.i) ; Takes a screenshot of the client area of this process' active window.
; The portion of the client area that is captured is described by capRect, which is in window coordinates and relative to the client area.
; x and y defines the top left position of the captured rectangle in scaled world coordinates. The scale depends on the window to world pixel ratio.
; sx and sy defines the final dimensions that the screenshot will be resized to. No resize will happen if set to 0.
ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
Protected hWnd.l = GetProcHwnd() Protected hWnd.l = GetProcHwnd()
If Not hWnd If Not hWnd
ProcedureReturn #False ProcedureReturn #False
@ -99,12 +111,18 @@ ProcedureDLL Capture(px.i, py.i)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
imageID = CreateImage(#PB_Any, rect\right-rect\left, rect\bottom-rect\top) ; Limit the desired capture area to the actual client area of the window.
If *capRect\left < 0 : *capRect\left = 0 : EndIf
If *capRect\right > rect\right-rect\left : *capRect\right = rect\right-rect\left : EndIf
If *capRect\top < 0 : *capRect\top = 0 : EndIf
If *capRect\bottom > rect\bottom-rect\top : *capRect\bottom = rect\bottom-rect\top : EndIf
imageID = CreateImage(#PB_Any, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top)
If Not imageID If Not imageID
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
; Get DC of whole screen ; Get DC of window.
windowDC = GetDC_(hWnd) windowDC = GetDC_(hWnd)
If Not windowDC If Not windowDC
FreeImage(imageID) FreeImage(imageID)
@ -117,7 +135,7 @@ ProcedureDLL Capture(px.i, py.i)
FreeImage(imageID) FreeImage(imageID)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
If Not BitBlt_(hDC, 0, 0, rect\right-rect\left, rect\bottom-rect\top, windowDC, 0, 0, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes. If Not BitBlt_(hDC, 0, 0, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top, windowDC, *capRect\left, *capRect\top, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes.
StopDrawing() StopDrawing()
ReleaseDC_(hWnd, windowDC) ReleaseDC_(hWnd, windowDC)
FreeImage(imageID) FreeImage(imageID)
@ -128,17 +146,19 @@ ProcedureDLL Capture(px.i, py.i)
ReleaseDC_(hWnd, windowDC) ReleaseDC_(hWnd, windowDC)
LockMutex(Mutex) LockMutex(Mutex)
; Check if the queue has too many elements, if so, wait. (Simulate go's channels) ; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
While ListSize(Queue()) > 0 While ListSize(Queue()) > 1
UnlockMutex(Mutex) UnlockMutex(Mutex)
Delay(10) Delay(1)
LockMutex(Mutex) LockMutex(Mutex)
Wend Wend
LastElement(Queue()) LastElement(Queue())
AddElement(Queue()) AddElement(Queue())
Queue()\img = imageID Queue()\img = imageID
Queue()\x = px Queue()\x = x
Queue()\y = py Queue()\y = y
Queue()\sx = sx
Queue()\sy = sy
UnlockMutex(Mutex) UnlockMutex(Mutex)
SignalSemaphore(Semaphore) SignalSemaphore(Semaphore)
@ -153,12 +173,13 @@ EndProcedure
;Capture(123, 123) ;Capture(123, 123)
;Delay(1000) ;Delay(1000)
; IDE Options = PureBasic 5.72 (Windows - x64) ; IDE Options = PureBasic 6.00 LTS (Windows - x64)
; ExecutableFormat = Shared dll ; ExecutableFormat = Shared dll
; CursorPosition = 90 ; CursorPosition = 94
; FirstLine = 77 ; FirstLine = 39
; Folding = -- ; Folding = --
; Optimizer
; EnableThread ; EnableThread
; EnableXP ; EnableXP
; Executable = capture.dll ; Executable = capture.dll
; Compiler = PureBasic 5.71 LTS (Windows - x86) ; Compiler = PureBasic 6.00 LTS (Windows - x86)

Binary file not shown.

View File

@ -7,10 +7,15 @@
-- Load library modules -- -- Load library modules --
-------------------------- --------------------------
local JSON = require("noita-api.json") local CameraAPI = require("noita-api.camera")
local Coords = require("coordinates")
local EntityAPI = require("noita-api.entity") local EntityAPI = require("noita-api.entity")
local Utils = require("noita-api.utils")
local Hilbert = require("hilbert-curve") local Hilbert = require("hilbert-curve")
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")
---------- ----------
-- Code -- -- Code --
@ -194,47 +199,69 @@ function DebugEntityCapture()
end) end)
end end
--- Captures a screenshot at the given coordinates. ---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
--- This will block until all chunks in the given area are loaded. ---@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
--- @param x number -- Virtual x coordinate (World pixels) of the screen center. ---@return Vec2 bottomRightCapture
--- @param y number -- Virtual y coordinate (World pixels) of the screen center. ---@return Vec2 topLeftWorld
--- @param rx number -- Screen x coordinate of the top left corner of the screenshot rectangle. ---@return Vec2 bottomRightWorld
--- @param ry number -- Screen y coordinate of the top left corner of the screenshot rectangle. local function GenerateCaptureRectangle(pos)
local function captureScreenshot(x, y, rx, ry) local topLeft, bottomRight = Coords:ValidRenderingRect()
local virtualWidth, virtualHeight =
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2) -- Convert valid rendering rectangle into world coordinates, and round it towards the window center.
local xMin, yMin = x - virtualHalfWidth, y - virtualHalfHeight local topLeftWorld, bottomRightWorld = Coords:ToWorld(topLeft, pos):Rounded("ceil"), Coords:ToWorld(bottomRight, pos):Rounded("floor")
local xMax, yMax = xMin + virtualWidth, yMin + virtualHeight
-- 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!
---@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.
local function captureScreenshot(pos, ensureLoaded)
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = GenerateCaptureRectangle(pos)
UiCaptureDelay = 0 UiCaptureDelay = 0
GameSetCameraPos(x, y) if pos then CameraAPI.SetPos(pos) end
repeat if ensureLoaded then
if UiCaptureDelay > 100 then repeat
-- Wiggle the screen a bit, as chunks sometimes don't want to load. if UiCaptureDelay > 100 then
GameSetCameraPos(x + math.random(-100, 100), y + math.random(-100, 100)) -- 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
DrawUI()
wait(0)
UiCaptureDelay = UiCaptureDelay + 1
if pos then CameraAPI.SetPos(pos) end
end
DrawUI() DrawUI()
wait(0) wait(0)
UiCaptureDelay = UiCaptureDelay + 1 UiCaptureDelay = UiCaptureDelay + 1
GameSetCameraPos(x, y)
end
DrawUI() until DoesWorldExistAt(topLeftWorld.x, topLeftWorld.y, bottomRightWorld.x, bottomRightWorld.y)
wait(0) -- Chunks are loaded an will be drawn on the *next* frame.
UiCaptureDelay = UiCaptureDelay + 1 end
until DoesWorldExistAt(xMin, yMin, xMax, yMax) -- Chunks will be drawn on the *next* frame.
wait(0) -- Without this line empty chunks may still appear, also it's needed for the UI to disappear. wait(0) -- Without this line empty chunks may still appear, also it's needed for the UI to disappear.
if not TriggerCapture(rx, ry) then
-- Fetch coordinates again, as they may have changed.
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = GenerateCaptureRectangle(pos)
local outputPixelScale = 4
-- 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." UiCaptureProblem = "Screen capture failed. Please restart Noita."
end end
-- Reset monitor and PC standby each screenshot. -- Reset monitor and PC standby every screenshot.
ResetStandbyTimer() MonitorStandby.ResetTimer()
end end
function startCapturingSpiral() function startCapturingSpiral()
@ -245,9 +272,7 @@ function startCapturingSpiral()
ox, oy = ox + 256, oy + 256 -- Align screen with ingame chunk grid that is 512x512. ox, oy = ox + 256, oy + 256 -- Align screen with ingame chunk grid that is 512x512.
local x, y = ox, oy local x, y = ox, oy
local virtualWidth, virtualHeight = local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2) local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
@ -272,7 +297,7 @@ function startCapturingSpiral()
for i = 1, i, 1 do for i = 1, i, 1 do
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE 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 if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
captureScreenshot(x, y, rx, ry, entityFile) captureScreenshot(Vec2(x, y), true)
end end
x, y = x + CAPTURE_GRID_SIZE, y x, y = x + CAPTURE_GRID_SIZE, y
end end
@ -280,7 +305,7 @@ function startCapturingSpiral()
for i = 1, i, 1 do for i = 1, i, 1 do
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE 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 if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
captureScreenshot(x, y, rx, ry, entityFile) captureScreenshot(Vec2(x, y), true)
end end
x, y = x, y + CAPTURE_GRID_SIZE x, y = x, y + CAPTURE_GRID_SIZE
end end
@ -289,7 +314,7 @@ function startCapturingSpiral()
for i = 1, i, 1 do for i = 1, i, 1 do
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE 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 if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
captureScreenshot(x, y, rx, ry, entityFile) captureScreenshot(Vec2(x, y), true)
end end
x, y = x - CAPTURE_GRID_SIZE, y x, y = x - CAPTURE_GRID_SIZE, y
end end
@ -297,7 +322,7 @@ function startCapturingSpiral()
for i = 1, i, 1 do for i = 1, i, 1 do
local rx, ry = (x - virtualHalfWidth) * CAPTURE_PIXEL_SIZE, (y - virtualHalfHeight) * CAPTURE_PIXEL_SIZE 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 if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
captureScreenshot(x, y, rx, ry, entityFile) captureScreenshot(Vec2(x, y), true)
end end
x, y = x, y - CAPTURE_GRID_SIZE x, y = x, y - CAPTURE_GRID_SIZE
end end
@ -311,9 +336,7 @@ function startCapturingHilbert(area)
local ox, oy = GameGetCameraPos() local ox, oy = GameGetCameraPos()
local virtualWidth, virtualHeight = local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2) local virtualHalfWidth, virtualHalfHeight = math.floor(virtualWidth / 2), math.floor(virtualHeight / 2)
@ -342,7 +365,7 @@ function startCapturingHilbert(area)
local t, tLimit = 0, gridMaxSize * gridMaxSize local t, tLimit = 0, gridMaxSize * gridMaxSize
UiProgress = {Progress = 0, Max = gridWidth * gridHeight} UiProgress = { Progress = 0, Max = gridWidth * gridHeight }
GameSetCameraFree(true) GameSetCameraFree(true)
@ -367,7 +390,7 @@ function startCapturingHilbert(area)
x, y = x + 256, y + 256 -- Align screen with ingame chunk grid that is 512x512. 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 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 if not Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then
captureScreenshot(x, y, rx, ry, entityFile) captureScreenshot(Vec2(x, y), true)
end end
UiProgress.Progress = UiProgress.Progress + 1 UiProgress.Progress = UiProgress.Progress + 1
end end
@ -379,3 +402,46 @@ function startCapturingHilbert(area)
end end
) )
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

View File

@ -1,44 +0,0 @@
-- Copyright (c) 2019-2022 David Vogel
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT
local ffi = ffi or _G.ffi or require("ffi")
local status, caplib = pcall(ffi.load, "mods/noita-mapcap/bin/capture-b/capture")
if not status then
print("Error loading capture lib: " .. caplib)
end
ffi.cdef [[
typedef long LONG;
typedef struct {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
bool GetRect(RECT* rect);
bool Capture(int x, int y);
int SetThreadExecutionState(int esFlags);
]]
function TriggerCapture(x, y)
return caplib.Capture(x, y)
end
-- Get the client rectangle of the "Main" window of this process in screen coordinates
function GetRect()
local rect = ffi.new("RECT", 0, 0, 0, 0)
if not caplib.GetRect(rect) then
return nil
end
return rect
end
-- Reset computer and monitor standby timer
function ResetStandbyTimer()
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
end

View File

@ -5,6 +5,9 @@
-- Viewport coordinates transformation (world <-> window) for Noita. -- Viewport coordinates transformation (world <-> window) for Noita.
-- For it to work, you have to:
-- - Put Coords:ReadResolutions() inside of the OnMagicNumbersAndWorldSeedInitialized() hook.
-------------------------- --------------------------
-- Load library modules -- -- Load library modules --
-------------------------- --------------------------
@ -19,9 +22,9 @@ local Vec2 = require("noita-api.vec2")
---------- ----------
---@class Coords ---@class Coords
---@field InternalResolution Vec2 ---@field InternalResolution Vec2 -- Size of the internal rectangle in window pixels.
---@field WindowResolution Vec2 ---@field WindowResolution Vec2 -- Size of the window client area in window pixels.
---@field VirtualResolution Vec2 ---@field VirtualResolution Vec2 -- Size of the virtual rectangle in world/virtual pixels.
local Coords = { local Coords = {
InternalResolution = Vec2(0, 0), InternalResolution = Vec2(0, 0),
WindowResolution = Vec2(0, 0), WindowResolution = Vec2(0, 0),

View File

@ -0,0 +1,19 @@
-- Copyright (c) 2019-2022 David Vogel
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT
local ffi = require("ffi")
local MonitorStandby = {}
ffi.cdef([[
int SetThreadExecutionState(int esFlags);
]])
-- Reset computer and monitor standby timer
function MonitorStandby.ResetTimer()
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
end
return MonitorStandby

View File

@ -237,12 +237,14 @@ end
---Returns the squared distance of self to the given vector. ---Returns the squared distance of self to the given vector.
---@param v Vec2 ---@param v Vec2
---@return number
function Vec2:DistanceSqr(v) function Vec2:DistanceSqr(v)
return (v - self):LengthSqr() return (v - self):LengthSqr()
end end
---Returns the distance of self to the given vector. ---Returns the distance of self to the given vector.
---@param v Vec2 ---@param v Vec2
---@return number
function Vec2:Distance(v) function Vec2:Distance(v)
return (v - self):Length() return (v - self):Length()
end end
@ -272,6 +274,49 @@ function Vec2:EqualTo(v, tolerance)
return true return true
end end
---Round returns v rounded by the given method.
---@param x number
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
---@return integer
local function round(x, method)
method = method or "nearest"
if method == "nearest" then
return math.floor(x + 0.5)
elseif method == "floor" then
return math.floor(x)
elseif method == "ceil" then
return math.ceil(x)
elseif method == "to-zero" then
if x >= 0 then
return math.floor(x)
else
return math.ceil(x)
end
elseif method == "away-zero" then
if x >= 0 then
return math.ceil(x)
else
return math.floor(x)
end
end
error(string.format("invalid rounding method %q", method))
end
---Round rounds all vector fields individually by the given rounding method.
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
function Vec2:Round(method)
self[1], self[2] = round(self[1], method), round(self[2], method)
end
---Round rounds all vector fields individually by the given rounding method.
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
---@return Vec2
function Vec2:Rounded(method)
return Vec2(round(self[1], method), round(self[2], method))
end
------------------------- -------------------------
-- JSON Implementation -- -- JSON Implementation --
------------------------- -------------------------

View File

@ -0,0 +1,55 @@
-- Copyright (c) 2019-2022 David Vogel
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT
local Vec2 = require("noita-api.vec2")
local ffi = require("ffi")
local ScreenCap = {}
local status, res = pcall(ffi.load, "mods/noita-mapcap/bin/capture-b/capture")
if not status then
print(string.format("Error loading capture lib: %s", res))
return
end
ffi.cdef([[
typedef long LONG;
typedef struct {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
bool GetRect(RECT* rect);
bool Capture(RECT* rect, int x, int y, int sx, int sy);
]])
---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 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 finalDimensions Vec2|nil -- The final dimensions that the screenshot will be resized to. If set to zero, no resize will happen.
---@return boolean
function ScreenCap.Capture(topLeft, bottomRight, topLeftWorld, finalDimensions)
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) })
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))
end
---Returns the client rectangle of the "Main" window of this process in screen coordinates.
---@return any
function ScreenCap.GetRect()
local rect = ffi.new("RECT")
if not res.GetRect(rect) then
return nil
end
return rect
end
return ScreenCap

View File

@ -0,0 +1,4 @@
<MagicNumbers
VIRTUAL_RESOLUTION_X="1024"
VIRTUAL_RESOLUTION_Y="1024"
></MagicNumbers>

View File

@ -0,0 +1,4 @@
<MagicNumbers
VIRTUAL_RESOLUTION_X="512"
VIRTUAL_RESOLUTION_Y="512"
></MagicNumbers>

View File

@ -0,0 +1,4 @@
<MagicNumbers
VIRTUAL_RESOLUTION_X="64"
VIRTUAL_RESOLUTION_Y="64"
></MagicNumbers>

View File

@ -0,0 +1,3 @@
<MagicNumbers
DEBUG_FREE_CAMERA_SPEED="1"
></MagicNumbers>

View File

@ -1,9 +1,5 @@
<MagicNumbers VIRTUAL_RESOLUTION_X="1280" <MagicNumbers
VIRTUAL_RESOLUTION_Y="720"
VIRTUAL_RESOLUTION_OFFSET_X="-2"
VIRTUAL_RESOLUTION_OFFSET_Y="0"
DRAW_PARALLAX_BACKGROUND="0" DRAW_PARALLAX_BACKGROUND="0"
DEBUG_FREE_CAMERA_SPEED="10"
DEBUG_NO_LOGO_SPLASHES="1" DEBUG_NO_LOGO_SPLASHES="1"
DEBUG_PAUSE_GRID_UPDATE="1" DEBUG_PAUSE_GRID_UPDATE="1"
DEBUG_PAUSE_BOX2D="1" DEBUG_PAUSE_BOX2D="1"
@ -16,5 +12,5 @@
UI_QUICKBAR_OFFSET_X="2000" UI_QUICKBAR_OFFSET_X="2000"
UI_QUICKBAR_OFFSET_Y="2000" UI_QUICKBAR_OFFSET_Y="2000"
UI_BARS_POS_X="2000" UI_BARS_POS_X="2000"
UI_BARS_POS_Y="2000"> UI_BARS_POS_Y="2000"
</MagicNumbers> ></MagicNumbers>

View File

@ -0,0 +1,4 @@
<MagicNumbers
VIRTUAL_RESOLUTION_OFFSET_X="-2"
VIRTUAL_RESOLUTION_OFFSET_Y="0"
></MagicNumbers>

View File

@ -8,6 +8,7 @@
-------------------------- --------------------------
local Utils = require("noita-api.utils") local Utils = require("noita-api.utils")
local ScreenCap = require("screen-capture")
---------- ----------
-- Code -- -- Code --
@ -33,7 +34,7 @@ function DrawUI()
if not UiProgress then if not UiProgress then
-- Show informations -- Show informations
local problem local problem
local rect = GetRect() local rect = ScreenCap.GetRect()
if not rect then if not rect then
GuiTextCentered(modGUI, 0, 0, '!!! WARNING !!! You are not using "Windowed" mode.') GuiTextCentered(modGUI, 0, 0, '!!! WARNING !!! You are not using "Windowed" mode.')
@ -45,9 +46,7 @@ function DrawUI()
if rect then if rect then
local screenWidth, screenHeight = rect.right - rect.left, rect.bottom - rect.top local screenWidth, screenHeight = rect.right - rect.left, rect.bottom - rect.top
local virtualWidth, virtualHeight = local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
local ratioX, ratioY = screenWidth / virtualWidth, screenHeight / virtualHeight local ratioX, ratioY = screenWidth / virtualWidth, screenHeight / virtualHeight
--GuiTextCentered(modGUI, 0, 0, string.format("SCREEN_RESOLUTION_*: %d, %d", screenWidth, screenHeight)) --GuiTextCentered(modGUI, 0, 0, string.format("SCREEN_RESOLUTION_*: %d, %d", screenWidth, screenHeight))
--GuiTextCentered(modGUI, 0, 0, string.format("VIRTUAL_RESOLUTION_*: %d, %d", virtualWidth, virtualHeight)) --GuiTextCentered(modGUI, 0, 0, string.format("VIRTUAL_RESOLUTION_*: %d, %d", virtualWidth, virtualHeight))
@ -108,19 +107,14 @@ function DrawUI()
GuiTextCentered(modGUI, 0, 0, "You can freely look around and search a place to start capturing.") GuiTextCentered(modGUI, 0, 0, "You can freely look around and search a place to start capturing.")
GuiTextCentered(modGUI, 0, 0, "When started the mod will take pictures automatically.") GuiTextCentered(modGUI, 0, 0, "When started the mod will take pictures automatically.")
GuiTextCentered(modGUI, 0, 0, "Use ESC to pause, and close the game to stop the process.") GuiTextCentered(modGUI, 0, 0, "Use ESC to pause, and close the game to stop the process.")
GuiTextCentered( GuiTextCentered(modGUI, 0, 0, 'You can resume capturing just by restarting noita and pressing "Start capturing map" again,')
modGUI,
0,
0,
'You can resume capturing just by restarting noita and pressing "Start capturing map" again,'
)
GuiTextCentered(modGUI, 0, 0, "the mod will skip already captured files.") GuiTextCentered(modGUI, 0, 0, "the mod will skip already captured files.")
GuiTextCentered( GuiTextCentered(modGUI, 0, 0, 'If you want to start a new map, you have to delete all images from the "output" folder!')
modGUI, --GuiTextCentered(modGUI, 0, 0, " ")
0, --GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_X"))
0, --GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
'If you want to start a new map, you have to delete all images from the "output" folder!' --GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_X"))
) --GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_Y"))
GuiTextCentered(modGUI, 0, 0, " ") GuiTextCentered(modGUI, 0, 0, " ")
if GuiButton(modGUI, 0, 0, ">> Start capturing map around view <<", 1) then if GuiButton(modGUI, 0, 0, ">> Start capturing map around view <<", 1) then
UiProgress = {} UiProgress = {}
@ -139,6 +133,10 @@ function DrawUI()
UiProgress = {} UiProgress = {}
startCapturingHilbert(CAPTURE_AREA_EXTENDED) startCapturingHilbert(CAPTURE_AREA_EXTENDED)
end end
if GuiButton(modGUI, 0, 0, ">> Start capturing run live <<", 1) then
UiProgress = {}
StartCapturingLive()
end
GuiTextCentered(modGUI, 0, 0, " ") GuiTextCentered(modGUI, 0, 0, " ")
elseif not UiProgress.Done then elseif not UiProgress.Done then
-- Show progress -- Show progress
@ -146,15 +144,9 @@ function DrawUI()
GuiTextCentered(modGUI, 0, 0, string.format("Coordinates: %d, %d", x, y)) GuiTextCentered(modGUI, 0, 0, string.format("Coordinates: %d, %d", x, y))
GuiTextCentered(modGUI, 0, 0, string.format("Waiting %d frames...", UiCaptureDelay)) GuiTextCentered(modGUI, 0, 0, string.format("Waiting %d frames...", UiCaptureDelay))
if UiProgress.Progress then if UiProgress.Progress then
GuiTextCentered( GuiTextCentered(modGUI, 0, 0, progressBarString(
modGUI, UiProgress, { BarLength = 100, CharFull = "l", CharEmpty = ".", Format = "|%s| [%d / %d] [%1.2f%%]" }
0, ))
0,
progressBarString(
UiProgress,
{BarLength = 100, CharFull = "l", CharEmpty = ".", Format = "|%s| [%d / %d] [%1.2f%%]"}
)
)
end end
if UiCaptureProblem then if UiCaptureProblem then
GuiTextCentered(modGUI, 0, 0, string.format("A problem occurred while capturing: %s", UiCaptureProblem)) GuiTextCentered(modGUI, 0, 0, string.format("A problem occurred while capturing: %s", UiCaptureProblem))

View File

@ -21,12 +21,14 @@ end
-------------------------- --------------------------
local Coords = require("coordinates") local Coords = require("coordinates")
local CameraAPI = require("noita-api.camera")
local DebugAPI = require("noita-api.debug")
local Vec2 = require("noita-api.vec2")
------------------------------- -------------------------------
-- Load and run script files -- -- Load and run script files --
------------------------------- -------------------------------
dofile("mods/noita-mapcap/files/external.lua")
dofile("mods/noita-mapcap/files/capture.lua") dofile("mods/noita-mapcap/files/capture.lua")
dofile("mods/noita-mapcap/files/ui.lua") dofile("mods/noita-mapcap/files/ui.lua")
--dofile("mods/noita-mapcap/files/blablabla.lua") --dofile("mods/noita-mapcap/files/blablabla.lua")
@ -52,10 +54,24 @@ end
---@param playerEntityID integer ---@param playerEntityID integer
function OnPlayerSpawned(playerEntityID) function OnPlayerSpawned(playerEntityID)
modGUI = GuiCreate() modGUI = GuiCreate()
GameSetCameraFree(true)
-- Start entity capturing right when the player spawn. -- Start entity capturing right when the player spawn.
--DebugEntityCapture() --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.
@ -110,7 +126,10 @@ end
--------------- ---------------
-- Override virtual resolution and some other stuff. -- Override virtual resolution and some other stuff.
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic_numbers.xml") --ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/1024.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/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")