Improve user experience
- Modernise UI, and simplify its logic - Add UI graphics - Add modification.lua which contains everything to modify Noita settings - Add message.lua which handles messages for users - Add check.lua which checks things, triggers messages and suggest user actions - Remove ACTIONS category from settings - Add more live capturing parameters to settings - Restrict vector input fields in settings - Rename pixel-size setting to pixel-scale - Let GetRect return two vectors instead of RECT object - Add VirtualOffsetPixelPerfect and FullscreenMode field to Coords - Fix captureScreenshot when the outputPixelScale is 0 - Show runtime errors in UI via message.lua - Other small fixes
3
.gitignore
vendored
@ -105,4 +105,5 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
/output/
|
/output/
|
||||||
/dist/
|
/dist/
|
||||||
/bin/stitch/output.png
|
/bin/stitch/output.png
|
||||||
|
/files/magic-numbers/generated.xml
|
@ -64,7 +64,12 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
|||||||
|
|
||||||
---Top left in output coordinates.
|
---Top left in output coordinates.
|
||||||
---@type Vec2
|
---@type Vec2
|
||||||
local outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
local outputTopLeft
|
||||||
|
if outputPixelScale > 0 then
|
||||||
|
outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
||||||
|
else
|
||||||
|
outputTopLeft = topLeftWorld
|
||||||
|
end
|
||||||
|
|
||||||
-- Check if the file exists, and if we are allowed to overwrite it.
|
-- 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
|
if dontOverwrite and Utils.FileExists(string.format("mods/noita-mapcap/output/%d,%d.png", outputTopLeft.x, outputTopLeft.y)) then
|
||||||
@ -77,7 +82,7 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
|||||||
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
|
||||||
|
|
||||||
if delayFrames > 100 then
|
if delayFrames > 100 then
|
||||||
-- Wiggle the screen a bit, as chunks sometimes don't want to load.
|
-- 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
|
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-100, 100), math.random(-100, 100))) end
|
||||||
@ -94,14 +99,18 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Suspend UI drawing for 1 frame.
|
-- Suspend UI drawing for 1 frame.
|
||||||
UI.SuspendDrawing(1)
|
UI:SuspendDrawing(1)
|
||||||
|
|
||||||
wait(0)
|
wait(0)
|
||||||
|
|
||||||
-- Fetch coordinates again, as they may have changed.
|
-- Fetch coordinates again, as they may have changed.
|
||||||
if not pos then
|
if not pos then
|
||||||
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
|
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
|
||||||
outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
if outputPixelScale > 0 then
|
||||||
|
outputTopLeft = (topLeftWorld * outputPixelScale):Rounded()
|
||||||
|
else
|
||||||
|
outputTopLeft = topLeftWorld
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The top left world position needs to be upscaled by the pixel scale.
|
-- The top left world position needs to be upscaled by the pixel scale.
|
||||||
@ -119,7 +128,7 @@ end
|
|||||||
---@param scope "init"|"do"|"end"
|
---@param scope "init"|"do"|"end"
|
||||||
local function mapCapturingCtxErrHandler(err, scope)
|
local function mapCapturingCtxErrHandler(err, scope)
|
||||||
print(string.format("Failed to capture map: %s", err))
|
print(string.format("Failed to capture map: %s", err))
|
||||||
-- TODO: Forward error to user interface
|
Message:ShowRuntimeError("MapCaptureError", "Failed to capture map:", tostring(err))
|
||||||
end
|
end
|
||||||
|
|
||||||
---Starts the capturing process in a spiral around origin.
|
---Starts the capturing process in a spiral around origin.
|
||||||
@ -128,6 +137,11 @@ end
|
|||||||
---@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|nil -- The resulting image pixel to world pixel ratio.
|
||||||
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
|
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
|
||||||
|
|
||||||
|
-- Create file that signals that there are files in the output directory.
|
||||||
|
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
|
||||||
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
---Origin rounded to capture grid.
|
---Origin rounded to capture grid.
|
||||||
---@type Vec2
|
---@type Vec2
|
||||||
local origin = (origin / captureGridSize):Rounded("Floor") * captureGridSize
|
local origin = (origin / captureGridSize):Rounded("Floor") * captureGridSize
|
||||||
@ -180,6 +194,11 @@ end
|
|||||||
---@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|nil -- The resulting image pixel to world pixel ratio.
|
||||||
function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outputPixelScale)
|
function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outputPixelScale)
|
||||||
|
|
||||||
|
-- Create file that signals that there are files in the output directory.
|
||||||
|
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
|
||||||
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
---The rectangle in grid coordinates.
|
---The rectangle in grid coordinates.
|
||||||
---@type Vec2, Vec2
|
---@type Vec2, Vec2
|
||||||
local gridTopLeft, gridBottomRight = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
|
local gridTopLeft, gridBottomRight = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
|
||||||
@ -241,6 +260,10 @@ function Capture:StartCapturingLive(interval, minDistance, maxDistance, outputPi
|
|||||||
minDistance = minDistance or 10
|
minDistance = minDistance or 10
|
||||||
maxDistance = maxDistance or 50
|
maxDistance = maxDistance or 50
|
||||||
|
|
||||||
|
-- Create file that signals that there are files in the output directory.
|
||||||
|
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
|
||||||
|
if file ~= nil then file:close() end
|
||||||
|
|
||||||
---Process main callback.
|
---Process main callback.
|
||||||
---@param ctx ProcessRunnerCtx
|
---@param ctx ProcessRunnerCtx
|
||||||
local function handleDo(ctx)
|
local function handleDo(ctx)
|
||||||
@ -429,8 +452,31 @@ function Capture:StartCapturingEntities(store, modify)
|
|||||||
---@param scope "init"|"do"|"end"
|
---@param scope "init"|"do"|"end"
|
||||||
local function handleErr(err, scope)
|
local function handleErr(err, scope)
|
||||||
print(string.format("Failed to capture entities: %s", err))
|
print(string.format("Failed to capture entities: %s", err))
|
||||||
|
Message:ShowRuntimeError("EntitiesCaptureError", "Failed to capture entities:", tostring(err))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Run process, if there is no other running right now.
|
-- Run process, if there is no other running right now.
|
||||||
self.EntityCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
self.EntityCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Starts the capturing process based on user/mod settings.
|
||||||
|
function Capture:StartCapturing()
|
||||||
|
local mode = ModSettingGet("noita-mapcap.capture-mode")
|
||||||
|
local outputPixelScale = ModSettingGet("noita-mapcap.pixel-scale")
|
||||||
|
|
||||||
|
if mode == "live" then
|
||||||
|
local interval = ModSettingGet("noita-mapcap.live-interval")
|
||||||
|
local minDistance = ModSettingGet("noita-mapcap.live-min-distance")
|
||||||
|
local maxDistance = ModSettingGet("noita-mapcap.live-max-distance")
|
||||||
|
|
||||||
|
self:StartCapturingLive(interval, minDistance, maxDistance, outputPixelScale)
|
||||||
|
else
|
||||||
|
Message:ShowRuntimeError("StartCapturing", string.format("Unknown capturing mode %q", tostring(mode)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Stops all capturing processes.
|
||||||
|
function Capture:StopCapturing()
|
||||||
|
self.EntityCapturingCtx:Stop()
|
||||||
|
self.MapCapturingCtx:Stop()
|
||||||
|
end
|
||||||
|
87
files/check.lua
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
-- Copyright (c) 2019-2022 David Vogel
|
||||||
|
--
|
||||||
|
-- This software is released under the MIT License.
|
||||||
|
-- https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
-- Check if everything is alright.
|
||||||
|
-- This does mainly trigger user messages and suggest actions.
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Load global stuff --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
-- Load library modules --
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
local Coords = require("coordinates")
|
||||||
|
local ScreenCap = require("screen-capture")
|
||||||
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
local Utils= require("noita-api.utils")
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Code --
|
||||||
|
----------
|
||||||
|
|
||||||
|
---Runs a list of checks at addon startup.
|
||||||
|
function Check:Startup()
|
||||||
|
if Utils.FileExists("mods/noita-mapcap/output/nonempty") then
|
||||||
|
Message:ShowOutputNonEmpty()
|
||||||
|
end
|
||||||
|
|
||||||
|
if not Utils.FileExists("mods/noita-mapcap/bin/capture-b/capture.dll") then
|
||||||
|
Message:ShowGeneralInstallationProblem("`capture.dll` is missing.", "Make sure you have installed the mod correctly.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not Utils.FileExists("mods/noita-mapcap/bin/stitch/stitch.exe") then
|
||||||
|
Message:ShowGeneralInstallationProblem("`stitch.exe` is missing.", "Make sure you have installed the mod correctly.", " ", "You can still use the mod to capture, though.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Runs a list of checks for everything resolution related.
|
||||||
|
---@param interval integer -- Check interval in frames.
|
||||||
|
function Check:Resolutions(interval)
|
||||||
|
interval = interval or 60
|
||||||
|
self.Counter = (self.Counter or 0) - 1
|
||||||
|
if self.Counter > 0 then return end
|
||||||
|
self.Counter = interval
|
||||||
|
|
||||||
|
-- Compare Noita config and actual window resolution.
|
||||||
|
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
|
||||||
|
if topLeft and bottomRight then
|
||||||
|
local actual = bottomRight - topLeft
|
||||||
|
if actual ~= Coords.WindowResolution then
|
||||||
|
Message:ShowWrongResolution(Modification.AutoSet, string.format("Old window resolution is %s. Current resolution is %s.", Coords.WindowResolution, actual))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if we have the required settings.
|
||||||
|
local config, magic = Modification.RequiredChanges()
|
||||||
|
if config["fullscreen"] then
|
||||||
|
local expected = tonumber(config["fullscreen"])
|
||||||
|
if expected ~= Coords.FullscreenMode then
|
||||||
|
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Expected fullscreen mode %s. But got %s.", expected, Coords.FullscreenMode))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if config["window_w"] and config["window_h"] then
|
||||||
|
local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"]))
|
||||||
|
if expected ~= Coords.WindowResolution then
|
||||||
|
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Expected window resolution is %s. But got %s.", expected, Coords.WindowResolution))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if config["internal_size_w"] and config["internal_size_h"] then
|
||||||
|
local expected = Vec2(tonumber(config["internal_size_w"]), tonumber(config["internal_size_h"]))
|
||||||
|
if expected ~= Coords.InternalResolution then
|
||||||
|
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Expected internal resolution is %s. But got %s.", expected, Coords.InternalResolution))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if magic["VIRTUAL_RESOLUTION_X"] and magic["VIRTUAL_RESOLUTION_Y"] then
|
||||||
|
local expected = Vec2(tonumber(magic["VIRTUAL_RESOLUTION_X"]), tonumber(magic["VIRTUAL_RESOLUTION_Y"]))
|
||||||
|
if expected ~= Coords.VirtualResolution then
|
||||||
|
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Expected virtual resolution is %s. But got %s.", expected, Coords.VirtualResolution))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -34,18 +34,20 @@ local Vec2 = require("noita-api.vec2")
|
|||||||
-- Code --
|
-- Code --
|
||||||
----------
|
----------
|
||||||
|
|
||||||
local virtualOffsetPixelPerfect = Vec2(-2, 0)
|
|
||||||
|
|
||||||
---@class Coords
|
---@class Coords
|
||||||
---@field InternalResolution Vec2 -- Size of the internal rectangle in window pixels.
|
---@field InternalResolution Vec2 -- Size of the internal rectangle in window pixels.
|
||||||
---@field WindowResolution Vec2 -- Size of the window client area in window pixels.
|
---@field WindowResolution Vec2 -- Size of the window client area in window pixels.
|
||||||
---@field VirtualResolution Vec2 -- Size of the virtual rectangle in world/virtual pixels.
|
---@field VirtualResolution Vec2 -- Size of the virtual rectangle in world/virtual pixels.
|
||||||
---@field VirtualOffset Vec2 -- Offset of the virtual rectangle in world/virtual pixels.
|
---@field VirtualOffset Vec2 -- Offset of the virtual rectangle in world/virtual pixels.
|
||||||
|
---@field VirtualOffsetPixelPerfect Vec2 -- Offset of the virtual rectangle that maps chunks perfectly to the window.
|
||||||
|
---@field FullscreenMode integer -- The fullscreen mode the game is in. 0 is windowed.
|
||||||
local Coords = {
|
local Coords = {
|
||||||
InternalResolution = Vec2(0, 0),
|
InternalResolution = Vec2(0, 0),
|
||||||
WindowResolution = Vec2(0, 0),
|
WindowResolution = Vec2(0, 0),
|
||||||
VirtualResolution = Vec2(0, 0),
|
VirtualResolution = Vec2(0, 0),
|
||||||
VirtualOffset = Vec2(0, 0),
|
VirtualOffset = Vec2(0, 0),
|
||||||
|
VirtualOffsetPixelPerfect = Vec2(-2, 0),
|
||||||
|
FullscreenMode = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
---Reads and updates the internal, window and virtual resolutions from Noita's config files and API.
|
---Reads and updates the internal, window and virtual resolutions from Noita's config files and API.
|
||||||
@ -62,6 +64,7 @@ function Coords:ReadResolutions()
|
|||||||
self.InternalResolution = Vec2(tonumber(xml.attr["internal_size_w"]), tonumber(xml.attr["internal_size_h"]))
|
self.InternalResolution = Vec2(tonumber(xml.attr["internal_size_w"]), tonumber(xml.attr["internal_size_h"]))
|
||||||
self.VirtualResolution = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y")))
|
self.VirtualResolution = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y")))
|
||||||
self.VirtualOffset = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_Y")))
|
self.VirtualOffset = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_Y")))
|
||||||
|
self.FullscreenMode = tonumber(xml.attr["fullscreen"]) or 0
|
||||||
|
|
||||||
f:close()
|
f:close()
|
||||||
return nil
|
return nil
|
||||||
@ -130,7 +133,7 @@ function Coords:ToWindow(world, viewportCenter)
|
|||||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||||
local pixelScale = self:PixelScale()
|
local pixelScale = self:PixelScale()
|
||||||
|
|
||||||
return internalTopLeft + (self.VirtualResolution / 2 + world - viewportCenter - virtualOffsetPixelPerfect + self.VirtualOffset) * pixelScale
|
return internalTopLeft + (self.VirtualResolution / 2 + world - viewportCenter - self.VirtualOffsetPixelPerfect + self.VirtualOffset) * pixelScale
|
||||||
end
|
end
|
||||||
|
|
||||||
---Converts the given window coordinates into world/virtual coordinates.
|
---Converts the given window coordinates into world/virtual coordinates.
|
||||||
@ -143,7 +146,7 @@ function Coords:ToWorld(window, viewportCenter)
|
|||||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||||
local pixelScale = self:PixelScale()
|
local pixelScale = self:PixelScale()
|
||||||
|
|
||||||
return viewportCenter - self.VirtualResolution / 2 + (window - internalTopLeft) / pixelScale + virtualOffsetPixelPerfect - self.VirtualOffset
|
return viewportCenter - self.VirtualResolution / 2 + (window - internalTopLeft) / pixelScale + self.VirtualOffsetPixelPerfect - self.VirtualOffset
|
||||||
end
|
end
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
|
@ -42,14 +42,15 @@ function ScreenCap.Capture(topLeft, bottomRight, topLeftOutput, finalDimensions)
|
|||||||
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.
|
||||||
---@return any
|
---@return Vec2|nil topLeft
|
||||||
|
---@return Vec2|nil bottomRight
|
||||||
function ScreenCap.GetRect()
|
function ScreenCap.GetRect()
|
||||||
local rect = ffi.new("RECT")
|
local rect = ffi.new("RECT")
|
||||||
if not res.GetRect(rect) then
|
if not res.GetRect(rect) then
|
||||||
return nil
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return rect
|
return Vec2(rect.left, rect.top), Vec2(rect.right, rect.bottom)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ScreenCap
|
return ScreenCap
|
||||||
|
133
files/message.lua
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
-- Copyright (c) 2019-2022 David Vogel
|
||||||
|
--
|
||||||
|
-- This software is released under the MIT License.
|
||||||
|
-- https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Load global stuff --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
-- Load library modules --
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
local Coords = require("coordinates")
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Code --
|
||||||
|
----------
|
||||||
|
|
||||||
|
---Add a general runtime error message to the message list.
|
||||||
|
---This will always overwrite the last runtime error with the same id.
|
||||||
|
---@param id string
|
||||||
|
---@param ... string
|
||||||
|
function Message:ShowRuntimeError(id, ...)
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["RuntimeError" .. id] = {
|
||||||
|
Type = "error",
|
||||||
|
Lines = { ... },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calls func and catches any exception.
|
||||||
|
---If there is one, a runtime error message will be shown to the user.
|
||||||
|
---@param id string
|
||||||
|
---@param func function
|
||||||
|
function Message:CatchException(id, func)
|
||||||
|
local ok, err = pcall(func)
|
||||||
|
if not ok then
|
||||||
|
self:ShowRuntimeError(id, string.format("An exception happened in %s", id), err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Request the user to let the addon automatically reset some Noita settings.
|
||||||
|
function Message:ShowResetNoitaSettings()
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["ResetNoitaSettings"] = {
|
||||||
|
Type = "info",
|
||||||
|
Lines = {
|
||||||
|
"You requested to reset some game settings like:",
|
||||||
|
"- Custom resolutions",
|
||||||
|
" ",
|
||||||
|
"Press the following button to reset the settings and close Noita automatically:",
|
||||||
|
},
|
||||||
|
Actions = {
|
||||||
|
{ Name = "Reset and close", Hint = nil, HintDesc = nil, Callback = function() Modification:Reset() end },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Request the user to let the addon automatically set Noita settings based on the given callback.
|
||||||
|
---@param callback function
|
||||||
|
---@param desc string -- What's wrong.
|
||||||
|
function Message:ShowSetNoitaSettings(callback, desc)
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["SetNoitaSettings"] = {
|
||||||
|
Type = "warning",
|
||||||
|
Lines = {
|
||||||
|
"It seems that not all requested settings are applied to Noita:",
|
||||||
|
desc or "",
|
||||||
|
" ",
|
||||||
|
"Press the button at the bottom to set up and close Noita automatically.",
|
||||||
|
"Alternatively disable `Use custom resolution` in the mod settings.",
|
||||||
|
" ",
|
||||||
|
"You can always reset these settings by right clicking the `start capture`",
|
||||||
|
"button at the top left.",
|
||||||
|
},
|
||||||
|
Actions = {
|
||||||
|
{ Name = "Setup and close", Hint = nil, HintDesc = nil, Callback = callback },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Request the user to let the addon automatically set Noita settings based on the given callback.
|
||||||
|
---@param callback function
|
||||||
|
---@param desc string -- What's wrong.
|
||||||
|
function Message:ShowWrongResolution(callback, desc)
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["WrongResolution"] = {
|
||||||
|
Type = "warning",
|
||||||
|
Lines = {
|
||||||
|
"The resolution changed:",
|
||||||
|
desc or "",
|
||||||
|
" ",
|
||||||
|
"To fix: Restart Noita or revert the change."
|
||||||
|
},
|
||||||
|
Actions = {
|
||||||
|
{ Name = "Query settings again", Hint = nil, HintDesc = nil, Callback = function() Coords:ReadResolutions() end },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Tell the user that there are files in the output directory.
|
||||||
|
function Message:ShowOutputNonEmpty()
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["OutputNonEmpty"] = {
|
||||||
|
Type = "hint",
|
||||||
|
Lines = {
|
||||||
|
"There are already files in the output directory.",
|
||||||
|
"If you are continuing a capture session, ignore this message.",
|
||||||
|
" ",
|
||||||
|
"If you are about to capture a new map, make sure to delete all files in the output directory first."
|
||||||
|
},
|
||||||
|
Actions = {
|
||||||
|
{ Name = "Open output directory", Hint = nil, HintDesc = nil, Callback = function() os.execute("start .\\mods\\noita-mapcap\\output\\") end },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Tell the user that there is something wrong with the mod installation.
|
||||||
|
---@param ... string
|
||||||
|
function Message:ShowGeneralInstallationProblem(...)
|
||||||
|
self.List = self.List or {}
|
||||||
|
|
||||||
|
self.List["GeneralInstallationProblem"] = {
|
||||||
|
Type = "error",
|
||||||
|
Lines = { ... },
|
||||||
|
}
|
||||||
|
end
|
120
files/modification.lua
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
-- Copyright (c) 2022 David Vogel
|
||||||
|
--
|
||||||
|
-- This software is released under the MIT License.
|
||||||
|
-- https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
-- Noita settings/configuration modifications.
|
||||||
|
-- We try to keep modifications to a minimum, but some things have to be changed in order for the mod to work correctly.
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
-- Load library modules --
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
local NXML = require("luanxml.nxml")
|
||||||
|
local Utils = require("noita-api.utils")
|
||||||
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
local Coords = require("coordinates")
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Code --
|
||||||
|
----------
|
||||||
|
|
||||||
|
---Will update Noita's `config.xml` with the values in the given table.
|
||||||
|
---
|
||||||
|
---This will force close Noita!
|
||||||
|
---@param config table<string, string> -- List of `config.xml` attributes that should be changed.
|
||||||
|
function Modification.SetConfig(config)
|
||||||
|
local configFilename = Utils.GetSpecialDirectory("save-shared") .. "config.xml"
|
||||||
|
|
||||||
|
-- Read and modify config.
|
||||||
|
local f, err = io.open(configFilename, "r")
|
||||||
|
if not f then error(string.format("failed to read config file: %s", err)) end
|
||||||
|
local xml = NXML.parse(f:read("*a"))
|
||||||
|
|
||||||
|
for k, v in pairs(config) do
|
||||||
|
xml.attr[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
-- Write modified config back.
|
||||||
|
local f, err = io.open(configFilename, "w")
|
||||||
|
if not f then error(string.format("failed to create config file: %s", err)) end
|
||||||
|
f:write(tostring(xml))
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
-- We need to force close Noita, so it doesn't have any chance to overwrite the file.
|
||||||
|
os.exit(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Will update Noita's `magic_numbers.xml` with the values in the given table.
|
||||||
|
---
|
||||||
|
---Should be called on mod initialization only.
|
||||||
|
---@param magic table<string, string> -- List of `magic_numbers.xml` attributes that should be changed.
|
||||||
|
function Modification.SetMagicNumbers(magic)
|
||||||
|
local xml = NXML.new_element("MagicNumbers", magic)
|
||||||
|
|
||||||
|
-- Write magic number file.
|
||||||
|
local f, err = io.open("mods/noita-mapcap/files/magic-numbers/generated.xml", "w")
|
||||||
|
if not f then error(string.format("failed to create config file: %s", err)) end
|
||||||
|
f:write(tostring(xml))
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/generated.xml")
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns tables with user requested game configuration changes.
|
||||||
|
---@return table config -- List of `config.xml` attributes that should be changed.
|
||||||
|
---@return table magic -- List of `magic_number.xml` attributes that should be changed.
|
||||||
|
function Modification.RequiredChanges()
|
||||||
|
local config, magic = {}, {}
|
||||||
|
|
||||||
|
-- Does the user request a custom resolution?
|
||||||
|
local customResolution = (ModSettingGet("noita-mapcap.custom-resolution-live") and ModSettingGet("noita-mapcap.capture-mode") == "live")
|
||||||
|
or (ModSettingGet("noita-mapcap.custom-resolution-other") and ModSettingGet("noita-mapcap.capture-mode") ~= "live")
|
||||||
|
|
||||||
|
if customResolution then
|
||||||
|
config["window_w"] = tostring(Vec2(ModSettingGet("noita-mapcap.window-resolution")).x)
|
||||||
|
config["window_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.window-resolution")).y)
|
||||||
|
config["internal_size_w"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).x)
|
||||||
|
config["internal_size_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).y)
|
||||||
|
config["backbuffer_width"] = config["window_w"]
|
||||||
|
config["backbuffer_height"] = config["window_h"]
|
||||||
|
magic["VIRTUAL_RESOLUTION_X"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).x)
|
||||||
|
magic["VIRTUAL_RESOLUTION_Y"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).y)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set virtual offset to be pixel perfect.
|
||||||
|
--magic["VIRTUAL_RESOLUTION_OFFSET_X"] = tostring(Coords.VirtualOffsetPixelPerfect.x)
|
||||||
|
--magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = tostring(Coords.VirtualOffsetPixelPerfect.y)
|
||||||
|
|
||||||
|
-- Always expect a fullscreen mode of 0 (windowed).
|
||||||
|
-- Capturing will not work in fullscreen.
|
||||||
|
config["fullscreen"] = "0"
|
||||||
|
|
||||||
|
return config, magic
|
||||||
|
end
|
||||||
|
|
||||||
|
---Will change the game settings according to `Modification.RequiredChanges()`.
|
||||||
|
---
|
||||||
|
---This will force close Noita!
|
||||||
|
function Modification.AutoSet()
|
||||||
|
local config, magic = Modification.RequiredChanges()
|
||||||
|
Modification.SetConfig(config)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Will reset all settings that may have been changed by this mod.
|
||||||
|
---
|
||||||
|
---This will force close Noita!
|
||||||
|
function Modification.Reset()
|
||||||
|
local config = {
|
||||||
|
window_w = "1280",
|
||||||
|
window_h = "720",
|
||||||
|
internal_size_w = "1280",
|
||||||
|
internal_size_h = "720",
|
||||||
|
backbuffer_width = "1280",
|
||||||
|
backbuffer_height = "720",
|
||||||
|
}
|
||||||
|
|
||||||
|
Modification.SetConfig(config)
|
||||||
|
end
|
BIN
files/ui-gfx/dismiss-8x8.png
Normal file
After Width: | Height: | Size: 171 B |
BIN
files/ui-gfx/hint-16x16.png
Normal file
After Width: | Height: | Size: 206 B |
BIN
files/ui-gfx/open-output-16x16.png
Normal file
After Width: | Height: | Size: 253 B |
BIN
files/ui-gfx/record-16x16.png
Normal file
After Width: | Height: | Size: 231 B |
BIN
files/ui-gfx/reset-16x16.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
files/ui-gfx/stop-16x16.png
Normal file
After Width: | Height: | Size: 215 B |
BIN
files/ui-gfx/warning-16x16.png
Normal file
After Width: | Height: | Size: 217 B |
239
files/ui.lua
@ -3,17 +3,120 @@
|
|||||||
-- 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
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Load global stuff --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- TODO: Wrap Noita utilities and wrap them into a table: https://stackoverflow.com/questions/9540732/loadfile-without-polluting-global-environment
|
||||||
|
require("utilities") -- Loads Noita's utilities from `data/scripts/lib/utilitites.lua`.
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
-- Load library modules --
|
-- Load library modules --
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
local Utils = require("noita-api.utils")
|
|
||||||
local ScreenCap = require("screen-capture")
|
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Code --
|
-- Code --
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
---Returns unique IDs for the widgets.
|
||||||
|
---`_ResetID` has to be called every time before the UI is rebuilt.
|
||||||
|
---@return integer
|
||||||
|
function UI:_GenID()
|
||||||
|
self.CurrentID = (self.CurrentID or 0) + 1
|
||||||
|
return self.CurrentID
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:_ResetID()
|
||||||
|
self.CurrentID = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:_DrawToolbar()
|
||||||
|
local gui = self.gui
|
||||||
|
GuiZSet(gui, 0)
|
||||||
|
|
||||||
|
GuiLayoutBeginHorizontal(gui, 2, 2, true, 2, 2)
|
||||||
|
|
||||||
|
if Capture.MapCapturingCtx:IsRunning() then
|
||||||
|
local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/stop-16x16.png")
|
||||||
|
GuiTooltip(gui, "Stop capture", "Stop the capturing process.\n \nRight click: Reset any modifications that this mod has done to Noita.")
|
||||||
|
if clicked then Capture:StopCapturing() end
|
||||||
|
if clickedRight then Message:ShowResetNoitaSettings() end
|
||||||
|
else
|
||||||
|
local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/record-16x16.png")
|
||||||
|
GuiTooltip(gui, "Start capture", "Start the capturing process based on mod settings.\n \nRight click: Reset any modifications that this mod has done to Noita.")
|
||||||
|
if clicked then Capture:StartCapturing() end
|
||||||
|
if clickedRight then Message:ShowResetNoitaSettings() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local clicked = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/open-output-16x16.png")
|
||||||
|
GuiTooltip(gui, "Open output directory", "Reveals the output directory in your file browser.")
|
||||||
|
if clicked then os.execute("start .\\mods\\noita-mapcap\\output\\") end
|
||||||
|
|
||||||
|
GuiLayoutEnd(gui)
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:_DrawMessages(messages)
|
||||||
|
local gui = self.gui
|
||||||
|
|
||||||
|
-- Abort if there is no messages list.
|
||||||
|
if not messages then return end
|
||||||
|
|
||||||
|
GuiZSet(gui, 0)
|
||||||
|
|
||||||
|
-- Unfortunately you can't stack multiple layout containers with the same direction.
|
||||||
|
-- So keep track of the y position manually.
|
||||||
|
local posY = 60
|
||||||
|
for key, message in pairs(messages) do
|
||||||
|
GuiZSet(gui, -10)
|
||||||
|
GuiBeginAutoBox(gui)
|
||||||
|
|
||||||
|
GuiLayoutBeginHorizontal(gui, 27, posY, true, 5, 0) posY = posY + 20
|
||||||
|
|
||||||
|
if message.Type == "warning" or message.Type == "error" then
|
||||||
|
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/warning-16x16.png", 1, 1, 0, 0, 0, "")
|
||||||
|
elseif message.Type == "hint" or message.Type == "info" then
|
||||||
|
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "")
|
||||||
|
else
|
||||||
|
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
GuiLayoutBeginVertical(gui, 0, 0, false, 0, 0)
|
||||||
|
if type(message.Lines) == "table" then
|
||||||
|
for _, line in ipairs(message.Lines) do
|
||||||
|
GuiText(gui, 0, 0, tostring(line)) posY = posY + 11
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(message.Actions) == "table" then
|
||||||
|
posY = posY + 11
|
||||||
|
for _, action in ipairs(message.Actions) do
|
||||||
|
local clicked = GuiButton(gui, self:_GenID(), 0, 11, ">" .. action.Name .. " <") posY = posY + 11
|
||||||
|
if action.Hint or action.HintDesc then
|
||||||
|
GuiTooltip(gui, action.Hint or "", action.HintDesc or "")
|
||||||
|
end
|
||||||
|
if clicked then
|
||||||
|
local ok, err = pcall(action.Callback)
|
||||||
|
if not ok then
|
||||||
|
Message:ShowRuntimeError("MessageAction", "Message action error:", err)
|
||||||
|
end
|
||||||
|
messages[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
GuiLayoutEnd(gui)
|
||||||
|
|
||||||
|
local clicked = GuiImageButton(gui, self:_GenID(), 5, 0, "", "mods/noita-mapcap/files/ui-gfx/dismiss-8x8.png")
|
||||||
|
--GuiTooltip(gui, "Dismiss message", "")
|
||||||
|
if clicked then messages[key] = nil end
|
||||||
|
|
||||||
|
GuiLayoutEnd(gui)
|
||||||
|
|
||||||
|
GuiZSet(gui, -9)
|
||||||
|
GuiEndAutoBoxNinePiece(gui, 5, 0, 0, false, 0, "data/ui_gfx/decorations/9piece0_gray.png", "data/ui_gfx/decorations/9piece0_gray.png")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Stops the UI from drawing for the next few frames.
|
||||||
|
---@param frames integer
|
||||||
function UI:SuspendDrawing(frames)
|
function UI:SuspendDrawing(frames)
|
||||||
self.suspendFrames = math.max(self.suspendFrames or 0, frames)
|
self.suspendFrames = math.max(self.suspendFrames or 0, frames)
|
||||||
end
|
end
|
||||||
@ -26,131 +129,15 @@ function UI:Draw()
|
|||||||
if self.suspendFrames and self.suspendFrames > 0 then self.suspendFrames = self.suspendFrames - 1 return end
|
if self.suspendFrames and self.suspendFrames > 0 then self.suspendFrames = self.suspendFrames - 1 return end
|
||||||
self.suspendFrames = nil
|
self.suspendFrames = nil
|
||||||
|
|
||||||
|
-- Reset ID generator.
|
||||||
|
self:_ResetID()
|
||||||
|
|
||||||
GuiStartFrame(gui)
|
GuiStartFrame(gui)
|
||||||
|
|
||||||
GuiLayoutBeginVertical(gui, 50, 20, false, 0, 0)
|
GuiIdPushString(gui, "noita-mapcap")
|
||||||
|
|
||||||
GuiTextCentered(gui, 0, 0, "Heyho")
|
self:_DrawToolbar()
|
||||||
|
self:_DrawMessages(Message.List)
|
||||||
|
|
||||||
GuiLayoutEnd(gui)
|
GuiIdPop(gui)
|
||||||
|
|
||||||
if true then return end
|
|
||||||
|
|
||||||
GuiStartFrame(modGUI)
|
|
||||||
|
|
||||||
GuiLayoutBeginVertical(modGUI, 50, 20)
|
|
||||||
if not UiProgress then
|
|
||||||
-- Show informations
|
|
||||||
local problem
|
|
||||||
local rect = ScreenCap.GetRect()
|
|
||||||
|
|
||||||
if not rect then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, '!!! WARNING !!! You are not using "Windowed" mode.')
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, '- Change the window mode in the game options to "Windowed"')
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
problem = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if rect then
|
|
||||||
local screenWidth, screenHeight = rect.right - rect.left, rect.bottom - rect.top
|
|
||||||
local virtualWidth, virtualHeight = tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
||||||
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("VIRTUAL_RESOLUTION_*: %d, %d", virtualWidth, virtualHeight))
|
|
||||||
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, "To fix the problem, do one of these:")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format(
|
|
||||||
"- Change the resolution in the game options to %dx%d",
|
|
||||||
virtualWidth * CAPTURE_PIXEL_SIZE,
|
|
||||||
virtualHeight * 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
|
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format("- Change the CAPTURE_PIXEL_SIZE in the mod to %f", ratioX))
|
|
||||||
end
|
|
||||||
GuiTextCentered(modGUI, 0, 0, '- Make sure that the console is not selected')
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
problem = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not Utils.FileExists("mods/noita-mapcap/bin/capture-b/capture.dll") then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Can't find library for screenshots.")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "- Redownload a release of this mod from GitHub, don't download the sourcecode")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
problem = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if not Utils.FileExists("mods/noita-mapcap/bin/stitch/stitch.exe") then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Can't find software for stitching.")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "You can still take screenshots, but you won't be able to stitch those screenshots.")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "- Redownload a release of this mod from GitHub, don't download the sourcecode")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
problem = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if not problem then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "No problems found.")
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
end
|
|
||||||
|
|
||||||
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, "Use ESC to pause, and close the game to stop the process.")
|
|
||||||
GuiTextCentered(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, 'If you want to start a new map, you have to delete all images from the "output" folder!')
|
|
||||||
--GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
--GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_X"))
|
|
||||||
--GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
|
||||||
--GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_X"))
|
|
||||||
--GuiTextCentered(modGUI, 0, 0, MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_Y"))
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
if GuiButton(modGUI, 0, 0, ">> Start capturing map around view <<", 1) then
|
|
||||||
UiProgress = {}
|
|
||||||
startCapturingSpiral()
|
|
||||||
end
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
if GuiButton(modGUI, 0, 0, ">> Start capturing base layout <<", 1) then
|
|
||||||
UiProgress = {}
|
|
||||||
startCapturingHilbert(CAPTURE_AREA_BASE_LAYOUT)
|
|
||||||
end
|
|
||||||
if GuiButton(modGUI, 0, 0, ">> Start capturing main world <<", 1) then
|
|
||||||
UiProgress = {}
|
|
||||||
startCapturingHilbert(CAPTURE_AREA_MAIN_WORLD)
|
|
||||||
end
|
|
||||||
if GuiButton(modGUI, 0, 0, ">> Start capturing extended map <<", 1) then
|
|
||||||
UiProgress = {}
|
|
||||||
startCapturingHilbert(CAPTURE_AREA_EXTENDED)
|
|
||||||
end
|
|
||||||
if GuiButton(modGUI, 0, 0, ">> Start capturing run live <<", 1) then
|
|
||||||
UiProgress = {}
|
|
||||||
StartCapturingLive()
|
|
||||||
end
|
|
||||||
GuiTextCentered(modGUI, 0, 0, " ")
|
|
||||||
elseif not UiProgress.Done then
|
|
||||||
-- Show progress
|
|
||||||
local x, y = GameGetCameraPos()
|
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format("Coordinates: %d, %d", x, y))
|
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format("Waiting %d frames...", UiCaptureDelay))
|
|
||||||
if UiProgress.Progress then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, progressBarString(
|
|
||||||
UiProgress, { BarLength = 100, CharFull = "l", CharEmpty = ".", Format = "|%s| [%d / %d] [%1.2f%%]" }
|
|
||||||
))
|
|
||||||
end
|
|
||||||
if UiCaptureProblem then
|
|
||||||
GuiTextCentered(modGUI, 0, 0, string.format("A problem occurred while capturing: %s", UiCaptureProblem))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
GuiTextCentered(modGUI, 0, 0, "Done!")
|
|
||||||
end
|
|
||||||
GuiLayoutEnd(modGUI)
|
|
||||||
end
|
end
|
||||||
|
39
init.lua
@ -12,6 +12,7 @@
|
|||||||
local libPath = "mods/noita-mapcap/files/libraries/"
|
local libPath = "mods/noita-mapcap/files/libraries/"
|
||||||
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
||||||
|
|
||||||
|
-- TODO: Replace Noita's coroutine lib with something better
|
||||||
if not async then
|
if not async then
|
||||||
require("coroutines") -- Loads Noita's coroutines library from `data/scripts/lib/coroutines.lua`.
|
require("coroutines") -- Loads Noita's coroutines library from `data/scripts/lib/coroutines.lua`.
|
||||||
end
|
end
|
||||||
@ -23,7 +24,7 @@ end
|
|||||||
local CameraAPI = require("noita-api.camera")
|
local CameraAPI = require("noita-api.camera")
|
||||||
local Coords = require("coordinates")
|
local Coords = require("coordinates")
|
||||||
local DebugAPI = require("noita-api.debug")
|
local DebugAPI = require("noita-api.debug")
|
||||||
--local LiveReload = require("noita-api.live-reload")
|
local LiveReload = require("noita-api.live-reload")
|
||||||
local Vec2 = require("noita-api.vec2")
|
local Vec2 = require("noita-api.vec2")
|
||||||
|
|
||||||
-----------------------
|
-----------------------
|
||||||
@ -31,7 +32,10 @@ local Vec2 = require("noita-api.vec2")
|
|||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Capture = Capture or {}
|
Capture = Capture or {}
|
||||||
|
Check = Check or {}
|
||||||
Config = Config or {}
|
Config = Config or {}
|
||||||
|
Message = Message or {}
|
||||||
|
Modification = Modification or {}
|
||||||
UI = UI or {}
|
UI = UI or {}
|
||||||
|
|
||||||
-------------------------------
|
-------------------------------
|
||||||
@ -40,6 +44,9 @@ UI = UI or {}
|
|||||||
|
|
||||||
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/config.lua")
|
||||||
|
dofile("mods/noita-mapcap/files/check.lua")
|
||||||
|
dofile("mods/noita-mapcap/files/message.lua")
|
||||||
|
dofile("mods/noita-mapcap/files/modification.lua")
|
||||||
dofile("mods/noita-mapcap/files/ui.lua")
|
dofile("mods/noita-mapcap/files/ui.lua")
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
@ -48,9 +55,13 @@ dofile("mods/noita-mapcap/files/ui.lua")
|
|||||||
|
|
||||||
---Called in order upon loading a new(?) game.
|
---Called in order upon loading a new(?) game.
|
||||||
function OnModPreInit()
|
function OnModPreInit()
|
||||||
|
-- Set magic numbers based on mod settings.
|
||||||
|
local config, magic = Modification.RequiredChanges()
|
||||||
|
Modification.SetMagicNumbers(magic)
|
||||||
|
|
||||||
-- Override virtual resolution and some other stuff.
|
-- Override virtual resolution and some other stuff.
|
||||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/64.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/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")
|
||||||
|
|
||||||
@ -84,19 +95,29 @@ end
|
|||||||
|
|
||||||
---Called *every* time the game is about to start updating the world.
|
---Called *every* time the game is about to start updating the world.
|
||||||
function OnWorldPreUpdate()
|
function OnWorldPreUpdate()
|
||||||
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
|
Message:CatchException("OnWorldPreUpdate", function ()
|
||||||
wake_up_waiting_threads(1)
|
|
||||||
|
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
|
||||||
|
wake_up_waiting_threads(1)
|
||||||
|
|
||||||
|
end)
|
||||||
end
|
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()
|
|
||||||
|
|
||||||
-- Reload mod every 60 frames.
|
-- Reload mod every 60 frames.
|
||||||
-- This allows live updates to the mod while Noita is running.
|
-- This allows live updates to the mod while Noita is running.
|
||||||
-- !!! DISABLE THIS LINE AND THE CORRESPONDING REQUIRE BEFORE COMMITTING !!!
|
-- !!! DISABLE THIS LINE AND THE CORRESPONDING REQUIRE BEFORE COMMITTING !!!
|
||||||
--LiveReload:Reload("mods/noita-mapcap/", 60)
|
--LiveReload:Reload("mods/noita-mapcap/", 60)
|
||||||
|
|
||||||
|
Message:CatchException("OnWorldPostUpdate", function ()
|
||||||
|
|
||||||
|
Check:Resolutions(60)
|
||||||
|
|
||||||
|
-- Draw UI after coroutines have been resumed.
|
||||||
|
UI:Draw()
|
||||||
|
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Called when the biome config is loaded.
|
---Called when the biome config is loaded.
|
||||||
@ -109,6 +130,8 @@ function OnMagicNumbersAndWorldSeedInitialized()
|
|||||||
-- Get resolutions for correct coordinate transformations.
|
-- Get resolutions for correct coordinate transformations.
|
||||||
-- This needs to be done once all magic numbers are set.
|
-- This needs to be done once all magic numbers are set.
|
||||||
Coords:ReadResolutions()
|
Coords:ReadResolutions()
|
||||||
|
|
||||||
|
Check:Startup()
|
||||||
end
|
end
|
||||||
|
|
||||||
---Called when the game is paused or unpaused.
|
---Called when the game is paused or unpaused.
|
||||||
|
86
settings.lua
@ -12,6 +12,7 @@
|
|||||||
local libPath = "mods/noita-mapcap/files/libraries/"
|
local libPath = "mods/noita-mapcap/files/libraries/"
|
||||||
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
||||||
|
|
||||||
|
-- TODO: Replace Noita's mod settings lib with something better. Or at least wrap it: https://stackoverflow.com/questions/9540732/loadfile-without-polluting-global-environment
|
||||||
require("mod_settings") -- Loads Noita's mod settings library from `data/scripts/lib/mod_settings.lua`.
|
require("mod_settings") -- Loads Noita's mod settings library from `data/scripts/lib/mod_settings.lua`.
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
@ -84,7 +85,8 @@ modSettings = {
|
|||||||
id = "capture-mode-spiral-origin-vector",
|
id = "capture-mode-spiral-origin-vector",
|
||||||
ui_name = " Origin",
|
ui_name = " Origin",
|
||||||
ui_description = "",
|
ui_description = "",
|
||||||
value_default = "0, 0",
|
value_default = "0,0",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||||
show_fn = function() return not modSettings:Get("capture-mode-spiral-origin").hidden and modSettings:GetNextValue("capture-mode-spiral-origin") == "custom" end,
|
show_fn = function() return not modSettings:Get("capture-mode-spiral-origin").hidden and modSettings:GetNextValue("capture-mode-spiral-origin") == "custom" end,
|
||||||
},
|
},
|
||||||
@ -101,7 +103,8 @@ modSettings = {
|
|||||||
id = "area-top-left",
|
id = "area-top-left",
|
||||||
ui_name = " Top left corner",
|
ui_name = " Top left corner",
|
||||||
ui_description = "The top left corner of the to be captured rectangle.",
|
ui_description = "The top left corner of the to be captured rectangle.",
|
||||||
value_default = "-512, -512",
|
value_default = "-512,-512",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||||
show_fn = function() return not modSettings:Get("area").hidden and modSettings:GetNextValue("area") == "custom" end,
|
show_fn = function() return not modSettings:Get("area").hidden and modSettings:GetNextValue("area") == "custom" end,
|
||||||
},
|
},
|
||||||
@ -109,7 +112,8 @@ modSettings = {
|
|||||||
id = "area-bottom-right",
|
id = "area-bottom-right",
|
||||||
ui_name = " Bottom right corner",
|
ui_name = " Bottom right corner",
|
||||||
ui_description = "The bottom right corner of the to be captured rectangle.",
|
ui_description = "The bottom right corner of the to be captured rectangle.",
|
||||||
value_default = "512, 512",
|
value_default = "512,512",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||||
show_fn = function() return not modSettings:Get("area").hidden and modSettings:GetNextValue("area") == "custom" end,
|
show_fn = function() return not modSettings:Get("area").hidden and modSettings:GetNextValue("area") == "custom" end,
|
||||||
},
|
},
|
||||||
@ -134,9 +138,9 @@ modSettings = {
|
|||||||
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
|
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id = "pixel-size",
|
id = "pixel-scale",
|
||||||
ui_name = "Pixel size",
|
ui_name = "Pixel scale",
|
||||||
ui_description = "How big a single resulting pixel will be.\nThis is the ratio of image to world pixels.\nA setting of 0 disables any scaling.",
|
ui_description = "How big a single resulting pixel will be.\nThis is the ratio of image to world pixels.\nA setting of 0 disables any scaling.\n \nDon't change while capturing,\nOr you will get unstitchable results.",
|
||||||
value_default = 1,
|
value_default = 1,
|
||||||
value_min = 0,
|
value_min = 0,
|
||||||
value_max = 8,
|
value_max = 8,
|
||||||
@ -165,7 +169,8 @@ modSettings = {
|
|||||||
id = "window-resolution",
|
id = "window-resolution",
|
||||||
ui_name = " Window resolution",
|
ui_name = " Window resolution",
|
||||||
ui_description = "Size of the window in screen pixels.",
|
ui_description = "Size of the window in screen pixels.",
|
||||||
value_default = "1024, 1024",
|
value_default = "1024,1024",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
||||||
show_fn = function()
|
show_fn = function()
|
||||||
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
||||||
@ -176,7 +181,8 @@ modSettings = {
|
|||||||
id = "internal-resolution",
|
id = "internal-resolution",
|
||||||
ui_name = " Internal resolution",
|
ui_name = " Internal resolution",
|
||||||
ui_description = "Size of the viewport in screen pixels.\nIdeally set to the window resolution.",
|
ui_description = "Size of the viewport in screen pixels.\nIdeally set to the window resolution.",
|
||||||
value_default = "1024, 1024",
|
value_default = "1024,1024",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
||||||
show_fn = function()
|
show_fn = function()
|
||||||
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
||||||
@ -187,13 +193,59 @@ modSettings = {
|
|||||||
id = "virtual-resolution",
|
id = "virtual-resolution",
|
||||||
ui_name = " Virtual resolution",
|
ui_name = " Virtual resolution",
|
||||||
ui_description = "Size of the viewport in world pixels.",
|
ui_description = "Size of the viewport in world pixels.",
|
||||||
value_default = "1024, 1024",
|
value_default = "1024,1024",
|
||||||
|
allowed_characters = "0123456789,",
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
scope = MOD_SETTING_SCOPE_RUNTIME_RESTART,
|
||||||
show_fn = function()
|
show_fn = function()
|
||||||
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
return (not modSettings:Get("advanced.settings.custom-resolution-live").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-live"))
|
||||||
or (not modSettings:Get("advanced.settings.custom-resolution-other").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-other"))
|
or (not modSettings:Get("advanced.settings.custom-resolution-other").hidden and modSettings:GetNextValue("advanced.settings.custom-resolution-other"))
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ui_fn = mod_setting_vertical_spacing,
|
||||||
|
not_setting = true,
|
||||||
|
show_fn = function() return modSettings:GetNextValue("capture-mode") == "live" end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = "live-interval",
|
||||||
|
ui_name = "Capture interval",
|
||||||
|
ui_description = "Capturing interval in frames.",
|
||||||
|
value_default = 60,
|
||||||
|
value_min = 5,
|
||||||
|
value_max = 240,
|
||||||
|
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 = "live-min-distance",
|
||||||
|
ui_name = "Min. capture distance",
|
||||||
|
ui_description = "The distance the viewport has to move to allow another screenshot.\nIn world pixels.",
|
||||||
|
value_default = 10,
|
||||||
|
value_min = 0,
|
||||||
|
value_max = 200,
|
||||||
|
value_display_multiplier = 1,
|
||||||
|
value_display_formatting = " $0 pixels",
|
||||||
|
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||||
|
show_fn = function() return modSettings:GetNextValue("capture-mode") == "live" end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = "live-max-distance",
|
||||||
|
ui_name = "Max. capture distance",
|
||||||
|
ui_description = "The distance the viewport has to move to force another screenshot.\nIn world pixels.",
|
||||||
|
value_default = 50,
|
||||||
|
value_min = 0,
|
||||||
|
value_max = 200,
|
||||||
|
value_display_multiplier = 1,
|
||||||
|
value_display_formatting = " $0 pixels",
|
||||||
|
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||||
|
show_fn = function() return modSettings:GetNextValue("capture-mode") == "live" end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ui_fn = mod_setting_vertical_spacing,
|
||||||
|
not_setting = true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id = "capture-entities",
|
id = "capture-entities",
|
||||||
ui_name = "Capture entities",
|
ui_name = "Capture entities",
|
||||||
@ -227,22 +279,6 @@ modSettings = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
category_id = "actions",
|
|
||||||
ui_name = "ACTIONS",
|
|
||||||
foldable = true,
|
|
||||||
_folded = true,
|
|
||||||
settings = {
|
|
||||||
{
|
|
||||||
id = "button-open-output",
|
|
||||||
ui_name = "Open output directory",
|
|
||||||
ui_description = "Reveals the output directory in your file browser.",
|
|
||||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
|
||||||
ui_fn = customSettingButton,
|
|
||||||
change_fn = function() print("test") end,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
---Hide/unhide some settings based on other settings.
|
---Hide/unhide some settings based on other settings.
|
||||||
|