diff --git a/README.md b/README.md index 1cc9fa8..5c650d9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ A resulting image with close to 3 gigapixels can be [seen here](https://easyzoom 3. Unpack it into your mods folder, so that you get the following file structure `.../Noita/mods/noita-mapcap/mod.xml`. 4. Set your resolution to 1280x720, and use the `Windowed` mode. (Not `Fullscreen (Windowed)`!) If you have to use a different resolution, see advanced usage. 5. Enable the mod and restart Noita. -6. In the game you should see a `>> Start capturing map <<` text on the screen, click it. +6. In the game you should see text on screen. + - Either press `>> Start capturing map around view <<` to capture in a spiral around your current view. + - Or press `>> Start capturing full map <<` to capture the whole map. 7. The screen will jump around, and the game will take screenshots automatically. - Screenshots are saved in `.../Noita/mods/noita-mapcap/output/`. - Don't cover the game window. @@ -50,6 +52,8 @@ The following two formulae have to be true: You can also change how much the tiles overlap by adjusting the `CAPTURE_GRID_SIZE` in `.../Noita/mods/noita-mapcap/files/capture.lua`. If you increase the grid size, you can capture more area per time. But on the other hand the stitcher may not be able to remove artifacts if the tiles don't overlap enough. +The rectangle for the full map capture mode is defined in `.../Noita/mods/noita-mapcap/files/capture.lua`. + ## License [MIT](LICENSE) diff --git a/files/capture.lua b/files/capture.lua index cc44df1..3051b58 100644 --- a/files/capture.lua +++ b/files/capture.lua @@ -6,8 +6,14 @@ CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio CAPTURE_GRID_SIZE = 420 -- in ingame pixels. There will always be 3 to 6 images overlapping CAPTURE_DELAY = 15 -- in frames +CAPTURE_BIGJUMP_DELAY = 20 -- in frames. Additional delay after doing a "larger than grid jump" CAPTURE_FORCE_HP = 4 -- * 25HP +CAPTURE_LEFT = -25000 -- in ingame pixels. Left edge of the full map capture rectangle +CAPTURE_TOP = -36000 -- in ingame pixels. Top edge of the full map capture rectangle +CAPTURE_RIGHT = 25000 -- in ingame pixels. Right edge of the full map capture rectangle (Pixels are not included in the rectangle) +CAPTURE_BOTTOM = 36000 -- in ingame pixels. Bottom edge of the full map capture rectangle (Pixels are not included in the rectangle) + local function preparePlayer() local playerEntity = getPlayer() addEffectToEntity(playerEntity, "PROTECTION_ALL") @@ -25,9 +31,8 @@ local function resetPlayer() setPlayerHP(CAPTURE_FORCE_HP) end -function startCapturing() +function startCapturingSpiral() local ox, oy = GameGetCameraPos() - --getPlayerPos() ox, oy = math.floor(ox / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE, math.floor(oy / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE local x, y = ox, oy @@ -47,7 +52,9 @@ function startCapturing() wait(CAPTURE_DELAY - 1) UiHide = true -- Hide UI while capturing the screenshot wait(1) - TriggerCapture(rx, ry) + if not TriggerCapture(rx, ry) then + UiCaptureProblem = "Screen capture failed. Please restart Noita." + end UiHide = false end x, y = x + CAPTURE_GRID_SIZE, y @@ -60,7 +67,9 @@ function startCapturing() wait(CAPTURE_DELAY - 1) UiHide = true wait(1) - TriggerCapture(rx, ry) + if not TriggerCapture(rx, ry) then + UiCaptureProblem = "Screen capture failed. Please restart Noita." + end UiHide = false end x, y = x, y + CAPTURE_GRID_SIZE @@ -74,7 +83,9 @@ function startCapturing() wait(CAPTURE_DELAY - 1) UiHide = true wait(1) - TriggerCapture(rx, ry) + if not TriggerCapture(rx, ry) then + UiCaptureProblem = "Screen capture failed. Please restart Noita." + end UiHide = false end x, y = x - CAPTURE_GRID_SIZE, y @@ -87,7 +98,9 @@ function startCapturing() wait(CAPTURE_DELAY - 1) UiHide = true wait(1) - TriggerCapture(rx, ry) + if not TriggerCapture(rx, ry) then + UiCaptureProblem = "Screen capture failed. Please restart Noita." + end UiHide = false end x, y = x, y - CAPTURE_GRID_SIZE @@ -96,3 +109,63 @@ function startCapturing() end ) end + +function startCapturingHilbert() + local ox, oy = GameGetCameraPos() + + -- Get size of the rectangle in grid/chunk coordinates + local gridLeft = math.floor(CAPTURE_LEFT / CAPTURE_GRID_SIZE) + local gridTop = math.floor(CAPTURE_TOP / CAPTURE_GRID_SIZE) + local gridRight = math.ceil(CAPTURE_RIGHT / CAPTURE_GRID_SIZE) + 1 + local gridBottom = math.ceil(CAPTURE_BOTTOM / CAPTURE_GRID_SIZE) + 1 + + -- Size of the grid in chunks + local gridWidth = gridRight - gridLeft + local gridHeight = gridBottom - gridTop + + -- Hilbert curve can only fit into a square, so get the longest side + local gridPOTSize = math.ceil(math.log(math.max(gridWidth, gridHeight)) / math.log(2)) + -- Max size (Already rounded up to the next power of two) + local gridMaxSize = math.pow(2, gridPOTSize) + + local t, tLimit = 0, gridMaxSize * gridMaxSize + + UiProgress = {Progress = 0, Max = gridWidth * gridHeight} + + preparePlayer() + + GameSetCameraFree(true) + + -- Coroutine to calculate next coordinate, and trigger screenshots + async( + function() + while t < tLimit do + local hx, hy = mapHilbert(t, gridPOTSize) + if hx < gridWidth and hy < gridHeight then + local x, y = (hx + gridLeft) * CAPTURE_GRID_SIZE, (hy + gridTop) * CAPTURE_GRID_SIZE + local rx, ry = x * CAPTURE_PIXEL_SIZE, y * CAPTURE_PIXEL_SIZE + if not fileExists(string.format("mods/noita-mapcap/output/%d,%d.png", rx, ry)) then + GameSetCameraPos(x, y) + + -- "Larger than grid jump" delay + if math.abs(x - ox) > CAPTURE_GRID_SIZE or math.abs(y - oy) > CAPTURE_GRID_SIZE then + wait(CAPTURE_BIGJUMP_DELAY) + end + ox, oy = x, y + + wait(CAPTURE_DELAY - 1) + UiHide = true -- Hide UI while capturing the screenshot + wait(1) + if not TriggerCapture(rx, ry) then + UiCaptureProblem = "Screen capture failed. Please restart Noita." + end + UiHide = false + end + UiProgress.Progress = UiProgress.Progress + 1 + end + + t = t + 1 + end + end + ) +end diff --git a/files/hilbert.lua b/files/hilbert.lua new file mode 100644 index 0000000..6e1aaa7 --- /dev/null +++ b/files/hilbert.lua @@ -0,0 +1,48 @@ +-- Copyright (c) 2019 David Vogel +-- +-- This software is released under the MIT License. +-- https://opensource.org/licenses/MIT + +local function hilbertRotate(n, x, y, rx, ry) + if not ry then + if rx then + x = n - 1 - x + y = n - 1 - y + end + + x, y = y, x + end + return x, y +end + +-- Maps a variable t to a hilbert curve with the side length of 2^potSize (Power of two size) +function mapHilbert(t, potSize) + local size = math.pow(2, potSize) + local x, y = 0, 0 + + if t < 0 or t >= size * size then + error("Variable t is outside of the range") + end + + for i = 0, potSize - 1, 1 do + local iPOT = math.pow(2, i) + local rx = bit.band(t, 2) == 2 + local ry = bit.band(t, 1) == 1 + if rx then + ry = not ry + end + + x, y = hilbertRotate(iPOT, x, y, rx, ry) + + if rx then + x = x + iPOT + end + if ry then + y = y + iPOT + end + + t = math.floor(t / 4) + end + + return x, y +end diff --git a/files/init.lua b/files/init.lua index 7d1d745..1e63f48 100644 --- a/files/init.lua +++ b/files/init.lua @@ -10,6 +10,7 @@ dofile("data/scripts/perks/perk_list.lua") dofile("mods/noita-mapcap/files/compatibility.lua") dofile("mods/noita-mapcap/files/util.lua") +dofile("mods/noita-mapcap/files/hilbert.lua") dofile("mods/noita-mapcap/files/external.lua") dofile("mods/noita-mapcap/files/capture.lua") dofile("mods/noita-mapcap/files/ui.lua") diff --git a/files/magic_numbers.xml b/files/magic_numbers.xml index 4ba9c84..d92cc88 100644 --- a/files/magic_numbers.xml +++ b/files/magic_numbers.xml @@ -1,12 +1,11 @@ + DEBUG_NO_PAUSE_ON_WINDOW_FOCUS_LOST="1"> diff --git a/files/ui.lua b/files/ui.lua index 5efb53f..0d92ece 100644 --- a/files/ui.lua +++ b/files/ui.lua @@ -5,6 +5,8 @@ UiHide = false local UiReduce = false +UiProgress = nil +UiCaptureProblem = nil async_loop( function() @@ -86,7 +88,7 @@ async_loop( end GuiTextCentered(modGUI, 0, 0, "You can freely look around and search a place to start capturing.") - GuiTextCentered(modGUI, 0, 0, "The mod will then take images in a spiral around your current view.") + 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, @@ -102,8 +104,12 @@ async_loop( 'If you want to start a new map, you have to delete all images from the "output" folder!' ) GuiTextCentered(modGUI, 0, 0, " ") - if GuiButton(modGUI, 0, 0, ">> Start capturing map <<", 1) then - startCapturing() + if GuiButton(modGUI, 0, 0, ">> Start capturing map around view <<", 1) then + startCapturingSpiral() + UiReduce = true + end + if GuiButton(modGUI, 0, 0, ">> Start capturing full map <<", 1) then + startCapturingHilbert() UiReduce = true end GuiTextCentered(modGUI, 0, 0, " ") @@ -111,6 +117,20 @@ async_loop( if not UiHide then local x, y = GameGetCameraPos() GuiTextCentered(modGUI, 0, 0, string.format("Coordinates: %d, %d", x, y)) + if UiProgress 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 end GuiLayoutEnd(modGUI) end diff --git a/files/util.lua b/files/util.lua index 9665950..cb53cea 100644 --- a/files/util.lua +++ b/files/util.lua @@ -97,3 +97,11 @@ function fileExists(fileName) return false end end + +function progressBarString(progress, look) + local factor = progress.Progress / progress.Max + local count = math.ceil(look.BarLength * factor) + local barString = string.rep(look.CharFull, count) .. string.rep(look.CharEmpty, look.BarLength - count) + + return string.format(look.Format, barString, progress.Progress, progress.Max, factor * 100) +end