Compare commits

..

No commits in common. "master" and "v2.7.0" have entirely different histories.

32 changed files with 274 additions and 1171 deletions

View File

@ -8,17 +8,18 @@ jobs:
build: build:
name: Build and release name: Build and release
runs-on: windows-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
goos: [windows] goos: [windows]
goarch: ["amd64"] goarch: ["amd64"]
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.22 go-version: ^1.21
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -31,7 +32,6 @@ jobs:
env: env:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
CGO_ENABLED: 1
- name: Create distribution archive - name: Create distribution archive
run: go run -v ./scripts/dist run: go run -v ./scripts/dist

View File

@ -12,7 +12,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.22 go-version: ^1.21
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2

6
.gitignore vendored
View File

@ -105,9 +105,5 @@ $RECYCLE.BIN/
/output/ /output/
/dist/ /dist/
/bin/stitch/*.png /bin/stitch/output.png
/bin/stitch/*.dzi
/bin/stitch/*_files/
/files/magic-numbers/generated.xml /files/magic-numbers/generated.xml
/bin/stitch/captures/*

11
.vscode/settings.json vendored
View File

@ -1,22 +1,18 @@
{ {
"cSpell.words": [ "cSpell.words": [
"aabb", "aabb",
"acidflow",
"appdata", "appdata",
"autosetup", "autosetup",
"backbuffer", "backbuffer",
"basicfont", "basicfont",
"bytecode", "bytecode",
"cheggaaa", "cheggaaa",
"Dadido",
"dofile", "dofile",
"dont", "dont",
"Downscales", "Downscales",
"downscaling", "downscaling",
"DPMM", "DPMM",
"executables", "executables",
"framebuffer",
"framebuffers",
"Fullscreen", "Fullscreen",
"goarch", "goarch",
"gridify", "gridify",
@ -28,7 +24,6 @@
"Lanczos", "Lanczos",
"lann", "lann",
"ldflags", "ldflags",
"libwebp",
"linearize", "linearize",
"longleg", "longleg",
"lowram", "lowram",
@ -41,30 +36,24 @@
"nfnt", "nfnt",
"Niccoli", "Niccoli",
"noita", "noita",
"noitamap",
"Nolla", "Nolla",
"NXML", "NXML",
"openseadragon",
"pixelated", "pixelated",
"polymorphed", "polymorphed",
"promptui", "promptui",
"rasterizer", "rasterizer",
"Regen", "Regen",
"respawn", "respawn",
"runfast",
"savegames", "savegames",
"schollz", "schollz",
"screenshake", "screenshake",
"svenstaro", "svenstaro",
"tcnksm", "tcnksm",
"tdewolff", "tdewolff",
"unmodded",
"unstitchable", "unstitchable",
"upscaled", "upscaled",
"Vogel", "Vogel",
"Voronoi", "Voronoi",
"webp",
"wepb",
"xmax", "xmax",
"xmin", "xmin",
"ymax", "ymax",

View File

@ -1,10 +1,6 @@
# Capture areas # Capture areas
A list of available capture areas. A list of available capture areas.
Other game-modes or mods may use a different biome setup, and therefore the coordinates shown here are not valid for them.
The values shown are for an unmodded `New Game` world.
The noita-mapcap mod will always automatically determine the required coordinates so that it correctly captures the base layout or multiples of it.
Coordinates are in in-game "virtual" or "world" pixels. Coordinates are in in-game "virtual" or "world" pixels.
`Right` and `Bottom` coordinates are not included in the rectangle. `Right` and `Bottom` coordinates are not included in the rectangle.
@ -68,16 +64,3 @@ Bottom = 41984
The end result will have a size of `51200 x 73728 pixels ~= 3775 megapixels`. The end result will have a size of `51200 x 73728 pixels ~= 3775 megapixels`.
![Base layout](images/scale32_extended.png) ![Base layout](images/scale32_extended.png)
## `3 Worlds`
This area consists of `Main world` plus a full left and right parallel world.
``` lua
Left = -53760
Top = -31744
Right = 53760
Bottom = 41984
```
The end result will have a size of `107520 x 73728 pixels ~= 7927 megapixels`.

View File

@ -5,10 +5,7 @@ It works with the regular Noita build and the dev build.
![Title image](images/title.png) ![Title image](images/title.png)
Map captures created with this mod can be viewed on [map.runfast.stream] (may contain spoilers). A resulting image with nearly 3.8 gigapixels can be [seen here](https://easyzoom.com/image/223556) (May contain spoilers).
If you are interested in creating similar captures, or if you want to contribute your own captures to [map.runfast.stream], you can take a look at [github.com/acidflow-noita/noitamap].
There you'll find detailed step-by-step instructions on how to quickly capture large parts of the Noita world with as little visual glitches and other issues as possible.
## System requirements ## System requirements
@ -58,7 +55,8 @@ To the top left of the window are 3 buttons:
- ![Output directory button](files/ui-gfx/open-output-16x16.png) Reveals the output directory in your file browser. - ![Output directory button](files/ui-gfx/open-output-16x16.png) Reveals the output directory in your file browser.
This will contain raw screenshots and other recorded data that later can be stitched. This will contain raw screenshots and other recorded data that later can be stitched.
- ![Stitch button](files/ui-gfx/stitch-16x16.png) Reveals the stitching tool directory in your file browser. - ![Stitch button](files/ui-gfx/stitch-16x16.png) Reveals the stitching tool
directory in your file browser.
To stitch the final result, click ![Stitch button](files/ui-gfx/stitch-16x16.png) to open the directory of the stitching tool. To stitch the final result, click ![Stitch button](files/ui-gfx/stitch-16x16.png) to open the directory of the stitching tool.
Start `stitch.exe` and proceed with the default values. Start `stitch.exe` and proceed with the default values.
@ -81,10 +79,6 @@ After a few minutes the file `output.png` will be created.
- `Spiral`: Will capture the world in a spiral. - `Spiral`: Will capture the world in a spiral.
The center starting point of the spiral can either be your current viewport, the world center or some custom coordinates. The center starting point of the spiral can either be your current viewport, the world center or some custom coordinates.
- `Animation`: Will capture an image sequence.
This will capture whatever you see frame by frame and stores it in the output folder by frame number.
You can't stitch the resulting images, but instead you can use something like ffmpeg to render the sequence into a video file.
### Advanced mod settings ### Advanced mod settings
- `World seed`: If non empty, this will set the next new game to this seed. - `World seed`: If non empty, this will set the next new game to this seed.
@ -152,9 +146,8 @@ The sliders are at their default values:
There is not a lot you can do about it: There is not a lot you can do about it:
- ~~You can try to increase the usable address space of your `.../Noita/noita_dev.exe` or `.../Noita/noita.exe` with [Large Address Aware] or a similar tool. - You can try to increase the usable address space of your `.../Noita/noita_dev.exe` or `.../Noita/noita.exe` with [Large Address Aware](https://www.techpowerup.com/forums/threads/large-address-aware.112556/) or a similar tool.
This will help with any crashes that are related to out of memory exceptions.~~ This will help with any crashes that are related to out of memory exceptions.
`Large Address Aware` is already set in newer Noita builds.
- You can disable the replay recorder. - You can disable the replay recorder.
@ -196,28 +189,25 @@ This will cause fast moving objects to completely disappear, and slow moving obj
To disable median blending, use the stitcher with `Blend tile limit` set to 1. To disable median blending, use the stitcher with `Blend tile limit` set to 1.
This will cause the stitcher to only use the newest image tile for every resulting pixel. This will cause the stitcher to only use the newest image tile for every resulting pixel.
## Viewing and hosting captures ### I always get the warning "The resolution changed"
The message is to be expected when you change the resolution in the Noita settings without restarting the game.
But it can also happen when you accidentally select the console window (using `noita_dev.exe`).
The mod uses the active/selected window of the Noita process for capturing, which in this case would make it take screenshots of the console.
This can be fixed by selecting the Noita window again, or by switching back and forth between console and the main Noita window.
## Additional information
The resulting stitched images are quite big. The resulting stitched images are quite big.
You can read [this comment](https://github.com/Dadido3/noita-mapcap/issues/7#issuecomment-723591552) that addresses how you can view, convert or even self-host your images. You can read [this comment](https://github.com/Dadido3/noita-mapcap/issues/7#issuecomment-723591552) that addresses how you can view, convert or even self-host your images.
You can use [github.com/Dadido3/noita-mapcap-openseadragon] if you want to host a browser based viewer on your own web space.
If you want to make your captures available to a wider audience, you should check out the [github.com/acidflow-noita/noitamap] project, which aims to make maps of all game modes (including mods) available to the public.
## Acknowledgements ## Acknowledgements
This mod uses the [LuaNXML] library by [Zatherz]. This mod uses the [LuaNXML](https://github.com/zatherz/luanxml) library by [Zatherz](https://github.com/zatherz).
Thanks to [Daniel Niccoli](https://github.com/danielniccoli) for figuring out how to change some in-game options by manipulating process memory. Thanks to [Daniel Niccoli](https://github.com/danielniccoli) for figuring out how to change some in-game options by manipulating process memory.
## License ## License
[MIT](LICENSE) [MIT](LICENSE)
[github.com/acidflow-noita/noitamap]: https://github.com/acidflow-noita/noitamap
[github.com/Dadido3/noita-mapcap-openseadragon]: https://github.com/Dadido3/noita-mapcap-openseadragon
[Large Address Aware]: https://www.techpowerup.com/forums/threads/large-address-aware.112556/
[LuaNXML]: https://github.com/zatherz/luanxml
[map.runfast.stream]: https://map.runfast.stream
[Zatherz]: https://github.com/zatherz

View File

@ -1,10 +1,8 @@
; Copyright (c) 2019-2024 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
EnableExplicit
UsePNGImageEncoder() UsePNGImageEncoder()
Declare Worker(*Dummy) Declare Worker(*Dummy)
@ -17,43 +15,44 @@ Structure QueueElement
sy.i sy.i
EndStructure EndStructure
Structure GLViewportDims ; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
x.i Procedure EnumWindowsProc(hWnd.l, *lParam.Long)
y.i Protected lpProc.l
width.i GetWindowThreadProcessId_(hWnd, @lpProc)
height.i If *lParam\l = lpProc ; Check if current window's processID matches
EndStructure *lParam\l = hWnd ; Replace processID in the param With the hwnd As result
ProcedureReturn #False ; Return false to stop iterating
Structure WorkerInfo
workerNumber.i
EndStructure
#Workers = 8
; Returns the size of the main OpenGL rendering output.
ProcedureDLL GetGLViewportSize(*dims.GLViewportDims)
If Not *dims
ProcedureReturn #False
EndIf EndIf
glGetIntegerv_(#GL_VIEWPORT, *dims)
ProcedureReturn #True ProcedureReturn #True
EndProcedure EndProcedure
; Returns the size of the main OpenGL rendering output as a windows RECT. ; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
; Returns the first window associated with the given process handle
Procedure GetProcHwnd()
Protected pID.l = GetCurrentProcessId_()
Protected tempParam.l = pID
EnumWindows_(@EnumWindowsProc(), @tempParam)
If tempParam = pID ; Check if anything was found
ProcedureReturn #Null
EndIf
ProcedureReturn tempParam ; This is a valid hWnd at this point
EndProcedure
; Get the client rectangle of the "Main" window of this process in screen coordinates
ProcedureDLL GetRect(*rect.RECT) ProcedureDLL GetRect(*rect.RECT)
Protected hWnd.l = GetProcHwnd()
If Not hWnd
ProcedureReturn #False
EndIf
If Not *rect If Not *rect
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
Protected dims.GLViewportDims GetClientRect_(hWnd, *rect)
glGetIntegerv_(#GL_VIEWPORT, dims)
*rect\left = dims\x ; A RECT consists basically of two POINT structures
*rect\top = dims\y ClientToScreen_(hWnd, @*rect\left)
*rect\right = dims\x + dims\width ClientToScreen_(hWnd, @*rect\Right)
*rect\bottom = dims\y + dims\height
ProcedureReturn #True ProcedureReturn #True
EndProcedure EndProcedure
@ -65,16 +64,13 @@ ProcedureDLL AttachProcess(Instance)
CreateDirectory("mods/noita-mapcap/output/") CreateDirectory("mods/noita-mapcap/output/")
Static Dim WorkerInfos.WorkerInfo(#Workers-1) For i = 1 To 6
Protected i CreateThread(@Worker(), #Null)
For i = 0 To #Workers-1
WorkerInfos(i)\workerNumber = i
CreateThread(@Worker(), @WorkerInfos(i))
Next Next
EndProcedure EndProcedure
Procedure Worker(*workerInfo.WorkerInfo) Procedure Worker(*Dummy)
Protected img, x, y, sx, sy Protected img, x, y
Repeat Repeat
WaitSemaphore(Semaphore) WaitSemaphore(Semaphore)
@ -93,61 +89,62 @@ Procedure Worker(*workerInfo.WorkerInfo)
ResizeImage(img, sx, sy) ResizeImage(img, sx, sy)
EndIf EndIf
; Save image temporary, and only move it once it's fully exported. SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG)
; This prevents images getting corrupted when the main process crashes. ;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test
If SaveImage(img, "mods/noita-mapcap/output/worker_" + *workerInfo\workerNumber + ".tmp", #PB_ImagePlugin_PNG)
RenameFile("mods/noita-mapcap/output/worker_" + *workerInfo\workerNumber + ".tmp", "mods/noita-mapcap/output/" + x + "," + y + ".png")
; We can't really do anything when either SaveImage or RenameFile fails, so just silently fail.
EndIf
FreeImage(img) FreeImage(img)
ForEver ForEver
EndProcedure EndProcedure
; Takes a screenshot of the client area of this process' active window. ; 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 viewport coordinates. ; 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. ; 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. ; 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) ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
Protected viewportRect.RECT Protected hWnd.l = GetProcHwnd()
If Not GetRect(@viewportRect) If Not hWnd
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
Protected imageID, hDC, *pixelBuffer Protected rect.RECT
If Not GetRect(@rect)
ProcedureReturn #False
EndIf
; Limit the desired capture area to the actual client area of the viewport. ; Limit the desired capture area to the actual client area of the window.
If *capRect\left < 0 : *capRect\left = 0 : EndIf 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\top < 0 : *capRect\top = 0 : EndIf
If *capRect\right < *capRect\left : *capRect\right = *capRect\left : EndIf If *capRect\bottom > rect\bottom-rect\top : *capRect\bottom = rect\bottom-rect\top : EndIf
If *capRect\bottom < *capRect\top : *capRect\bottom = *capRect\top : EndIf
If *capRect\right > viewportRect\right : *capRect\right = viewportRect\right : EndIf
If *capRect\bottom > viewportRect\bottom : *capRect\bottom = viewportRect\bottom : EndIf
Protected capWidth = *capRect\right - *capRect\left imageID = CreateImage(#PB_Any, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top)
Protected capHeight = *capRect\bottom - *capRect\top
imageID = CreateImage(#PB_Any, capWidth, capHeight)
If Not imageID If Not imageID
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
; Get DC of window.
windowDC = GetDC_(hWnd)
If Not windowDC
FreeImage(imageID)
ProcedureReturn #False
EndIf
hDC = StartDrawing(ImageOutput(imageID)) hDC = StartDrawing(ImageOutput(imageID))
If Not hDC If Not hDC
ReleaseDC_(hWnd, windowDC)
FreeImage(imageID) FreeImage(imageID)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
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.
*pixelBuffer = DrawingBuffer()
glReadPixels_(*capRect\left, *capRect\top, capWidth, capHeight, #GL_BGR_EXT, #GL_UNSIGNED_BYTE, *pixelBuffer)
If glGetError_() <> #GL_NO_ERROR
StopDrawing() StopDrawing()
ReleaseDC_(hWnd, windowDC)
FreeImage(imageID) FreeImage(imageID)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
StopDrawing() StopDrawing()
ReleaseDC_(hWnd, windowDC)
LockMutex(Mutex) LockMutex(Mutex)
; Check if the queue has too many elements, if so, wait. (Emulate go's channels) ; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
While ListSize(Queue()) > 1 While ListSize(Queue()) > 1
@ -176,14 +173,13 @@ EndProcedure
;Capture(123, 123) ;Capture(123, 123)
;Delay(1000) ;Delay(1000)
; IDE Options = PureBasic 6.04 LTS (Windows - x64) ; IDE Options = PureBasic 6.00 LTS (Windows - x64)
; ExecutableFormat = Shared dll ; ExecutableFormat = Shared dll
; CursorPosition = 99 ; CursorPosition = 94
; FirstLine = 72 ; FirstLine = 39
; Folding = - ; Folding = --
; Optimizer ; Optimizer
; EnableThread ; EnableThread
; EnableXP ; EnableXP
; Executable = capture.dll ; Executable = capture.dll
; DisableDebugger ; Compiler = PureBasic 6.00 LTS (Windows - x86)
; Compiler = PureBasic 6.04 LTS - C Backend (Windows - x86)

Binary file not shown.

View File

@ -36,20 +36,14 @@ example list of files:
If set to 1, only the newest tile will be used for any resulting pixel. If set to 1, only the newest tile will be used for any resulting pixel.
Use 1 to prevent ghosting and blurry objects. Use 1 to prevent ghosting and blurry objects.
- `input string` - `input string`
The source path of the image tiles to be stitched. Defaults to "./..//..//output" The source path of the image tiles to be stitched. Defaults to "./..//..//output")
- `entities string` - `entities string`
The path to the `entities.json` file. This contains Noita specific entity data. Defaults to "./../../output/entities.json". The path to the `entities.json` file. This contains Noita specific entity data. Defaults to "./../../output/entities.json".
- `player-path string` - `player-path string`
The path to the player-path.json file. This contains the tracked path of the player. Defaults to "./../../output/player-path.json". The path to the player-path.json file. This contains the tracked path of the player. Defaults to "./../../output/player-path.json".
- `output string` - `output string`
The path and filename of the resulting stitched image. Defaults to "output.png". The path and filename of the resulting stitched image. Defaults to "output.png".
Supported formats/file extensions: `.png`, `.webp`, `.jpg`, `.dzi`. Supported formats/file extensions: `.png`, `.jpg`, `.dzi`.
- `dzi-tile-size`
The size of the resulting deep zoom image (DZI) tiles in pixels. Defaults to 512.
- `dzi-tile-overlap`
The number of additional pixels around every deep zoom image (DZI) tile. Defaults to 2.
- `webp-level`
Compression level of WebP files, from 0 (fast) to 9 (slow, best compression). Defaults to 8.
- `xmax int` - `xmax int`
Right bound of the output rectangle. This coordinate is not included in the output. Right bound of the output rectangle. This coordinate is not included in the output.
- `xmin int` - `xmin int`

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022-2024 David Vogel // Copyright (c) 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
@ -8,7 +8,6 @@ package main
import ( import (
"image" "image"
"image/color" "image/color"
"image/draw"
"math" "math"
"sort" "sort"
) )
@ -107,7 +106,7 @@ func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
images = append(images, tile.GetImage()) images = append(images, tile.GetImage())
} }
// Create color variables reused every pixel. // Create arrays to be reused every pixel.
var col color.RGBA var col color.RGBA
var centerDistSqrMin int var centerDistSqrMin int
@ -148,17 +147,3 @@ func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
} }
} }
} }
// BlendMethodFast just draws all tiles into the destination image.
// No mixing is done, and this is very fast when there is no or minimal tile overlap.
type BlendMethodFast struct{}
// Draw implements the StitchedImageBlendMethod interface.
func (b BlendMethodFast) Draw(tiles []*ImageTile, destImage *image.RGBA) {
for _, tile := range tiles {
if image := tile.GetImage(); image != nil {
bounds := image.Bounds()
draw.Draw(destImage, bounds, image, bounds.Min, draw.Src)
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023-2024 David Vogel // Copyright (c) 2023 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
@ -12,13 +12,14 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
)
"github.com/cheggaaa/pb/v3" const (
dziTileSize = 512 // The (maximum) width and height of a tile in pixels, not including the overlap.
dziOverlap = 1 // The amount of additional pixels on every side of every tile. The real (max) width/height of an image is `2*overlap + tileSize`.
) )
type DZI struct { type DZI struct {
@ -32,14 +33,11 @@ type DZI struct {
maxZoomLevel int // The maximum zoom level that is needed. maxZoomLevel int // The maximum zoom level that is needed.
} }
// NewDZI creates a new DZI from the given StitchedImages. func NewDZI(stitchedImage *StitchedImage) DZI {
//
// dziTileSize and dziOverlap define the size and overlap of the resulting DZI tiles.
func NewDZI(stitchedImage *StitchedImage, dziTileSize, dziOverlap int) DZI {
dzi := DZI{ dzi := DZI{
stitchedImage: stitchedImage, stitchedImage: stitchedImage,
fileExtension: ".webp", fileExtension: ".png",
overlap: dziOverlap, overlap: dziOverlap,
tileSize: dziTileSize, tileSize: dziTileSize,
@ -82,70 +80,24 @@ func (d DZI) ExportDZIDescriptor(outputPath string) error {
Width string Width string
Height string Height string
} }
TopLeft struct {
X string
Y string
}
} }
} }
dziDescriptor.Image.XMLNS = "http://schemas.microsoft.com/deepzoom/2008" dziDescriptor.Image.XMLNS = "http://schemas.microsoft.com/deepzoom/2008"
dziDescriptor.Image.Format = "webp" dziDescriptor.Image.Format = "png"
dziDescriptor.Image.Overlap = strconv.Itoa(d.overlap) dziDescriptor.Image.Overlap = strconv.Itoa(d.overlap)
dziDescriptor.Image.TileSize = strconv.Itoa(d.tileSize) dziDescriptor.Image.TileSize = strconv.Itoa(d.tileSize)
dziDescriptor.Image.Size.Width = strconv.Itoa(d.stitchedImage.bounds.Dx()) dziDescriptor.Image.Size.Width = strconv.Itoa(d.stitchedImage.bounds.Dx())
dziDescriptor.Image.Size.Height = strconv.Itoa(d.stitchedImage.bounds.Dy()) dziDescriptor.Image.Size.Height = strconv.Itoa(d.stitchedImage.bounds.Dy())
dziDescriptor.Image.TopLeft.X = strconv.Itoa(d.stitchedImage.bounds.Min.X)
dziDescriptor.Image.TopLeft.Y = strconv.Itoa(d.stitchedImage.bounds.Min.Y)
jsonEnc := json.NewEncoder(f) jsonEnc := json.NewEncoder(f)
return jsonEnc.Encode(dziDescriptor) return jsonEnc.Encode(dziDescriptor)
} }
// ExportDZITiles exports the single image tiles for every zoom level. // ExportDZITiles exports the single image tiles for every zoom level.
func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int) error { func (d DZI) ExportDZITiles(outputDir string) error {
log.Printf("Creating DZI tiles in %q.", outputDir) log.Printf("Creating DZI tiles in %q.", outputDir)
const scaleDivider = 2
var exportedTiles atomic.Int64
// If there is a progress bar, start a goroutine that regularly updates it.
// We will base that on the number of exported tiles.
if bar != nil {
// Count final number of tiles.
bounds := d.stitchedImage.bounds
var finalTiles int64
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
for iY := 0; iY <= (bounds.Dy()-1)/d.tileSize; iY++ {
for iX := 0; iX <= (bounds.Dx()-1)/d.tileSize; iX++ {
finalTiles++
}
}
bounds = image.Rect(DivideFloor(bounds.Min.X, scaleDivider), DivideFloor(bounds.Min.Y, scaleDivider), DivideCeil(bounds.Max.X, scaleDivider), DivideCeil(bounds.Max.Y, scaleDivider))
}
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(finalTiles).Start()
done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
bar.SetCurrent(exportedTiles.Load())
}
}
}()
}
// Start with the highest zoom level (Where every world pixel is exactly mapped into one image pixel). // Start with the highest zoom level (Where every world pixel is exactly mapped into one image pixel).
// Generate all tiles for this level, and then stitch another image (scaled down by a factor of 2) based on the previously generated tiles. // Generate all tiles for this level, and then stitch another image (scaled down by a factor of 2) based on the previously generated tiles.
// Repeat this process until we have generated level 0. // Repeat this process until we have generated level 0.
@ -156,7 +108,7 @@ func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- { for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
levelBasePath := filepath.Join(outputDir, fmt.Sprintf("%d", zoomLevel)) levelBasePath := filepath.Join(outputDir, fmt.Sprintf("%d", zoomLevel))
if err := os.MkdirAll(levelBasePath, 0755); err != nil { if err := os.Mkdir(levelBasePath, 0755); err != nil {
return fmt.Errorf("failed to create zoom level base directory %q: %w", levelBasePath, err) return fmt.Errorf("failed to create zoom level base directory %q: %w", levelBasePath, err)
} }
@ -164,7 +116,6 @@ func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int
imageTiles := ImageTiles{} imageTiles := ImageTiles{}
// Export tiles. // Export tiles.
lg := NewLimitGroup(runtime.NumCPU())
for iY := 0; iY <= (stitchedImage.bounds.Dy()-1)/d.tileSize; iY++ { for iY := 0; iY <= (stitchedImage.bounds.Dy()-1)/d.tileSize; iY++ {
for iX := 0; iX <= (stitchedImage.bounds.Dx()-1)/d.tileSize; iX++ { for iX := 0; iX <= (stitchedImage.bounds.Dx()-1)/d.tileSize; iX++ {
rect := image.Rect(iX*d.tileSize, iY*d.tileSize, iX*d.tileSize+d.tileSize, iY*d.tileSize+d.tileSize) rect := image.Rect(iX*d.tileSize, iY*d.tileSize, iX*d.tileSize+d.tileSize, iY*d.tileSize+d.tileSize)
@ -172,16 +123,11 @@ func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int
rect = rect.Inset(-d.overlap) rect = rect.Inset(-d.overlap)
img := stitchedImage.SubStitchedImage(rect) img := stitchedImage.SubStitchedImage(rect)
filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension)) filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension))
if err := exportPNGSilent(img, filePath); err != nil {
return fmt.Errorf("failed to export PNG: %w", err)
}
lg.Add(1) scaleDivider := 2
go func() {
defer lg.Done()
if err := exportWebP(img, filePath, webPLevel); err != nil {
log.Printf("Failed to export WebP: %v", err)
}
exportedTiles.Add(1)
}()
imageTiles = append(imageTiles, ImageTile{ imageTiles = append(imageTiles, ImageTile{
fileName: filePath, fileName: filePath,
modTime: time.Now(), modTime: time.Now(),
@ -193,12 +139,11 @@ func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int
}) })
} }
} }
lg.Wait()
// Create new stitched image from the previously exported tiles. // Create new stitched image from the previously exported tiles.
// The tiles are already created in a way, that they are scaled down by a factor of 2. // The tiles are already created in a way, that they are scaled down by a factor of 2.
var err error var err error
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodFast{}, 128, nil) stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodMedian{BlendTileLimit: 0}, 128, nil)
if err != nil { if err != nil {
return fmt.Errorf("failed to run NewStitchedImage(): %w", err) return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023-2024 David Vogel // Copyright (c) 2023 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
@ -10,16 +10,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/cheggaaa/pb/v3"
) )
func exportDZIStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar, dziTileSize, dziOverlap int, webPLevel int) error { func exportDZI(stitchedImage *StitchedImage, outputPath string) error {
descriptorPath := outputPath descriptorPath := outputPath
extension := filepath.Ext(outputPath) extension := filepath.Ext(outputPath)
outputTilesPath := strings.TrimSuffix(outputPath, extension) + "_files" outputTilesPath := strings.TrimSuffix(outputPath, extension) + "_files"
dzi := NewDZI(stitchedImage, dziTileSize, dziOverlap) dzi := NewDZI(stitchedImage)
// Create base directory of all DZI files. // Create base directory of all DZI files.
if err := os.MkdirAll(outputTilesPath, 0755); err != nil { if err := os.MkdirAll(outputTilesPath, 0755); err != nil {
@ -32,7 +30,7 @@ func exportDZIStitchedImage(stitchedImage *StitchedImage, outputPath string, bar
} }
// Export DZI tiles. // Export DZI tiles.
if err := dzi.ExportDZITiles(outputTilesPath, bar, webPLevel); err != nil { if err := dzi.ExportDZITiles(outputTilesPath); err != nil {
return fmt.Errorf("failed to export DZI tiles: %w", err) return fmt.Errorf("failed to export DZI tiles: %w", err)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023-2024 David Vogel // Copyright (c) 2023 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,44 +11,15 @@ import (
"image/jpeg" "image/jpeg"
"log" "log"
"os" "os"
"time"
"github.com/cheggaaa/pb/v3"
) )
func exportJPEGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error { func exportJPEG(stitchedImage image.Image, outputPath string) error {
log.Printf("Creating output file %q.", outputPath) log.Printf("Creating output file %q.", outputPath)
// If there is a progress bar, start a goroutine that regularly updates it. return exportJPEGSilent(stitchedImage, outputPath)
// We will base the progress on the number of pixels read from the stitched image.
if bar != nil {
_, max := stitchedImage.Progress()
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(int64(max)).Start()
done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
value, max := stitchedImage.Progress()
bar.SetCurrent(int64(value)).SetTotal(int64(max))
}
}
}()
}
return exportJPEG(stitchedImage, outputPath)
} }
func exportJPEG(img image.Image, outputPath string) error { func exportJPEGSilent(stitchedImage image.Image, outputPath string) error {
f, err := os.Create(outputPath) f, err := os.Create(outputPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create file: %w", err) return fmt.Errorf("failed to create file: %w", err)
@ -59,7 +30,7 @@ func exportJPEG(img image.Image, outputPath string) error {
Quality: 80, Quality: 80,
} }
if err := jpeg.Encode(f, img, options); err != nil { if err := jpeg.Encode(f, stitchedImage, options); err != nil {
return fmt.Errorf("failed to encode image %q: %w", outputPath, err) return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023-2024 David Vogel // Copyright (c) 2023 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,44 +11,15 @@ import (
"image/png" "image/png"
"log" "log"
"os" "os"
"time"
"github.com/cheggaaa/pb/v3"
) )
func exportPNGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error { func exportPNG(stitchedImage image.Image, outputPath string) error {
log.Printf("Creating output file %q.", outputPath) log.Printf("Creating output file %q.", outputPath)
// If there is a progress bar, start a goroutine that regularly updates it. return exportPNGSilent(stitchedImage, outputPath)
// We will base the progress on the number of pixels read from the stitched image.
if bar != nil {
_, max := stitchedImage.Progress()
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(int64(max)).Start()
done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
value, max := stitchedImage.Progress()
bar.SetCurrent(int64(value)).SetTotal(int64(max))
}
}
}()
}
return exportPNG(stitchedImage, outputPath)
} }
func exportPNG(img image.Image, outputPath string) error { func exportPNGSilent(stitchedImage image.Image, outputPath string) error {
f, err := os.Create(outputPath) f, err := os.Create(outputPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create file: %w", err) return fmt.Errorf("failed to create file: %w", err)
@ -59,7 +30,7 @@ func exportPNG(img image.Image, outputPath string) error {
CompressionLevel: png.DefaultCompression, CompressionLevel: png.DefaultCompression,
} }
if err := encoder.Encode(f, img); err != nil { if err := encoder.Encode(f, stitchedImage); err != nil {
return fmt.Errorf("failed to encode image %q: %w", outputPath, err) return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
} }

View File

@ -1,73 +0,0 @@
// Copyright (c) 2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package main
import (
"fmt"
"image"
"log"
"os"
"time"
"github.com/Dadido3/go-libwebp/webp"
"github.com/cheggaaa/pb/v3"
)
func exportWebPStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar, webPLevel int) error {
log.Printf("Creating output file %q.", outputPath)
// If there is a progress bar, start a goroutine that regularly updates it.
// We will base the progress on the number of pixels read from the stitched image.
if bar != nil {
_, max := stitchedImage.Progress()
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(int64(max)).Start()
done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
value, max := stitchedImage.Progress()
bar.SetCurrent(int64(value)).SetTotal(int64(max))
}
}
}()
}
return exportWebP(stitchedImage, outputPath, webPLevel)
}
func exportWebP(img image.Image, outputPath string, webPLevel int) error {
bounds := img.Bounds()
if bounds.Dx() > 16383 || bounds.Dy() > 16383 {
return fmt.Errorf("image size exceeds the maximum allowed size (16383) of a WebP image: %d x %d", bounds.Dx(), bounds.Dy())
}
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
webPConfig, err := webp.ConfigLosslessPreset(webPLevel)
if err != nil {
return fmt.Errorf("failed to create webP config: %v", err)
}
if err = webp.Encode(f, img, webPConfig); err != nil {
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
}
return nil
}

View File

@ -8,7 +8,6 @@ package main
import ( import (
"fmt" "fmt"
"image" "image"
"image/draw"
_ "image/png" _ "image/png"
"log" "log"
"os" "os"
@ -128,16 +127,9 @@ func (it *ImageTile) GetImage() *image.RGBA {
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor) img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
} }
var imgRGBA *image.RGBA imgRGBA, ok := img.(*image.RGBA)
switch img := img.(type) { if !ok {
case *image.RGBA: log.Printf("Expected an RGBA image for %q, got %T instead.", it.fileName, img)
imgRGBA = img
case *image.NRGBA:
bounds := img.Bounds()
imgRGBA = image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
draw.Draw(imgRGBA, imgRGBA.Bounds(), img, bounds.Min, draw.Src)
default:
log.Printf("Expected an RGBA or NRGBA image for %q, got %T instead.", it.fileName, img)
return nil return nil
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019-2024 David Vogel // Copyright (c) 2019-2023 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
@ -12,6 +12,7 @@ import (
"log" "log"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
"github.com/1lann/promptui" "github.com/1lann/promptui"
@ -21,12 +22,9 @@ import (
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.") var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
var flagEntitiesInputPath = flag.String("entities", filepath.Join(".", "..", "..", "output", "entities.json"), "The path to the entities.json file.") var flagEntitiesInputPath = flag.String("entities", filepath.Join(".", "..", "..", "output", "entities.json"), "The path to the entities.json file.")
var flagPlayerPathInputPath = flag.String("player-path", filepath.Join(".", "..", "..", "output", "player-path.json"), "The path to the player-path.json file.") var flagPlayerPathInputPath = flag.String("player-path", filepath.Join(".", "..", "..", "output", "player-path.json"), "The path to the player-path.json file.")
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image. Supported formats/file extensions: `.png`, `.webp`, `.jpg`, `.dzi`.") var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image. Supported formats/file extensions: `.png`, `.jpg`, `.dzi`.")
var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.") var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
var flagBlendTileLimit = flag.Int("blend-tile-limit", 9, "Limits median blending to the n newest tiles by file modification time. If set to 0, all available tiles will be median blended.") var flagBlendTileLimit = flag.Int("blend-tile-limit", 9, "Limits median blending to the n newest tiles by file modification time. If set to 0, all available tiles will be median blended.")
var flagDZITileSize = flag.Int("dzi-tile-size", 512, "The size of the resulting deep zoom image (DZI) tiles in pixels.")
var flagDZIOverlap = flag.Int("dzi-tile-overlap", 2, "The number of additional pixels around every deep zoom image (DZI) tile.")
var flagWebPLevel = flag.Int("webp-level", 8, "Compression level of WebP files, from 0 (fast) to 9 (slow, best compression).")
var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.") var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.")
var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This coordinate is included in the output.") var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This coordinate is included in the output.")
var flagXMax = flag.Int("xmax", 0, "Right bound of the output rectangle. This coordinate is not included in the output.") var flagXMax = flag.Int("xmax", 0, "Right bound of the output rectangle. This coordinate is not included in the output.")
@ -231,91 +229,11 @@ func main() {
*flagOutputPath = result *flagOutputPath = result
} }
fileExtension := strings.ToLower(filepath.Ext(*flagOutputPath)) startTime := time.Now()
// Query the user, if there were no cmd arguments given. bar := pb.Full.New(0)
if flag.NFlag() == 0 && fileExtension == ".dzi" { var wg sync.WaitGroup
prompt := promptui.Prompt{ done := make(chan struct{})
Label: "Enter DZI tile size:",
Default: fmt.Sprint(*flagDZITileSize),
AllowEdit: true,
Validate: func(s string) error {
var num int
_, err := fmt.Sscanf(s, "%d", &num)
if err != nil {
return err
}
if int(num) < 1 {
return fmt.Errorf("number must be at least 1")
}
return nil
},
}
result, err := prompt.Run()
if err != nil {
log.Panicf("Error while getting user input: %v.", err)
}
fmt.Sscanf(result, "%d", flagDZITileSize)
}
// Query the user, if there were no cmd arguments given.
if flag.NFlag() == 0 && fileExtension == ".dzi" {
prompt := promptui.Prompt{
Label: "Enter DZI tile overlap:",
Default: fmt.Sprint(*flagDZIOverlap),
AllowEdit: true,
Validate: func(s string) error {
var num int
_, err := fmt.Sscanf(s, "%d", &num)
if err != nil {
return err
}
if int(num) < 0 {
return fmt.Errorf("number must be at least 0")
}
return nil
},
}
result, err := prompt.Run()
if err != nil {
log.Panicf("Error while getting user input: %v.", err)
}
fmt.Sscanf(result, "%d", flagDZIOverlap)
}
// Query the user, if there were no cmd arguments given.
if flag.NFlag() == 0 && (fileExtension == ".dzi" || fileExtension == ".webp") {
prompt := promptui.Prompt{
Label: "Enter WebP compression level:",
Default: fmt.Sprint(*flagWebPLevel),
AllowEdit: true,
Validate: func(s string) error {
var num int
_, err := fmt.Sscanf(s, "%d", &num)
if err != nil {
return err
}
if int(num) < 0 {
return fmt.Errorf("level must be at least 0")
}
if int(num) > 9 {
return fmt.Errorf("level must not be larger than 9")
}
return nil
},
}
result, err := prompt.Run()
if err != nil {
log.Panicf("Error while getting user input: %v.", err)
}
fmt.Sscanf(result, "%d", flagWebPLevel)
}
blendMethod := BlendMethodMedian{ blendMethod := BlendMethodMedian{
BlendTileLimit: *flagBlendTileLimit, // Limit median blending to the n newest tiles by file modification time. BlendTileLimit: *flagBlendTileLimit, // Limit median blending to the n newest tiles by file modification time.
@ -325,31 +243,50 @@ func main() {
if err != nil { if err != nil {
log.Panicf("NewStitchedImage() failed: %v.", err) log.Panicf("NewStitchedImage() failed: %v.", err)
} }
_, max := stitchedImage.Progress()
bar.SetTotal(int64(max)).Start().SetRefreshRate(250 * time.Millisecond)
bar := pb.Full.New(0) // Query progress and draw progress bar.
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
value, _ := stitchedImage.Progress()
bar.SetCurrent(int64(value))
bar.Finish()
return
case <-ticker.C:
value, _ := stitchedImage.Progress()
bar.SetCurrent(int64(value))
}
}
}()
fileExtension := strings.ToLower(filepath.Ext(*flagOutputPath))
switch fileExtension { switch fileExtension {
case ".png": case ".png":
if err := exportPNGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil { if err := exportPNG(stitchedImage, *flagOutputPath); err != nil {
log.Panicf("Export of PNG file failed: %v", err) log.Panicf("Export of PNG file failed: %v", err)
} }
case ".jpg", ".jpeg": case ".jpg", ".jpeg":
if err := exportJPEGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil { if err := exportJPEG(stitchedImage, *flagOutputPath); err != nil {
log.Panicf("Export of JPEG file failed: %v", err) log.Panicf("Export of JPEG file failed: %v", err)
} }
case ".webp":
if err := exportWebPStitchedImage(stitchedImage, *flagOutputPath, bar, *flagWebPLevel); err != nil {
log.Panicf("Export of WebP file failed: %v", err)
}
case ".dzi": case ".dzi":
if err := exportDZIStitchedImage(stitchedImage, *flagOutputPath, bar, *flagDZITileSize, *flagDZIOverlap, *flagWebPLevel); err != nil { if err := exportDZI(stitchedImage, *flagOutputPath); err != nil {
log.Panicf("Export of DZI file failed: %v", err) log.Panicf("Export of DZI file failed: %v", err)
} }
default: default:
log.Panicf("Unknown output format %q.", fileExtension) log.Panicf("Unknown output format %q.", fileExtension)
} }
log.Printf("Created output in %v.", time.Since(bar.StartTime())) done <- struct{}{}
wg.Wait()
log.Printf("Created output in %v.", time.Since(startTime))
//fmt.Println("Press the enter key to terminate the console screen!") //fmt.Println("Press the enter key to terminate the console screen!")
//fmt.Scanln() //fmt.Scanln()

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022-2024 David Vogel // Copyright (c) 2022-2023 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
@ -8,7 +8,6 @@ package main
import ( import (
"image" "image"
"image/color" "image/color"
"image/draw"
"runtime" "runtime"
"sync" "sync"
) )
@ -70,9 +69,7 @@ func (sic *StitchedImageCache) Regenerate() *image.RGBA {
si := sic.stitchedImage si := sic.stitchedImage
// Create new image with default background color.
cacheImage := image.NewRGBA(sic.rect) cacheImage := image.NewRGBA(sic.rect)
draw.Draw(cacheImage, cacheImage.Bounds(), &image.Uniform{colorBackground}, cacheImage.Bounds().Min, draw.Src)
// List of tiles that intersect with the to be generated cache image. // List of tiles that intersect with the to be generated cache image.
intersectingTiles := []*ImageTile{} intersectingTiles := []*ImageTile{}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022-2024 David Vogel // Copyright (c) 2022-2023 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
@ -13,10 +13,6 @@ import (
"time" "time"
) )
// The default background color.
// We use a non transparent black.
var colorBackground = color.RGBA{0, 0, 0, 255}
// StitchedImageCacheGridSize defines the worker chunk size when the cache image is regenerated. // StitchedImageCacheGridSize defines the worker chunk size when the cache image is regenerated.
var StitchedImageCacheGridSize = 256 var StitchedImageCacheGridSize = 256
@ -120,7 +116,7 @@ func (si *StitchedImage) RGBAAt(x, y int) color.RGBA {
// Determine the cache rowIndex index. // Determine the cache rowIndex index.
rowIndex := (y + si.cacheRowYOffset) / si.cacheRowHeight rowIndex := (y + si.cacheRowYOffset) / si.cacheRowHeight
if rowIndex < 0 || rowIndex >= len(si.cacheRows) { if rowIndex < 0 || rowIndex >= len(si.cacheRows) {
return colorBackground return color.RGBA{}
} }
// Check if we advanced/changed the row index. // Check if we advanced/changed the row index.

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023-2024 David Vogel // Copyright (c) 2023 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
@ -29,7 +29,7 @@ func (s SubStitchedImage) At(x, y int) color.Color {
func (s SubStitchedImage) RGBAAt(x, y int) color.RGBA { func (s SubStitchedImage) RGBAAt(x, y int) color.RGBA {
point := image.Point{X: x, Y: y} point := image.Point{X: x, Y: y}
if !point.In(s.bounds) { if !point.In(s.bounds) {
return colorBackground return color.RGBA{}
} }
return s.StitchedImage.RGBAAt(x, y) return s.StitchedImage.RGBAAt(x, y)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019-2024 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
@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"image" "image"
"os" "os"
"sync"
) )
// QuickSelect returns the kth smallest element of the given unsorted list. // QuickSelect returns the kth smallest element of the given unsorted list.
@ -97,44 +96,3 @@ func DivideCeil(a, b int) int {
return temp return temp
} }
// https://gist.github.com/cstockton/d611ced26bb6b4d3f7d4237abb8613c4
type LimitGroup struct {
wg sync.WaitGroup
mu *sync.Mutex
c *sync.Cond
l, n int
}
func NewLimitGroup(n int) *LimitGroup {
mu := new(sync.Mutex)
return &LimitGroup{
mu: mu,
c: sync.NewCond(mu),
l: n,
n: n,
}
}
func (lg *LimitGroup) Add(delta int) {
lg.mu.Lock()
defer lg.mu.Unlock()
if delta > lg.l {
panic(`LimitGroup: delta must not exceed limit`)
}
for lg.n < 1 {
lg.c.Wait()
}
lg.n -= delta
lg.wg.Add(delta)
}
func (lg *LimitGroup) Done() {
lg.mu.Lock()
defer lg.mu.Unlock()
lg.n++
lg.c.Signal()
lg.wg.Done()
}
func (lg *LimitGroup) Wait() { lg.wg.Wait() }

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2024 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
@ -32,7 +32,7 @@ Capture.PlayerPathCapturingCtx = Capture.PlayerPathCapturingCtx or ProcessRunner
---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle. ---Returns a capturing rectangle in window coordinates, and also the world coordinates for the same rectangle.
---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates. ---The rectangle is sized and placed in a way that aligns as pixel perfect as possible with the world coordinates.
---@param pos Vec2? -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically. ---@param pos Vec2|nil -- Position of the viewport center in world coordinates. If set to nil, the viewport center will be queried automatically.
---@return Vec2 topLeftCapture ---@return Vec2 topLeftCapture
---@return Vec2 bottomRightCapture ---@return Vec2 bottomRightCapture
---@return Vec2 topLeftWorld ---@return Vec2 topLeftWorld
@ -53,20 +53,19 @@ end
---This will block until all chunks in the virtual rectangle are loaded. ---This will block until all chunks in the virtual rectangle are loaded.
--- ---
---Don't set `ensureLoaded` to true when `pos` is nil! ---Don't set `ensureLoaded` to true when `pos` is nil!
---@param pos Vec2? -- Position of the viewport center in world coordinates. If set to nil, the viewport will not be modified. ---@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? -- If true, the function will wait until all chunks in the virtual rectangle are loaded. ---@param ensureLoaded boolean|nil -- If true, the function will wait until all chunks in the virtual rectangle are loaded.
---@param dontOverwrite boolean? -- If true, the function will abort if there is already a file with the same coordinates. ---@param dontOverwrite boolean|nil -- If true, the function will abort if there is already a file with the same coordinates.
---@param ctx ProcessRunnerCtx? -- The process runner context this runs in. ---@param ctx ProcessRunnerCtx|nil -- The process runner context this runs in.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
---@param captureDelay number? -- The number of additional frames to wait before a screen capture. local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPixelScale)
local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPixelScale, captureDelay)
if outputPixelScale == 0 or outputPixelScale == nil then if outputPixelScale == 0 or outputPixelScale == nil then
outputPixelScale = Coords:PixelScale() outputPixelScale = Coords:PixelScale()
end end
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect() local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then if Coords.WindowResolution ~= rectBottomRight - rectTopLeft then
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft)) error(string.format("window size seems to have changed from %s to %s", Coords.WindowResolution, rectBottomRight - rectTopLeft))
end end
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos) local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
@ -80,23 +79,10 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
return return
end end
-- Reset the count for the "Waiting for x frames." message in the UI.
if ctx then ctx.state.WaitFrames = 0 end
-- Wait some additional frames.
-- We will shake the screen a little bit so that Noita generates/populates chunks.
if captureDelay and captureDelay > 0 then
for _ = 1, captureDelay do
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-1, 1), math.random(-1, 1))) end
wait(0)
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end
end
end
if pos then CameraAPI.SetPos(pos) end if pos then CameraAPI.SetPos(pos) end
if ensureLoaded then if ensureLoaded then
local delayFrames = 0 local delayFrames = 0
if ctx then ctx.state.WaitFrames = delayFrames end
repeat repeat
-- Prematurely stop capturing if that is requested by the context. -- Prematurely stop capturing if that is requested by the context.
if ctx and ctx:IsStopping() then return end if ctx and ctx:IsStopping() then return end
@ -106,43 +92,25 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-10, 10), math.random(-10, 10))) end if pos then CameraAPI.SetPos(pos + Vec2(math.random(-10, 10), math.random(-10, 10))) end
wait(0) wait(0)
delayFrames = delayFrames + 1 delayFrames = delayFrames + 1
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end if ctx then ctx.state.WaitFrames = delayFrames end
if pos then CameraAPI.SetPos(pos) end if pos then CameraAPI.SetPos(pos) end
end end
if delayFrames > 600 then
-- Shaking wasn't enough, we will just move somewhere else an try again.
if pos then CameraAPI.SetPos(pos + Vec2(math.random(-4000, 4000), math.random(-4000, 4000))) end
wait(50)
delayFrames = delayFrames + 50
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 50 end
if pos then CameraAPI.SetPos(pos) end
wait(10)
delayFrames = delayFrames + 10
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 10 end
end
wait(0) wait(0)
delayFrames = delayFrames + 1 delayFrames = delayFrames + 1
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end if ctx then ctx.state.WaitFrames = delayFrames end
local topLeftBounds, bottomRightBounds = CameraAPI:Bounds() local topLeftBounds, bottomRightBounds = CameraAPI:Bounds()
until DoesWorldExistAt(topLeftBounds.x, topLeftBounds.y, bottomRightBounds.x, bottomRightBounds.y) until DoesWorldExistAt(topLeftBounds.x, topLeftBounds.y, bottomRightBounds.x, bottomRightBounds.y)
-- Chunks are loaded and will be drawn on the *next* frame. -- Chunks are loaded and will be drawn on the *next* frame.
end end
if ctx then ctx.state.WaitFrames = 0 end
-- Suspend UI drawing for 1 frame. -- Suspend UI drawing for 1 frame.
UI:SuspendDrawing(1) UI:SuspendDrawing(1)
-- First we wait one frame for the current state to be drawn.
wait(0) wait(0)
-- At this point the needed frame is fully drawn, but the framebuffers are swapped. -- Fetch coordinates again, as they may have changed.
-- Recalculate capture position and rectangle if we are not forcing any capture position.
-- We are in the `OnWorldPreUpdate` hook, this means that `CameraAPI.GetPos` return the position of the last frame.
if not pos then if not pos then
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos) topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
if outputPixelScale > 0 then if outputPixelScale > 0 then
@ -152,10 +120,6 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end end
end end
-- Wait another frame.
-- After this `wait` the framebuffer will be swapped again, and we can grab the correct frame.
wait(0)
-- 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.
-- Otherwise it's not possible to stitch the images correctly. -- Otherwise it's not possible to stitch the images correctly.
if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then
@ -166,37 +130,6 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
MonitorStandby.ResetTimer() MonitorStandby.ResetTimer()
end end
---Captures a screenshot of the current viewport.
---This is used to capture animations, therefore the resulting image may not be suitable for stitching.
---@param outputPixelScale number? The resulting image pixel to world pixel ratio.
---@param frameNumber integer The frame number of the animation.
local function captureScreenshotAnimation(outputPixelScale, frameNumber)
if outputPixelScale == 0 or outputPixelScale == nil then
outputPixelScale = Coords:PixelScale()
end
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
if not rectTopLeft or not rectBottomRight then
error(string.format("couldn't determine capturing rectangle"))
end
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
end
local topLeftWorld, bottomRightWorld = Coords:ToWorld(rectTopLeft), Coords:ToWorld(rectBottomRight)
---We will use this to get our fame number into the filename.
---@type Vec2
local outputTopLeft = Vec2(frameNumber, 0)
if not ScreenCapture.Capture(rectTopLeft, rectBottomRight, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then
error(string.format("failed to capture screenshot"))
end
-- Reset monitor and PC standby every screenshot.
MonitorStandby.ResetTimer()
end
---Map capture process runner context error handler callback. Just rolls off the tongue. ---Map capture process runner context error handler callback. Just rolls off the tongue.
---@param err string ---@param err string
---@param scope "init"|"do"|"end" ---@param scope "init"|"do"|"end"
@ -209,9 +142,8 @@ end
---Use `Capture.MapCapturingCtx` to stop, control or view the progress. ---Use `Capture.MapCapturingCtx` to stop, control or view the progress.
---@param origin Vec2 -- Center of the spiral in world pixels. ---@param origin Vec2 -- Center of the spiral in world pixels.
---@param captureGridSize number -- The grid size in world pixels. ---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
---@param captureDelay number? -- The number of additional frames to wait before a screen capture. function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale)
function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory. -- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a") local file = io.open("mods/noita-mapcap/output/nonempty", "a")
@ -235,23 +167,23 @@ function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale,
repeat repeat
-- +x -- +x
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay) captureScreenshot(pos, true, true, ctx, outputPixelScale)
pos:Add(Vec2(captureGridSize, 0)) pos:Add(Vec2(captureGridSize, 0))
end end
-- +y -- +y
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay) captureScreenshot(pos, true, true, ctx, outputPixelScale)
pos:Add(Vec2(0, captureGridSize)) pos:Add(Vec2(0, captureGridSize))
end end
i = i + 1 i = i + 1
-- -x -- -x
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay) captureScreenshot(pos, true, true, ctx, outputPixelScale)
pos:Add(Vec2(-captureGridSize, 0)) pos:Add(Vec2(-captureGridSize, 0))
end end
-- -y -- -y
for _ = 1, i, 1 do for _ = 1, i, 1 do
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay) captureScreenshot(pos, true, true, ctx, outputPixelScale)
pos:Add(Vec2(0, -captureGridSize)) pos:Add(Vec2(0, -captureGridSize))
end end
i = i + 1 i = i + 1
@ -268,32 +200,27 @@ function Capture:StartCapturingSpiral(origin, captureGridSize, outputPixelScale,
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler) self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end end
---Starts the capturing process of the given area using a hilbert curve. ---Starts the capturing process of the given area.
---Use `Capture.MapCapturingCtx` to stop, control or view the process. ---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param topLeft Vec2 -- Top left of the to be captured rectangle. ---@param topLeft Vec2 -- Top left of the to be captured rectangle.
---@param bottomRight Vec2 -- Non inclusive bottom right coordinate of the to be captured rectangle. ---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle.
---@param captureGridSize number -- The grid size in world pixels. ---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
---@param captureDelay number? -- The number of additional frames to wait before a screen capture. function Capture:StartCapturingArea(topLeft, bottomRight, captureGridSize, outputPixelScale)
function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory. -- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a") local file = io.open("mods/noita-mapcap/output/nonempty", "a")
if file ~= nil then file:close() end if file ~= nil then file:close() end
-- The capture offset which is needed to center the grid cells in the viewport. ---The rectangle in grid coordinates.
local captureOffset = Vec2(captureGridSize / 2, captureGridSize / 2)
-- Get the extended capture rectangle that encloses all grid cells that need to be included in the capture.
-- In this case we only need to extend the capture area by the valid rendering rectangle.
local validTopLeft, validBottomRight = Coords:ValidRenderingRect()
local validTopLeftWorld, validBottomRightWorld = Coords:ToWorld(validTopLeft, topLeft + captureOffset), Coords:ToWorld(validBottomRight, bottomRight + captureOffset)
---The capture rectangle in grid coordinates.
---@type Vec2, Vec2 ---@type Vec2, Vec2
local gridTopLeft, gridBottomRight = (validTopLeftWorld / captureGridSize):Rounded("floor"), ((validBottomRightWorld) / captureGridSize):Rounded("ceil") - Vec2(1, 1) local gridTopLeft, gridBottomRight = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
---Size of the rectangle in grid cells. -- Handle edge cases.
if topLeft.x == bottomRight.x then gridBottomRight.x = gridTopLeft.x end
if topLeft.y == bottomRight.y then gridBottomRight.y = gridTopLeft.y end
---Size of the rectangle in grid coordinates.
---@type Vec2 ---@type Vec2
local gridSize = gridBottomRight - gridTopLeft local gridSize = gridBottomRight - gridTopLeft
@ -322,8 +249,8 @@ function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize
---Position in world coordinates. ---Position in world coordinates.
---@type Vec2 ---@type Vec2
local pos = (hilbertPos + gridTopLeft) * captureGridSize local pos = (hilbertPos + gridTopLeft) * captureGridSize
pos:Add(captureOffset) -- Move to center of grid cell. pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay) captureScreenshot(pos, true, true, ctx, outputPixelScale)
ctx.state.Current = ctx.state.Current + 1 ctx.state.Current = ctx.state.Current + 1
end end
@ -341,73 +268,9 @@ function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler) self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end end
---Starts the capturing process of the given area by scanning from left to right, and top to bottom.
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param topLeft Vec2 -- Top left of the to be captured rectangle.
---@param bottomRight Vec2 -- Non inclusive bottom right coordinate of the to be captured rectangle.
---@param captureGridSize number -- The grid size in world pixels.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
---@param captureDelay number? -- The number of additional frames to wait before a screen capture.
function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
-- Create file that signals that there are files in the output directory.
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
if file ~= nil then file:close() end
-- The capture offset which is needed to center the grid cells in the viewport.
local captureOffset = Vec2(captureGridSize / 2, captureGridSize / 2)
-- Get the extended capture rectangle that encloses all grid cells that need to be included in the capture.
-- In this case we only need to extend the capture area by the valid rendering rectangle.
local validTopLeft, validBottomRight = Coords:ValidRenderingRect()
local validTopLeftWorld, validBottomRightWorld = Coords:ToWorld(validTopLeft, topLeft + captureOffset), Coords:ToWorld(validBottomRight, bottomRight + captureOffset)
---The capture rectangle in grid coordinates.
---@type Vec2, Vec2
local gridTopLeft, gridBottomRight = (validTopLeftWorld / captureGridSize):Rounded("floor"), ((validBottomRightWorld) / captureGridSize):Rounded("ceil") - Vec2(1, 1)
---Size of the rectangle in grid cells.
---@type Vec2
local gridSize = gridBottomRight - gridTopLeft
---Process main callback.
---@param ctx ProcessRunnerCtx
local function handleDo(ctx)
Modification.SetCameraFree(true)
ctx.state = { Current = 0, Max = gridSize.x * gridSize.y }
for gridY = gridTopLeft.y, gridBottomRight.y-1, 1 do
for gridX = gridTopLeft.x, gridBottomRight.x-1, 1 do
-- Prematurely stop capturing if that is requested by the context.
if ctx:IsStopping() then return end
---Position in grid coordinates.
---@type Vec2
local gridPos = Vec2(gridX, gridY)
---Position in world coordinates.
---@type Vec2
local pos = gridPos * captureGridSize
pos:Add(captureOffset) -- Move to center of grid cell.
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
ctx.state.Current = ctx.state.Current + 1
end
end
end
---Process end callback.
---@param ctx ProcessRunnerCtx
local function handleEnd(ctx)
Modification.SetCameraFree()
end
-- Run process, if there is no other running right now.
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end
---Starts the live capturing process. ---Starts the live capturing process.
---Use `Capture.MapCapturingCtx` to stop, control or view the process. ---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingLive(outputPixelScale) function Capture:StartCapturingLive(outputPixelScale)
---Queries the mod settings for the live capture parameters. ---Queries the mod settings for the live capture parameters.
@ -445,7 +308,7 @@ function Capture:StartCapturingLive(outputPixelScale)
if oldPos then distanceSqr = CameraAPI.GetPos():DistanceSqr(oldPos) else distanceSqr = math.huge end if oldPos then distanceSqr = CameraAPI.GetPos():DistanceSqr(oldPos) else distanceSqr = math.huge end
until ctx:IsStopping() or ((delayFrames >= interval or distanceSqr >= maxDistanceSqr) and distanceSqr >= minDistanceSqr) until ctx:IsStopping() or ((delayFrames >= interval or distanceSqr >= maxDistanceSqr) and distanceSqr >= minDistanceSqr)
captureScreenshot(nil, false, false, ctx, outputPixelScale, nil) captureScreenshot(nil, false, false, ctx, outputPixelScale)
oldPos = CameraAPI.GetPos() oldPos = CameraAPI.GetPos()
until ctx:IsStopping() until ctx:IsStopping()
end end
@ -460,8 +323,8 @@ function Capture:StartCapturingLive(outputPixelScale)
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler) self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end end
---Gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and/or modifies those entities. ---Gathers all entities on the screen (around x, y within radius), serializes them, appends them into entityFile and modifies those entities.
---@param file file*? ---@param file file*|nil
---@param modify boolean ---@param modify boolean
---@param x number ---@param x number
---@param y number ---@param y number
@ -584,7 +447,7 @@ local function captureModifyEntities(file, modify, x, y, radius)
end end
--- ---
---@return file*? ---@return file*|nil
local function createOrOpenEntityCaptureFile() local function createOrOpenEntityCaptureFile()
-- Make sure the file exists. -- Make sure the file exists.
local file = io.open("mods/noita-mapcap/output/entities.json", "a") local file = io.open("mods/noita-mapcap/output/entities.json", "a")
@ -644,7 +507,7 @@ function Capture:StartCapturingEntities(store, modify)
end end
---Writes the current player position and other stats onto disk. ---Writes the current player position and other stats onto disk.
---@param file file*? ---@param file file*|nil
---@param pos Vec2 ---@param pos Vec2
---@param oldPos Vec2 ---@param oldPos Vec2
---@param hp number ---@param hp number
@ -677,7 +540,7 @@ local function writePlayerPathEntry(file, pos, oldPos, hp, maxHP, polymorphed)
end end
--- ---
---@return file*? ---@return file*|nil
local function createOrOpenPlayerPathCaptureFile() local function createOrOpenPlayerPathCaptureFile()
-- Make sure the file exists. -- Make sure the file exists.
local file = io.open("mods/noita-mapcap/output/player-path.json", "a") local file = io.open("mods/noita-mapcap/output/player-path.json", "a")
@ -692,8 +555,8 @@ end
---Starts capturing the player path. ---Starts capturing the player path.
---Use `Capture.PlayerPathCapturingCtx` to stop, control or view the progress. ---Use `Capture.PlayerPathCapturingCtx` to stop, control or view the progress.
---@param interval integer? -- Wait time between captures in frames. ---@param interval integer|nil -- Wait time between captures in frames.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio. ---@param outputPixelScale number|nil -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingPlayerPath(interval, outputPixelScale) function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
interval = interval or 20 interval = interval or 20
@ -721,7 +584,7 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
-- It seems that the entity can still be found by the tag, but its components/values can't be accessed anymore. -- It seems that the entity can still be found by the tag, but its components/values can't be accessed anymore.
-- Solution: Don't do that. -- Solution: Don't do that.
---@type NoitaEntity? ---@type NoitaEntity|nil
local playerEntity local playerEntity
-- Try to find the regular player entity. -- Try to find the regular player entity.
@ -781,56 +644,6 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr) self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
end end
---Starts to capture an animation.
---This stores sequences of images that can't be stitched, but can be rendered into a video instead.
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
---@param outputPixelScale number? -- The resulting image pixel to world pixel ratio.
function Capture:StartCapturingAnimation(outputPixelScale)
---Queries the mod settings for the live capture parameters.
---@return integer interval -- The interval length in frames.
local function querySettings()
local interval = 1--tonumber(ModSettingGet("noita-mapcap.live-interval")) or 30
return interval
end
-- 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.
---@param ctx ProcessRunnerCtx
local function handleDo(ctx)
Modification.SetCameraFree(false)
local frame = 0
repeat
local interval = querySettings()
-- Wait until we are allowed to take a new screenshot.
local delayFrames = 0
repeat
wait(0)
delayFrames = delayFrames + 1
until ctx:IsStopping() or delayFrames >= interval
captureScreenshotAnimation(outputPixelScale, frame)
frame = frame + 1
until ctx:IsStopping()
end
---Process end callback.
---@param ctx ProcessRunnerCtx
local function handleEnd(ctx)
Modification.SetCameraFree()
end
-- Run process, if there is no other running right now.
self.MapCapturingCtx:Run(nil, handleDo, handleEnd, mapCapturingCtxErrHandler)
end
---Starts the capturing process based on user/mod settings. ---Starts the capturing process based on user/mod settings.
function Capture:StartCapturing() function Capture:StartCapturing()
Message:CatchException("Capture:StartCapturing", function() Message:CatchException("Capture:StartCapturing", function()
@ -838,26 +651,21 @@ function Capture:StartCapturing()
local mode = ModSettingGet("noita-mapcap.capture-mode") local mode = ModSettingGet("noita-mapcap.capture-mode")
local outputPixelScale = ModSettingGet("noita-mapcap.pixel-scale") local outputPixelScale = ModSettingGet("noita-mapcap.pixel-scale")
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size")) local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
local captureDelay = tonumber(ModSettingGet("noita-mapcap.capture-delay"))
if mode == "live" then if mode == "live" then
self:StartCapturingLive(outputPixelScale) self:StartCapturingLive(outputPixelScale)
self:StartCapturingPlayerPath(5, outputPixelScale) -- Capture player path with an interval of 5 frames. self:StartCapturingPlayerPath(5, outputPixelScale) -- Capture player path with an interval of 5 frames.
elseif mode == "animation" then
self:StartCapturingAnimation(outputPixelScale)
elseif mode == "area" then elseif mode == "area" then
local area = ModSettingGet("noita-mapcap.area") local area = ModSettingGet("noita-mapcap.area")
if area == "custom" then if area == "custom" then
local topLeft = Vec2(ModSettingGet("noita-mapcap.area-top-left")) local topLeft = Vec2(ModSettingGet("noita-mapcap.area-top-left"))
local bottomRight = Vec2(ModSettingGet("noita-mapcap.area-bottom-right")) local bottomRight = Vec2(ModSettingGet("noita-mapcap.area-bottom-right"))
self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay) self:StartCapturingArea(topLeft, bottomRight, captureGridSize, outputPixelScale)
else else
---@type fun():Vec2, Vec2 local predefinedArea = Config.CaptureArea[area]
local predefinedAreaFunction = Config.CaptureArea[area] if predefinedArea then
if predefinedAreaFunction then self:StartCapturingArea(predefinedArea.TopLeft, predefinedArea.BottomRight, captureGridSize, outputPixelScale)
local topLeft, bottomRight = predefinedAreaFunction()
self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
else else
Message:ShowRuntimeError("PredefinedArea", string.format("Unknown predefined capturing area %q", tostring(area))) Message:ShowRuntimeError("PredefinedArea", string.format("Unknown predefined capturing area %q", tostring(area)))
end end
@ -866,13 +674,13 @@ function Capture:StartCapturing()
local origin = ModSettingGet("noita-mapcap.capture-mode-spiral-origin") local origin = ModSettingGet("noita-mapcap.capture-mode-spiral-origin")
if origin == "custom" then if origin == "custom" then
local originVec = Vec2(ModSettingGet("noita-mapcap.capture-mode-spiral-origin-vector")) local originVec = Vec2(ModSettingGet("noita-mapcap.capture-mode-spiral-origin-vector"))
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale)
elseif origin == "0" then elseif origin == "0" then
local originVec = Vec2(0, 0) local originVec = Vec2(0, 0)
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale)
elseif origin == "current" then elseif origin == "current" then
local originVec = CameraAPI:GetPos() local originVec = CameraAPI:GetPos()
self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale, captureDelay) self:StartCapturingSpiral(originVec, captureGridSize, outputPixelScale)
else else
Message:ShowRuntimeError("SpiralOrigin", string.format("Unknown spiral origin %q", tostring(origin))) Message:ShowRuntimeError("SpiralOrigin", string.format("Unknown spiral origin %q", tostring(origin)))
end end

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2024 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
@ -53,8 +53,8 @@ function Check:Regular(interval)
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area. local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
if topLeft and bottomRight then if topLeft and bottomRight then
local actual = bottomRight - topLeft local actual = bottomRight - topLeft
if actual ~= Coords:InternalRectSize() then if actual ~= Coords.WindowResolution then
Message:ShowWrongResolution(Modification.AutoSet, string.format("Internal rectangle size is %s. Current resolution is %s.", Coords:InternalRectSize(), actual)) Message:ShowWrongResolution(Modification.AutoSet, string.format("Old window resolution is %s. Current resolution is %s.", Coords.WindowResolution, actual))
end end
else else
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.") Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
@ -86,12 +86,6 @@ function Check:Regular(interval)
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Screen shake intensity is %s, expected %s.", self.StartupConfig.screenshake_intensity, expected)) Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Screen shake intensity is %s, expected %s.", self.StartupConfig.screenshake_intensity, expected))
end end
end end
if config["application_rendered_cursor"] then
local expected = config.application_rendered_cursor
if expected ~= self.StartupConfig.application_rendered_cursor then
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Application rendered cursor is %s, expected %s.", self.StartupConfig.application_rendered_cursor, expected))
end
end
-- Magic numbers stuff doesn't need a forced restart, just a normal restart by the user. -- Magic numbers stuff doesn't need a forced restart, just a normal restart by the user.
if magic["VIRTUAL_RESOLUTION_X"] and magic["VIRTUAL_RESOLUTION_Y"] then if magic["VIRTUAL_RESOLUTION_X"] and magic["VIRTUAL_RESOLUTION_Y"] then
@ -120,7 +114,7 @@ function Check:Regular(interval)
-- This is not perfect, as it doesn't take rounding and cropping into account, so the actual captured area may be a few pixels smaller. -- This is not perfect, as it doesn't take rounding and cropping into account, so the actual captured area may be a few pixels smaller.
local mode = ModSettingGet("noita-mapcap.capture-mode") local mode = ModSettingGet("noita-mapcap.capture-mode")
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size")) local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
if (mode ~= "live" and mode ~= "animation") and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then if mode ~= "live" and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
Message:ShowGeneralSettingsProblem( Message:ShowGeneralSettingsProblem(
"The virtual resolution is smaller than the capture grid size.", "The virtual resolution is smaller than the capture grid size.",
"This means that you will get black areas in your final stitched image.", "This means that you will get black areas in your final stitched image.",

View File

@ -1,9 +1,8 @@
-- Copyright (c) 2022-2024 David Vogel -- Copyright (c) 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
local NXML = require("luanxml.nxml")
local Vec2 = require("noita-api.vec2") local Vec2 = require("noita-api.vec2")
-- List of components that will be disabled on every encountered entity. -- List of components that will be disabled on every encountered entity.
@ -28,65 +27,22 @@ Config.ComponentsToDisable = {
--"AudioComponent", --"AudioComponent",
} }
local CHUNK_SIZE = 512
---Returns the rectangle of the base area as two vectors.
---@return Vec2 TopLeft Top left corner in world coordinates.
---@return Vec2 BottomRight Bottom right corner in world coordinates. This pixel is not included in the final rectangle.
local function getBaseArea()
local xml = NXML.parse(ModTextFileGetContent("data/biome/_biomes_all.xml"))
local width, height = BiomeMapGetSize()
local offsetX, offsetY = math.floor(width/2), xml.attr.biome_offset_y -- TODO: This may not be right. Check what Noita is really doing when we have a biome map with an odd width.
return Vec2(-offsetX, -offsetY)*CHUNK_SIZE, Vec2(-offsetX+width, -offsetY+height)*CHUNK_SIZE
--return Vec2(-17920, -7168), Vec2(17920, 17408) -- Coordinates for a "New Game" without mods or anything.
end
---A list of capture areas.
---This contains functions that determine the capture area based on the biome size and other parameters.
---The returned vectors are the top left corner, and the bottom right corner of the capture area in world coordinates.
---The bottom right corner pixel is not included in the rectangle.
---@type table<string, fun():Vec2, Vec2>
Config.CaptureArea = { Config.CaptureArea = {
-- Base layout: Every part outside this is based on a similar layout, but uses different materials/seeds. -- Base layout: Every part outside this is based on a similar layout, but uses different materials/seeds.
["1x1"] = getBaseArea, ["1x1"] = {
TopLeft = Vec2(-17920, -7168), -- in world coordinates.
BottomRight = Vec2(17920, 17408), -- in world coordinates. This pixel is not included in the rectangle.
},
-- Main world: The main world with 3 parts: sky, normal and hell. -- Main world: The main world with 3 parts: sky, normal and hell.
["1x3"] = function() ["1x3"] = {
local width, height = BiomeMapGetSize() TopLeft = Vec2(-17920, -31744), -- in world coordinates.
local topLeft, bottomRight = getBaseArea() BottomRight = Vec2(17920, 41984), -- in world coordinates. This pixel is not included in the rectangle.
return topLeft + Vec2(0, -height)*CHUNK_SIZE, bottomRight + Vec2(0, height)*CHUNK_SIZE },
--return Vec2(-17920, -31744), Vec2(17920, 41984) -- Coordinates for a "New Game" without mods or anything.
end,
-- -1 parallel world: The parallel world with 3 parts: sky, normal and hell.
["1x3 -1"] = function()
local width, height = BiomeMapGetSize()
local topLeft, bottomRight = getBaseArea()
return topLeft + Vec2(-width, -height)*CHUNK_SIZE, bottomRight + Vec2(-width, height)*CHUNK_SIZE
--return Vec2(-17920, -31744) + Vec2(-35840, 0), Vec2(17920, 41984) + Vec2(-35840, 0) -- Coordinates for a "New Game" without mods or anything.
end,
-- +1 parallel world: The parallel world with 3 parts: sky, normal and hell.
["1x3 +1"] = function()
local width, height = BiomeMapGetSize()
local topLeft, bottomRight = getBaseArea()
return topLeft + Vec2(width, -height)*CHUNK_SIZE, bottomRight + Vec2(width, height)*CHUNK_SIZE
--return Vec2(-17920, -31744) + Vec2(35840, 0), Vec2(17920, 41984) + Vec2(35840, 0) -- Coordinates for a "New Game" without mods or anything.
end,
-- Extended: Main world + a fraction of the parallel worlds to the left and right. -- Extended: Main world + a fraction of the parallel worlds to the left and right.
["1.5x3"] = function() ["1.5x3"] = {
local width, height = BiomeMapGetSize() TopLeft = Vec2(-25600, -31744), -- in world coordinates.
local topLeft, bottomRight = getBaseArea() BottomRight = Vec2(25600, 41984), -- in world coordinates. This pixel is not included in the rectangle.
return topLeft + Vec2(-math.floor(0.25*width), -height)*CHUNK_SIZE, bottomRight + Vec2(math.floor(0.25*width), height)*CHUNK_SIZE },
--return Vec2(-25600, -31744), Vec2(25600, 41984) -- Coordinates for a "New Game" without mods or anything. These coordinates may not exactly be 1.5 of the base width for historic reasons.
end,
-- Extended: Main world + each parallel world to the left and right.
["3x3"] = function()
local width, height = BiomeMapGetSize()
local topLeft, bottomRight = getBaseArea()
return topLeft + Vec2(-width, -height)*CHUNK_SIZE, bottomRight + Vec2(width, height)*CHUNK_SIZE
--return Vec2(-53760, -31744), Vec2(53760, 41984) -- Coordinates for a "New Game" without mods or anything.
end,
} }

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2023 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
@ -24,13 +24,6 @@ ffi.cdef([[
LONG bottom; LONG bottom;
} RECT; } RECT;
typedef struct {
LONG x;
LONG y;
LONG width;
LONG height;
} GLViewportDims;
bool GetRect(RECT* rect); bool GetRect(RECT* rect);
bool Capture(RECT* rect, int x, int y, int sx, int sy); bool Capture(RECT* rect, int x, int y, int sx, int sy);
]]) ]])

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2024 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
@ -12,7 +12,6 @@
-------------------------- --------------------------
local Coords = require("coordinates") local Coords = require("coordinates")
local DebugAPI = require("noita-api.debug")
---------- ----------
-- Code -- -- Code --
@ -128,13 +127,12 @@ function Message:ShowWrongResolution(callback, desc)
"The resolution changed:", "The resolution changed:",
desc or "", desc or "",
" ", " ",
"Press the button at the bottom to set up and close Noita automatically.", "To fix:",
" ", "- Deselect and select the Noita window, or",
"You can always reset any custom settings by right clicking the `start capture`", "- restart Noita or revert the resolution change."
"button at the top left.",
}, },
Actions = { Actions = {
{ Name = "Setup and close (May corrupt current save!)", Hint = nil, HintDesc = nil, Callback = callback }, { Name = "Query settings again", Hint = nil, HintDesc = nil, Callback = function() Coords:ReadResolutions() end },
}, },
AutoClose = true, -- This message will automatically close. AutoClose = true, -- This message will automatically close.
} }
@ -188,53 +186,13 @@ end
function Message:ShowModificationUnsupported(realm, name, value) function Message:ShowModificationUnsupported(realm, name, value)
self.List = self.List or {} self.List = self.List or {}
self.List["ModificationFailed"] = self.List["ModificationFailed"] or { self.List["ModificationFailed"] = {
Type = "warning", Type = "warning",
Lines = {
string.format("Couldn't modify %q in %q realm.", name, realm),
" ",
"This simply means that this modification is not supported for the Noita version you are using.",
"Feel free to open an issue at https://github.com/Dadido3/noita-mapcap.",
},
} }
-- Create or append to list of modifications.
-- We have to prevent duplicate entries.
self.List["ModificationFailed"].ModificationEntries = self.List["ModificationFailed"].ModificationEntries or {}
local found
for _, modEntry in ipairs(self.List["ModificationFailed"].ModificationEntries) do
if modEntry.realm == realm and modEntry.name == name then
found = true
break
end
end
if not found then
table.insert(self.List["ModificationFailed"].ModificationEntries, {realm = realm, name = name, value = value})
end
-- Build message lines.
self.List["ModificationFailed"].Lines = {"The mod couldn't apply the following changes:"}
table.insert(self.List["ModificationFailed"].Lines, " ")
for _, modEntry in ipairs(self.List["ModificationFailed"].ModificationEntries) do
table.insert(self.List["ModificationFailed"].Lines, string.format("- %q in %q realm", modEntry.name, modEntry.realm))
end
table.insert(self.List["ModificationFailed"].Lines, " ")
table.insert(self.List["ModificationFailed"].Lines, "This simply means that the mod can't automatically apply this change in the Noita version you are using.")
table.insert(self.List["ModificationFailed"].Lines, "If you are running a non-beta version of Noita, feel free to open an issue at https://github.com/Dadido3/noita-mapcap.")
-- Tell the user to change some settings manually, if possible.
local manuallyWithF7 = {}
local possibleManualWithF7 = {mPostFxDisabled = true, mGuiDisabled = true, mGuiHalfSize = true, mFogOfWarOpenEverywhere = true, mTrailerMode = true, mDayTimeRotationPause = true, mPlayerNeverDies = true, mFreezeAI = true}
for _, modEntry in ipairs(self.List["ModificationFailed"].ModificationEntries) do
if modEntry.realm == "processMemory" and possibleManualWithF7[modEntry.name] then
table.insert(manuallyWithF7, modEntry)
end
end
if #manuallyWithF7 > 0 then
table.insert(self.List["ModificationFailed"].Lines, " ")
table.insert(self.List["ModificationFailed"].Lines, "You can apply the setting manually:")
table.insert(self.List["ModificationFailed"].Lines, " ")
table.insert(self.List["ModificationFailed"].Lines, "- Press F7 to open the debug menu.")
for _, modEntry in ipairs(manuallyWithF7) do
table.insert(self.List["ModificationFailed"].Lines, string.format("- Change %q to %q.", modEntry.name, modEntry.value))
end
table.insert(self.List["ModificationFailed"].Lines, "- Press F7 again to close the menu.")
table.insert(self.List["ModificationFailed"].Lines, "- Close this warning when you are done.")
end
end end

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2022-2024 David Vogel -- Copyright (c) 2022-2023 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
@ -160,114 +160,14 @@ function Modification.SetMemoryOptions(memory)
}, },
{_Offset = 0x00F8A9DC, _BuildString = "Build Dec 21 2023 00:07:29", -- Steam dev build. {_Offset = 0x00F8A9DC, _BuildString = "Build Dec 21 2023 00:07:29", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x010F814C)[0] = value end, mPostFxDisabled = function(value) ffi.cast("char*", 0x010F814C)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x010F814D)[0] = value end, mGuiDisabled = function(value) ffi.cast("char*", 0x0010F814D)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x010F814E)[0] = value end, mGuiHalfSize = function(value) ffi.cast("char*", 0x0010F814E)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F814F)[0] = value end, mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0010F814F)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x010F8150)[0] = value end, mTrailerMode = function(value) ffi.cast("char*", 0x010F8150)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F8151)[0] = value end, mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F8151)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F8152)[0] = value end, mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F8152)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x010F8153)[0] = value end, mFreezeAI = function(value) ffi.cast("char*", 0x010F8153)[0] = value end,
}, },
{_Offset = 0x00F71DE4, _BuildString = "Build Dec 29 2023 23:36:18", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0111758C)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0111758D)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0111758E)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0111758F)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x01117590)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01117591)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x01117592)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x01117593)[0] = value end,
},
{_Offset = 0x00F74FA8, _BuildString = "Build Dec 30 2023 19:37:04", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0111A5BC)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0111A5BD)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0111A5BE)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0111A5BF)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x0111A5C0)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0111A5C1)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x0111A5C2)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x0111A5C3)[0] = value end,
},
{_Offset = 0x00F8BA04, _BuildString = "Build Jan 18 2024 12:57:44", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x010F91FC+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x010F91FC+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x010F91FC+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F91FC+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x010F91FC+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F91FC+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F91FC+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x010F91FC+7)[0] = value end,
},
{_Offset = 0x0117091C, _BuildString = "Build Feb 2 2024 14:29:06", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0130585C)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0130585D)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0130585E)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0130585F)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x01305860)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01305861)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x01305862)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x01305863)[0] = value end,
},
{_Offset = 0x01173F34, _BuildString = "Build Feb 6 2024 15:54:02", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0130982C+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0130982C+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0130982C+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0130982C+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x0130982C+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0130982C+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x0130982C+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x0130982C+7)[0] = value end,
},
{_Offset = 0x01182F70, _BuildString = "Build Mar 25 2024 17:42:49", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x013198AC+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x013198AC+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x013198AC+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x013198AC+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x013198AC+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x013198AC+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x013198AC+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x013198AC+7)[0] = value end,
},
{_Offset = 0x011871FC, _BuildString = "Build Apr 6 2024 20:50:04", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0131D89C+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0131D89C+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0131D89C+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0131D89C+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x0131D89C+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0131D89C+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x0131D89C+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x0131D89C+7)[0] = value end,
},
{_Offset = 0x0118718C, _BuildString = "Build Apr 8 2024 18:07:16", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x0131D8DC+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x0131D8DC+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x0131D8DC+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0131D8DC+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x0131D8DC+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0131D8DC+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x0131D8DC+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x0131D8DC+7)[0] = value end,
},
{_Offset = 0x0118FD3C, _BuildString = "Build Aug 12 2024 21:10:13", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x01327D3C+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x01327D3C+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x01327D3C+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x01327D3C+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x01327D3C+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01327D3C+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x01327D3C+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x01327D3C+7)[0] = value end,
},
{_Offset = 0x0118FD3C, _BuildString = "Build Aug 12 2024 21:43:22", -- Steam dev build.
mPostFxDisabled = function(value) ffi.cast("char*", 0x01327D3C+0)[0] = value end,
mGuiDisabled = function(value) ffi.cast("char*", 0x01327D3C+1)[0] = value end,
mGuiHalfSize = function(value) ffi.cast("char*", 0x01327D3C+2)[0] = value end,
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x01327D3C+3)[0] = value end,
mTrailerMode = function(value) ffi.cast("char*", 0x01327D3C+4)[0] = value end,
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01327D3C+5)[0] = value end,
mPlayerNeverDies = function(value) ffi.cast("char*", 0x01327D3C+6)[0] = value end,
mFreezeAI = function(value) ffi.cast("char*", 0x01327D3C+7)[0] = value end,
},
}, },
}, },
[false] = { [false] = {
@ -314,104 +214,13 @@ function Modification.SetMemoryOptions(memory)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled. ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end, end,
}, },
{_Offset = 0x00E14FA0, _BuildString = "Build Dec 29 2023 23:40:18", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x00625FFD)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00E180E8, _BuildString = "Build Dec 30 2023 19:40:49", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x00626EFD)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00E24E6C, _BuildString = "Build Jan 18 2024 13:01:21", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x00642A47+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00FECE94, _BuildString = "Build Feb 2 2024 14:33:26", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006AD407)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00FEEFC0, _BuildString = "Build Feb 6 2024 15:58:22", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006AD611+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00FF21D8, _BuildString = "Build Feb 9 2024 15:52:49", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006AE101+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00FF22D4, _BuildString = "Build Feb 12 2024 19:07:19", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006AE161+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00E23E80, _BuildString = "Build Feb 14 2024 07:46:57", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006427A7+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x00FFDB54, _BuildString = "Build Mar 25 2024 17:48:04", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006B1FD8+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x01001DDC, _BuildString = "Build Apr 6 2024 20:54:23", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006B35B5+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x01001DF4, _BuildString = "Build Apr 8 2024 18:11:27", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006B3355+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x01007CA4, _BuildString = "Build Aug 12 2024 21:14:23", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006B3925+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
{_Offset = 0x01007CA4, _BuildString = "Build Aug 12 2024 21:48:01", -- Steam build.
enableModDetection = function(value)
local ptr = ffi.cast("char*", 0x006B3925+6)
Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
},
}, },
}, },
} }
-- Look up the tree and set options accordingly. -- Look up the tree and set options accordingly.
local level1 = lookup[DebugAPI.IsDevBuild()] local level1 = lookup[DebugGetIsDevBuild()]
level1 = level1 or {} level1 = level1 or {}
local level2 = level1[ffi.os] local level2 = level1[ffi.os]
@ -448,13 +257,6 @@ function Modification.PatchFiles(patches)
end end
ModTextFileSetContent("data/shaders/post_final.frag", postFinal) ModTextFileSetContent("data/shaders/post_final.frag", postFinal)
end end
if patches.PostFinalReplace then
local postFinal = ModTextFileGetContent("data/shaders/post_final.frag")
for k, v in pairs(patches.PostFinalReplace) do
postFinal = postFinal:gsub(k, v)
end
ModTextFileSetContent("data/shaders/post_final.frag", postFinal)
end
end end
---Returns tables with user requested game configuration changes. ---Returns tables with user requested game configuration changes.
@ -476,16 +278,12 @@ function Modification.RequiredChanges()
config["internal_size_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).y) config["internal_size_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).y)
config["backbuffer_width"] = config["window_w"] config["backbuffer_width"] = config["window_w"]
config["backbuffer_height"] = config["window_h"] config["backbuffer_height"] = config["window_h"]
config["fullscreen"] = "0"
magic["VIRTUAL_RESOLUTION_X"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).x) magic["VIRTUAL_RESOLUTION_X"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).x)
magic["VIRTUAL_RESOLUTION_Y"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).y) magic["VIRTUAL_RESOLUTION_Y"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).y)
-- Set virtual offset to prevent/reduce not correctly drawn pixels at the window border. -- Set virtual offset to prevent/reduce not correctly drawn pixels at the window border.
magic["GRID_RENDER_BORDER"] = "3" -- This will widen the right side of the virtual rectangle. It also shifts the world coordinates to the right. magic["GRID_RENDER_BORDER"] = "3" -- This will widen the right side of the virtual rectangle. It also shifts the world coordinates to the right.
magic["VIRTUAL_RESOLUTION_OFFSET_X"] = "-3" magic["VIRTUAL_RESOLUTION_OFFSET_X"] = "-3"
magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "0" magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "0"
magic["STREAMING_CHUNK_TARGET"] = "16" -- Keep more chunks alive.
magic["GRID_MAX_UPDATES_PER_FRAME"] = "1024" -- Allow more pixel physics simulation steps (in 32x32 regions) per frame. With too few, objects can glitch through the terrain/explode.
magic["GRID_MIN_UPDATES_PER_FRAME"] = "0" -- Also allow no updates.
else else
-- Reset some values if there is no custom resolution requested. -- Reset some values if there is no custom resolution requested.
config["internal_size_w"] = "1280" config["internal_size_w"] = "1280"
@ -497,12 +295,13 @@ function Modification.RequiredChanges()
magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "-1" magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "-1"
end end
-- Always expect a fullscreen mode of 0 (windowed).
-- Capturing will not work in fullscreen.
config["fullscreen"] = "0"
-- Also disable screen shake. -- Also disable screen shake.
config["screenshake_intensity"] = "0" config["screenshake_intensity"] = "0"
-- And disable the cursor being rendered by Noita itself, which would make it appear in screen captures.
config["application_rendered_cursor"] = "0"
magic["DRAW_PARALLAX_BACKGROUND"] = ModSettingGet("noita-mapcap.disable-background") and "0" or "1" magic["DRAW_PARALLAX_BACKGROUND"] = ModSettingGet("noita-mapcap.disable-background") and "0" or "1"
-- These magic numbers seem only to work in the dev build. -- These magic numbers seem only to work in the dev build.
@ -510,11 +309,6 @@ function Modification.RequiredChanges()
magic["DEBUG_PAUSE_BOX2D"] = ModSettingGet("noita-mapcap.disable-physics") and "1" or "0" magic["DEBUG_PAUSE_BOX2D"] = ModSettingGet("noita-mapcap.disable-physics") and "1" or "0"
magic["DEBUG_DISABLE_POSTFX_DITHERING"] = ModSettingGet("noita-mapcap.disable-postfx") and "1" or "0" magic["DEBUG_DISABLE_POSTFX_DITHERING"] = ModSettingGet("noita-mapcap.disable-postfx") and "1" or "0"
-- These magic numbers stop any grid updates (pixel physics), even in the release build.
-- But any Box2D objects glitch, therefore the mod option (disable-physics) is disabled and hidden in the non dev build.
--magic["GRID_MAX_UPDATES_PER_FRAME"] = ModSettingGet("noita-mapcap.disable-physics") and "0" or "128"
--magic["GRID_MIN_UPDATES_PER_FRAME"] = ModSettingGet("noita-mapcap.disable-physics") and "1" or "40"
if ModSettingGet("noita-mapcap.disable-postfx") then if ModSettingGet("noita-mapcap.disable-postfx") then
patches.PostFinalConst = { patches.PostFinalConst = {
ENABLE_REFRACTION = false, ENABLE_REFRACTION = false,
@ -528,24 +322,13 @@ function Modification.RequiredChanges()
FOG_FOREGROUND_NIGHT = "vec4(0.0,0.0,0.0,1.0)", FOG_FOREGROUND_NIGHT = "vec4(0.0,0.0,0.0,1.0)",
FOG_BACKGROUND_NIGHT = "vec3(0.0,0.0,0.0)", FOG_BACKGROUND_NIGHT = "vec3(0.0,0.0,0.0)",
} }
-- Disable color grading, which may make the world look a tad more blue when there is freezing/snowing weather.
-- This is dependent on the seed and the PC's wall clock, and it only snows in December, January or February.
patches.PostFinalReplace = {
["color%.rgb = mix%( color, additive_overlay_color%.rgb, additive_overlay_color%.a %);"] = "",
["color = mix%(color, vec3%(%(color%.r %+ color%.g %+ color%.b%) %* 0%.3333%), color_grading%.a%);"] = "",
["color = color %* color_grading%.rgb;"] = "// Here lies the remains of the tone-mapping/color grading code. 2019-2024. RIP",
}
end end
if ModSettingGet("noita-mapcap.disable-shaders-gui-ai") and DebugAPI.IsDevBuild() then if ModSettingGet("noita-mapcap.disable-shaders-gui-ai") and DebugAPI.IsDevBuild() then
memory["mPostFxDisabled"] = 1 memory["mPostFxDisabled"] = 1
memory["mGuiDisabled"] = 1 memory["mGuiDisabled"] = 1
memory["mFreezeAI"] = 1 memory["mFreezeAI"] = 1
end memory["mTrailerMode"] = 1 -- Is necessary for chunks to correctly load when DEBUG_PAUSE_GRID_UPDATE is enabled.
if DebugAPI.IsDevBuild() and magic["DEBUG_PAUSE_GRID_UPDATE"] == "1" then
memory["mTrailerMode"] = 1 -- This is necessary for chunks to correctly load when DEBUG_PAUSE_GRID_UPDATE is enabled.
end end
if ModSettingGet("noita-mapcap.disable-mod-detection") and not DebugAPI.IsDevBuild() then if ModSettingGet("noita-mapcap.disable-mod-detection") and not DebugAPI.IsDevBuild() then

11
go.mod
View File

@ -1,10 +1,9 @@
module github.com/Dadido3/noita-mapcap module github.com/Dadido3/noita-mapcap
go 1.22 go 1.21
require ( require (
github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1 github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1
github.com/Dadido3/go-libwebp v0.3.0
github.com/cheggaaa/pb/v3 v3.1.4 github.com/cheggaaa/pb/v3 v3.1.4
github.com/coreos/go-semver v0.3.1 github.com/coreos/go-semver v0.3.1
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
@ -35,9 +34,9 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/tdewolff/minify/v2 v2.20.10 // indirect github.com/tdewolff/minify/v2 v2.20.10 // indirect
github.com/tdewolff/parse/v2 v2.7.7 // indirect github.com/tdewolff/parse/v2 v2.7.7 // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/image v0.14.0 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect
star-tex.org/x/tex v0.4.0 // indirect star-tex.org/x/tex v0.4.0 // indirect
) )

18
go.sum
View File

@ -4,8 +4,6 @@ github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1 h1:LejjvYg4tCW5HO
github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1/go.mod h1:cnC/60IoLiDM0GhdKYJ6oO7AwpZe1IQfPnSKlAURgHw= github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1/go.mod h1:cnC/60IoLiDM0GhdKYJ6oO7AwpZe1IQfPnSKlAURgHw=
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw=
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic=
github.com/Dadido3/go-libwebp v0.3.0 h1:Qr3Gt8Kn4qgemezDVnjAJffMB9C0QJhxP+9u0U5mC94=
github.com/Dadido3/go-libwebp v0.3.0/go.mod h1:rYiWwlI58XRSMUFMw23nMezErbjX3Z5Xv0Kk3w6Mwwo=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
@ -87,11 +85,11 @@ github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -99,14 +97,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU= gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2022-2024 David Vogel -- Copyright (c) 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
@ -108,6 +108,7 @@ 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()
Message:CatchException("OnWorldPreUpdate", function() Message:CatchException("OnWorldPreUpdate", function()
-- Coroutines aren't run every frame in this lua sandbox, do it manually here. -- Coroutines aren't run every frame in this lua sandbox, do it manually here.
wake_up_waiting_threads(1) wake_up_waiting_threads(1)

View File

@ -62,7 +62,7 @@ func addPathToZip(zipWriter *zip.Writer, srcPath, archiveBasePath string, ignore
return err return err
} }
header.Name = filepath.ToSlash(archivePath) header.Name = archivePath
header.Method = zip.Deflate header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header) writer, err := zipWriter.CreateHeader(header)

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2022-2024 David Vogel -- Copyright (c) 2022-2023 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
@ -67,9 +67,9 @@ modSettings = {
{ {
id = "capture-mode", id = "capture-mode",
ui_name = "Mode", ui_name = "Mode",
ui_description = "How the mod captures:\n- Live: Capture as you play along.\n- Area: Capture a defined area of the world.\n- Spiral: Capture in a spiral around a starting point indefinitely.\n- Animation: Capture the screen frame by frame.", ui_description = "How the mod captures:\n- Live: Capture as you play along.\n- Area: Capture a defined area of the world.\n- Spiral: Capture in a spiral around a starting point indefinitely.",
value_default = "live", value_default = "live",
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" }, { "animation", "Animation"} }, values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" } },
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
}, },
{ {
@ -95,7 +95,7 @@ modSettings = {
ui_name = " Rectangle", ui_name = " Rectangle",
ui_description = "The area to be captured.\nSee documentation for more information.", ui_description = "The area to be captured.\nSee documentation for more information.",
value_default = "1x1", value_default = "1x1",
values = { { "1x1", "Base layout" }, { "1x3", "Main World" }, { "1x3 -1", "-1 Parallel World" }, { "1x3 +1", "+1 Parallel World" }, { "1.5x3", "Extended" }, { "3x3", "3 Worlds" }, { "custom", "Custom" } }, values = { { "1x1", "Base layout" }, { "1x3", "Main World" }, { "1.5x3", "Extended" }, { "custom", "Custom" } },
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" end, show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" end,
}, },
@ -143,7 +143,7 @@ modSettings = {
value_default = "512", value_default = "512",
allowed_characters = "0123456789", allowed_characters = "0123456789",
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end, show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
}, },
{ {
id = "pixel-scale", id = "pixel-scale",
@ -157,18 +157,6 @@ modSettings = {
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
change_fn = roundChange, change_fn = roundChange,
}, },
{
id = "capture-delay",
ui_name = "Capture delay",
ui_description = "Additional delay before a screen capture is taken.\nThis can help the world to be populated, and the physics simulation to settle down.\nA setting of 0 means that the screenshot is taken as soon as possible.\n \nUse a value of 10 for a good result without slowing the capture process down too much.",
value_default = 0,
value_min = 0,
value_max = 60,
value_display_multiplier = 1,
value_display_formatting = " $0 frames",
scope = MOD_SETTING_SCOPE_RUNTIME,
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
},
{ {
id = "custom-resolution-live", id = "custom-resolution-live",
ui_name = "Use custom resolution", ui_name = "Use custom resolution",