mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2025-04-17 23:33:16 +00:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
f7ac3c4009 | |||
|
09d8bcfe09 | ||
b76233caa4 | |||
b6224e657f | |||
|
3e5950810a | ||
d3edf29a80 | |||
|
3fd0d970b7 | ||
|
627dc545d4 | ||
203a0ea159 | |||
0065afe413 | |||
9c6e57b340 | |||
44ed26952c | |||
|
1aa8b02882 | ||
|
e125df80b2 | ||
1936dc100c | |||
1d83b0dfa0 | |||
|
442c8f7f43 | ||
dcfe240bfc | |||
|
42f3af0655 | ||
4e79c08e6a | |||
aee72fd3c6 | |||
04eeca26e2 | |||
ae6bcd9743 | |||
920288614b | |||
c0fd7aab56 | |||
20d7c330a6 | |||
05e6b18745 | |||
|
f109c66152 | ||
|
8b2fe81f42 | ||
|
0758ec7a62 | ||
|
a9141679f4 | ||
690a2c55ab | |||
751877b472 | |||
b4dca53fc8 | |||
9cfb01187c | |||
dddaad938f | |||
41271d5321 | |||
b1a10870c1 |
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.21
|
||||
go-version: ^1.22
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.21
|
||||
go-version: ^1.22
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -109,3 +109,5 @@ $RECYCLE.BIN/
|
||||
/bin/stitch/*.dzi
|
||||
/bin/stitch/*_files/
|
||||
/files/magic-numbers/generated.xml
|
||||
|
||||
/bin/stitch/captures/*
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -1,12 +1,14 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"aabb",
|
||||
"acidflow",
|
||||
"appdata",
|
||||
"autosetup",
|
||||
"backbuffer",
|
||||
"basicfont",
|
||||
"bytecode",
|
||||
"cheggaaa",
|
||||
"Dadido",
|
||||
"dofile",
|
||||
"dont",
|
||||
"Downscales",
|
||||
@ -26,6 +28,7 @@
|
||||
"Lanczos",
|
||||
"lann",
|
||||
"ldflags",
|
||||
"libwebp",
|
||||
"linearize",
|
||||
"longleg",
|
||||
"lowram",
|
||||
@ -38,25 +41,30 @@
|
||||
"nfnt",
|
||||
"Niccoli",
|
||||
"noita",
|
||||
"noitamap",
|
||||
"Nolla",
|
||||
"NXML",
|
||||
"openseadragon",
|
||||
"pixelated",
|
||||
"polymorphed",
|
||||
"promptui",
|
||||
"rasterizer",
|
||||
"Regen",
|
||||
"respawn",
|
||||
"runfast",
|
||||
"savegames",
|
||||
"schollz",
|
||||
"screenshake",
|
||||
"svenstaro",
|
||||
"tcnksm",
|
||||
"tdewolff",
|
||||
"unmodded",
|
||||
"unstitchable",
|
||||
"upscaled",
|
||||
"Vogel",
|
||||
"Voronoi",
|
||||
"webp",
|
||||
"wepb",
|
||||
"xmax",
|
||||
"xmin",
|
||||
"ymax",
|
||||
|
17
AREAS.md
17
AREAS.md
@ -1,6 +1,10 @@
|
||||
# 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.
|
||||
`Right` and `Bottom` coordinates are not included in the rectangle.
|
||||
|
||||
@ -64,3 +68,16 @@ Bottom = 41984
|
||||
The end result will have a size of `51200 x 73728 pixels ~= 3775 megapixels`.
|
||||
|
||||

|
||||
|
||||
## `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`.
|
||||
|
32
README.md
32
README.md
@ -5,7 +5,10 @@ It works with the regular Noita build and the dev build.
|
||||
|
||||

|
||||
|
||||
A resulting image with nearly 3.8 gigapixels can be [seen here](https://easyzoom.com/image/223556) (May contain spoilers).
|
||||
Map captures created with this mod can be viewed on [map.runfast.stream] (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
|
||||
|
||||
@ -55,8 +58,7 @@ To the top left of the window are 3 buttons:
|
||||
-  Reveals the output directory in your file browser.
|
||||
This will contain raw screenshots and other recorded data that later can be stitched.
|
||||
|
||||
-  Reveals the stitching tool
|
||||
directory in your file browser.
|
||||
-  Reveals the stitching tool directory in your file browser.
|
||||
|
||||
To stitch the final result, click  to open the directory of the stitching tool.
|
||||
Start `stitch.exe` and proceed with the default values.
|
||||
@ -79,6 +81,10 @@ After a few minutes the file `output.png` will be created.
|
||||
- `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.
|
||||
|
||||
- `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
|
||||
|
||||
- `World seed`: If non empty, this will set the next new game to this seed.
|
||||
@ -146,8 +152,9 @@ The sliders are at their default values:
|
||||
|
||||
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](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.
|
||||
- ~~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.
|
||||
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.
|
||||
|
||||
@ -189,17 +196,28 @@ 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.
|
||||
This will cause the stitcher to only use the newest image tile for every resulting pixel.
|
||||
|
||||
## Additional information
|
||||
## Viewing and hosting captures
|
||||
|
||||
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 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
|
||||
|
||||
This mod uses the [LuaNXML](https://github.com/zatherz/luanxml) library by [Zatherz](https://github.com/zatherz).
|
||||
This mod uses the [LuaNXML] library by [Zatherz].
|
||||
|
||||
Thanks to [Daniel Niccoli](https://github.com/danielniccoli) for figuring out how to change some in-game options by manipulating process memory.
|
||||
|
||||
## 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
|
||||
|
@ -3,6 +3,8 @@
|
||||
; This software is released under the MIT License.
|
||||
; https://opensource.org/licenses/MIT
|
||||
|
||||
EnableExplicit
|
||||
|
||||
UsePNGImageEncoder()
|
||||
|
||||
Declare Worker(*Dummy)
|
||||
@ -26,6 +28,8 @@ Structure WorkerInfo
|
||||
workerNumber.i
|
||||
EndStructure
|
||||
|
||||
#Workers = 8
|
||||
|
||||
; Returns the size of the main OpenGL rendering output.
|
||||
ProcedureDLL GetGLViewportSize(*dims.GLViewportDims)
|
||||
If Not *dims
|
||||
@ -61,16 +65,16 @@ ProcedureDLL AttachProcess(Instance)
|
||||
|
||||
CreateDirectory("mods/noita-mapcap/output/")
|
||||
|
||||
Static Workers = 8
|
||||
Dim WorkerInfos.WorkerInfo(Workers-1)
|
||||
For i = 0 To Workers-1
|
||||
Static Dim WorkerInfos.WorkerInfo(#Workers-1)
|
||||
Protected i
|
||||
For i = 0 To #Workers-1
|
||||
WorkerInfos(i)\workerNumber = i
|
||||
CreateThread(@Worker(), @WorkerInfos(i))
|
||||
Next
|
||||
EndProcedure
|
||||
|
||||
Procedure Worker(*workerInfo.WorkerInfo)
|
||||
Protected img, x, y
|
||||
Protected img, x, y, sx, sy
|
||||
|
||||
Repeat
|
||||
WaitSemaphore(Semaphore)
|
||||
@ -110,6 +114,8 @@ ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
Protected imageID, hDC, *pixelBuffer
|
||||
|
||||
; Limit the desired capture area to the actual client area of the viewport.
|
||||
If *capRect\left < 0 : *capRect\left = 0 : EndIf
|
||||
If *capRect\top < 0 : *capRect\top = 0 : EndIf
|
||||
@ -126,8 +132,6 @@ ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
;Protected *pixelBuf = AllocateMemory(3 * width * height)
|
||||
|
||||
hDC = StartDrawing(ImageOutput(imageID))
|
||||
If Not hDC
|
||||
FreeImage(imageID)
|
||||
@ -136,19 +140,11 @@ ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
|
||||
|
||||
*pixelBuffer = DrawingBuffer()
|
||||
glReadPixels_(*capRect\left, *capRect\top, capWidth, capHeight, #GL_BGR_EXT, #GL_UNSIGNED_BYTE, *pixelBuffer)
|
||||
|
||||
; For y = 0 To *capRect\height - 1
|
||||
; *Line.Pixel = Buffer + Pitch * y
|
||||
;
|
||||
; For x = 0 To *capRect\width - 1
|
||||
;
|
||||
; *Line\Pixel = ColorTable(pos2) ; Write the pixel directly to the memory !
|
||||
; *Line+Offset
|
||||
;
|
||||
; ; You can try with regular plot to see the speed difference
|
||||
; ; Plot(x, y, ColorTable(pos2))
|
||||
; Next
|
||||
; Next
|
||||
If glGetError_() <> #GL_NO_ERROR
|
||||
StopDrawing()
|
||||
FreeImage(imageID)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
StopDrawing()
|
||||
|
||||
@ -182,8 +178,8 @@ EndProcedure
|
||||
|
||||
; IDE Options = PureBasic 6.04 LTS (Windows - x64)
|
||||
; ExecutableFormat = Shared dll
|
||||
; CursorPosition = 94
|
||||
; FirstLine = 51
|
||||
; CursorPosition = 99
|
||||
; FirstLine = 72
|
||||
; Folding = -
|
||||
; Optimizer
|
||||
; EnableThread
|
||||
|
Binary file not shown.
@ -36,7 +36,7 @@ example list of files:
|
||||
If set to 1, only the newest tile will be used for any resulting pixel.
|
||||
Use 1 to prevent ghosting and blurry objects.
|
||||
- `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`
|
||||
The path to the `entities.json` file. This contains Noita specific entity data. Defaults to "./../../output/entities.json".
|
||||
- `player-path string`
|
||||
@ -48,6 +48,8 @@ example list of files:
|
||||
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`
|
||||
Right bound of the output rectangle. This coordinate is not included in the output.
|
||||
- `xmin int`
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
// Copyright (c) 2022-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -8,6 +8,7 @@ package main
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
@ -106,7 +107,7 @@ func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
|
||||
images = append(images, tile.GetImage())
|
||||
}
|
||||
|
||||
// Create arrays to be reused every pixel.
|
||||
// Create color variables reused every pixel.
|
||||
var col color.RGBA
|
||||
var centerDistSqrMin int
|
||||
|
||||
@ -147,3 +148,17 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,13 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
type DZI struct {
|
||||
@ -99,9 +103,49 @@ func (d DZI) ExportDZIDescriptor(outputPath string) error {
|
||||
}
|
||||
|
||||
// ExportDZITiles exports the single image tiles for every zoom level.
|
||||
func (d DZI) ExportDZITiles(outputDir string) error {
|
||||
func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int) error {
|
||||
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).
|
||||
// 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.
|
||||
@ -120,6 +164,7 @@ func (d DZI) ExportDZITiles(outputDir string) error {
|
||||
imageTiles := ImageTiles{}
|
||||
|
||||
// Export tiles.
|
||||
lg := NewLimitGroup(runtime.NumCPU())
|
||||
for iY := 0; iY <= (stitchedImage.bounds.Dy()-1)/d.tileSize; iY++ {
|
||||
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)
|
||||
@ -127,11 +172,16 @@ func (d DZI) ExportDZITiles(outputDir string) error {
|
||||
rect = rect.Inset(-d.overlap)
|
||||
img := stitchedImage.SubStitchedImage(rect)
|
||||
filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension))
|
||||
if err := exportWebPSilent(img, filePath); err != nil {
|
||||
return fmt.Errorf("failed to export WebP: %w", err)
|
||||
}
|
||||
|
||||
scaleDivider := 2
|
||||
lg.Add(1)
|
||||
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{
|
||||
fileName: filePath,
|
||||
modTime: time.Now(),
|
||||
@ -143,11 +193,12 @@ func (d DZI) ExportDZITiles(outputDir string) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
lg.Wait()
|
||||
|
||||
// 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.
|
||||
var err error
|
||||
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodMedian{BlendTileLimit: 0}, 128, nil)
|
||||
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodFast{}, 128, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2023 David Vogel
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -10,9 +10,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportDZI(stitchedImage *StitchedImage, outputPath string, dziTileSize, dziOverlap int) error {
|
||||
func exportDZIStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar, dziTileSize, dziOverlap int, webPLevel int) error {
|
||||
descriptorPath := outputPath
|
||||
extension := filepath.Ext(outputPath)
|
||||
outputTilesPath := strings.TrimSuffix(outputPath, extension) + "_files"
|
||||
@ -30,7 +32,7 @@ func exportDZI(stitchedImage *StitchedImage, outputPath string, dziTileSize, dzi
|
||||
}
|
||||
|
||||
// Export DZI tiles.
|
||||
if err := dzi.ExportDZITiles(outputTilesPath); err != nil {
|
||||
if err := dzi.ExportDZITiles(outputTilesPath, bar, webPLevel); err != nil {
|
||||
return fmt.Errorf("failed to export DZI tiles: %w", err)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2023 David Vogel
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -11,15 +11,44 @@ import (
|
||||
"image/jpeg"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportJPEG(stitchedImage image.Image, outputPath string) error {
|
||||
func exportJPEGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error {
|
||||
log.Printf("Creating output file %q.", outputPath)
|
||||
|
||||
return exportJPEGSilent(stitchedImage, 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 exportJPEG(stitchedImage, outputPath)
|
||||
}
|
||||
|
||||
func exportJPEGSilent(stitchedImage image.Image, outputPath string) error {
|
||||
func exportJPEG(img image.Image, outputPath string) error {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
@ -30,7 +59,7 @@ func exportJPEGSilent(stitchedImage image.Image, outputPath string) error {
|
||||
Quality: 80,
|
||||
}
|
||||
|
||||
if err := jpeg.Encode(f, stitchedImage, options); err != nil {
|
||||
if err := jpeg.Encode(f, img, options); err != nil {
|
||||
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2023 David Vogel
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -11,15 +11,44 @@ import (
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportPNG(stitchedImage image.Image, outputPath string) error {
|
||||
func exportPNGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error {
|
||||
log.Printf("Creating output file %q.", outputPath)
|
||||
|
||||
return exportPNGSilent(stitchedImage, 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 exportPNG(stitchedImage, outputPath)
|
||||
}
|
||||
|
||||
func exportPNGSilent(stitchedImage image.Image, outputPath string) error {
|
||||
func exportPNG(img image.Image, outputPath string) error {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
@ -30,7 +59,7 @@ func exportPNGSilent(stitchedImage image.Image, outputPath string) error {
|
||||
CompressionLevel: png.DefaultCompression,
|
||||
}
|
||||
|
||||
if err := encoder.Encode(f, stitchedImage); err != nil {
|
||||
if err := encoder.Encode(f, img); err != nil {
|
||||
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
|
||||
}
|
||||
|
||||
|
@ -10,18 +10,46 @@ import (
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/Dadido3/go-libwebp/webp"
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportWebP(stitchedImage image.Image, outputPath string) error {
|
||||
func exportWebPStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar, webPLevel int) error {
|
||||
log.Printf("Creating output file %q.", outputPath)
|
||||
|
||||
return exportWebPSilent(stitchedImage, 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 exportWebPSilent(stitchedImage image.Image, outputPath string) error {
|
||||
bounds := stitchedImage.Bounds()
|
||||
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())
|
||||
}
|
||||
@ -32,7 +60,12 @@ func exportWebPSilent(stitchedImage image.Image, outputPath string) error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = webp.Encode(f, stitchedImage, &webp.Options{Lossless: true}); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
_ "image/png"
|
||||
"log"
|
||||
"os"
|
||||
@ -127,9 +128,16 @@ func (it *ImageTile) GetImage() *image.RGBA {
|
||||
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
||||
}
|
||||
|
||||
imgRGBA, ok := img.(*image.RGBA)
|
||||
if !ok {
|
||||
log.Printf("Expected an RGBA image for %q, got %T instead.", it.fileName, img)
|
||||
var imgRGBA *image.RGBA
|
||||
switch img := img.(type) {
|
||||
case *image.RGBA:
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1lann/promptui"
|
||||
@ -27,6 +26,7 @@ var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produ
|
||||
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 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.")
|
||||
@ -287,11 +287,35 @@ func main() {
|
||||
fmt.Sscanf(result, "%d", flagDZIOverlap)
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
// 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")
|
||||
}
|
||||
|
||||
bar := pb.Full.New(0)
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan struct{})
|
||||
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{
|
||||
BlendTileLimit: *flagBlendTileLimit, // Limit median blending to the n newest tiles by file modification time.
|
||||
@ -301,53 +325,31 @@ func main() {
|
||||
if err != nil {
|
||||
log.Panicf("NewStitchedImage() failed: %v.", err)
|
||||
}
|
||||
_, max := stitchedImage.Progress()
|
||||
bar.SetTotal(int64(max)).Start().SetRefreshRate(250 * time.Millisecond)
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}()
|
||||
bar := pb.Full.New(0)
|
||||
|
||||
switch fileExtension {
|
||||
case ".png":
|
||||
if err := exportPNG(stitchedImage, *flagOutputPath); err != nil {
|
||||
if err := exportPNGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil {
|
||||
log.Panicf("Export of PNG file failed: %v", err)
|
||||
}
|
||||
case ".jpg", ".jpeg":
|
||||
if err := exportJPEG(stitchedImage, *flagOutputPath); err != nil {
|
||||
if err := exportJPEGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil {
|
||||
log.Panicf("Export of JPEG file failed: %v", err)
|
||||
}
|
||||
case ".webp":
|
||||
if err := exportWebP(stitchedImage, *flagOutputPath); err != nil {
|
||||
if err := exportWebPStitchedImage(stitchedImage, *flagOutputPath, bar, *flagWebPLevel); err != nil {
|
||||
log.Panicf("Export of WebP file failed: %v", err)
|
||||
}
|
||||
case ".dzi":
|
||||
if err := exportDZI(stitchedImage, *flagOutputPath, *flagDZITileSize, *flagDZIOverlap); err != nil {
|
||||
if err := exportDZIStitchedImage(stitchedImage, *flagOutputPath, bar, *flagDZITileSize, *flagDZIOverlap, *flagWebPLevel); err != nil {
|
||||
log.Panicf("Export of DZI file failed: %v", err)
|
||||
}
|
||||
default:
|
||||
log.Panicf("Unknown output format %q.", fileExtension)
|
||||
}
|
||||
|
||||
done <- struct{}{}
|
||||
wg.Wait()
|
||||
log.Printf("Created output in %v.", time.Since(startTime))
|
||||
log.Printf("Created output in %v.", time.Since(bar.StartTime()))
|
||||
|
||||
//fmt.Println("Press the enter key to terminate the console screen!")
|
||||
//fmt.Scanln()
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
// Copyright (c) 2019-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// QuickSelect returns the kth smallest element of the given unsorted list.
|
||||
@ -96,3 +97,44 @@ func DivideCeil(a, b int) int {
|
||||
|
||||
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() }
|
||||
|
@ -84,9 +84,10 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
||||
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(-10, 10), math.random(-10, 10))) end
|
||||
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
|
||||
@ -109,6 +110,18 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
||||
if pos then CameraAPI.SetPos(pos) 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)
|
||||
delayFrames = delayFrames + 1
|
||||
if ctx then ctx.state.WaitFrames = ctx.state.WaitFrames + 1 end
|
||||
@ -153,6 +166,37 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
|
||||
MonitorStandby.ResetTimer()
|
||||
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.
|
||||
---@param err string
|
||||
---@param scope "init"|"do"|"end"
|
||||
@ -227,7 +271,7 @@ end
|
||||
---Starts the capturing process of the given area using a hilbert curve.
|
||||
---Use `Capture.MapCapturingCtx` to stop, control or view the process.
|
||||
---@param topLeft Vec2 -- Top left of the to be captured rectangle.
|
||||
---@param bottomRight Vec2 -- Non included bottom left of the to be captured rectangle.
|
||||
---@param 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.
|
||||
@ -237,15 +281,19 @@ function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize
|
||||
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
|
||||
if file ~= nil then file:close() end
|
||||
|
||||
---The rectangle in grid coordinates.
|
||||
-- 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 = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
|
||||
local gridTopLeft, gridBottomRight = (validTopLeftWorld / captureGridSize):Rounded("floor"), ((validBottomRightWorld) / captureGridSize):Rounded("ceil") - Vec2(1, 1)
|
||||
|
||||
-- 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.
|
||||
---Size of the rectangle in grid cells.
|
||||
---@type Vec2
|
||||
local gridSize = gridBottomRight - gridTopLeft
|
||||
|
||||
@ -274,7 +322,7 @@ function Capture:StartCapturingAreaHilbert(topLeft, bottomRight, captureGridSize
|
||||
---Position in world coordinates.
|
||||
---@type Vec2
|
||||
local pos = (hilbertPos + gridTopLeft) * captureGridSize
|
||||
pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
|
||||
pos:Add(captureOffset) -- Move to center of grid cell.
|
||||
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
|
||||
ctx.state.Current = ctx.state.Current + 1
|
||||
end
|
||||
@ -296,7 +344,7 @@ 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 included bottom 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.
|
||||
@ -306,11 +354,19 @@ function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, o
|
||||
local file = io.open("mods/noita-mapcap/output/nonempty", "a")
|
||||
if file ~= nil then file:close() end
|
||||
|
||||
---The rectangle in grid coordinates.
|
||||
---@type Vec2, Vec2
|
||||
local gridTopLeft, gridBottomRight = (topLeft / captureGridSize):Rounded("floor"), (bottomRight / captureGridSize):Rounded("floor")
|
||||
-- The capture offset which is needed to center the grid cells in the viewport.
|
||||
local captureOffset = Vec2(captureGridSize / 2, captureGridSize / 2)
|
||||
|
||||
---Size of the rectangle in grid coordinates.
|
||||
-- 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
|
||||
|
||||
@ -332,7 +388,7 @@ function Capture:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, o
|
||||
---Position in world coordinates.
|
||||
---@type Vec2
|
||||
local pos = gridPos * captureGridSize
|
||||
pos:Add(Vec2(captureGridSize / 2, captureGridSize / 2)) -- Move to center of grid cell.
|
||||
pos:Add(captureOffset) -- Move to center of grid cell.
|
||||
captureScreenshot(pos, true, true, ctx, outputPixelScale, captureDelay)
|
||||
ctx.state.Current = ctx.state.Current + 1
|
||||
end
|
||||
@ -725,6 +781,56 @@ function Capture:StartCapturingPlayerPath(interval, outputPixelScale)
|
||||
self.PlayerPathCapturingCtx:Run(handleInit, handleDo, handleEnd, handleErr)
|
||||
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.
|
||||
function Capture:StartCapturing()
|
||||
Message:CatchException("Capture:StartCapturing", function()
|
||||
@ -737,6 +843,8 @@ function Capture:StartCapturing()
|
||||
if mode == "live" then
|
||||
self:StartCapturingLive(outputPixelScale)
|
||||
self:StartCapturingPlayerPath(5, outputPixelScale) -- Capture player path with an interval of 5 frames.
|
||||
elseif mode == "animation" then
|
||||
self:StartCapturingAnimation(outputPixelScale)
|
||||
elseif mode == "area" then
|
||||
local area = ModSettingGet("noita-mapcap.area")
|
||||
if area == "custom" then
|
||||
@ -745,9 +853,11 @@ function Capture:StartCapturing()
|
||||
|
||||
self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
|
||||
else
|
||||
local predefinedArea = Config.CaptureArea[area]
|
||||
if predefinedArea then
|
||||
self:StartCapturingAreaScan(predefinedArea.TopLeft, predefinedArea.BottomRight, captureGridSize, outputPixelScale, captureDelay)
|
||||
---@type fun():Vec2, Vec2
|
||||
local predefinedAreaFunction = Config.CaptureArea[area]
|
||||
if predefinedAreaFunction then
|
||||
local topLeft, bottomRight = predefinedAreaFunction()
|
||||
self:StartCapturingAreaScan(topLeft, bottomRight, captureGridSize, outputPixelScale, captureDelay)
|
||||
else
|
||||
Message:ShowRuntimeError("PredefinedArea", string.format("Unknown predefined capturing area %q", tostring(area)))
|
||||
end
|
||||
|
@ -120,7 +120,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.
|
||||
local mode = ModSettingGet("noita-mapcap.capture-mode")
|
||||
local captureGridSize = tonumber(ModSettingGet("noita-mapcap.grid-size"))
|
||||
if mode ~= "live" and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
|
||||
if (mode ~= "live" and mode ~= "animation") and (Coords.VirtualResolution.x < captureGridSize or Coords.VirtualResolution.y < captureGridSize) then
|
||||
Message:ShowGeneralSettingsProblem(
|
||||
"The virtual resolution is smaller than the capture grid size.",
|
||||
"This means that you will get black areas in your final stitched image.",
|
||||
|
@ -1,8 +1,9 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
-- Copyright (c) 2022-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local NXML = require("luanxml.nxml")
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
-- List of components that will be disabled on every encountered entity.
|
||||
@ -27,22 +28,65 @@ Config.ComponentsToDisable = {
|
||||
--"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 = {
|
||||
-- Base layout: Every part outside this is based on a similar layout, but uses different materials/seeds.
|
||||
["1x1"] = {
|
||||
TopLeft = Vec2(-17920, -7168), -- in world coordinates.
|
||||
BottomRight = Vec2(17920, 17408), -- in world coordinates. This pixel is not included in the rectangle.
|
||||
},
|
||||
["1x1"] = getBaseArea,
|
||||
|
||||
-- Main world: The main world with 3 parts: sky, normal and hell.
|
||||
["1x3"] = {
|
||||
TopLeft = Vec2(-17920, -31744), -- in world coordinates.
|
||||
BottomRight = Vec2(17920, 41984), -- in world coordinates. This pixel is not included in the rectangle.
|
||||
},
|
||||
["1x3"] = function()
|
||||
local width, height = BiomeMapGetSize()
|
||||
local topLeft, bottomRight = getBaseArea()
|
||||
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.
|
||||
["1.5x3"] = {
|
||||
TopLeft = Vec2(-25600, -31744), -- in world coordinates.
|
||||
BottomRight = Vec2(25600, 41984), -- in world coordinates. This pixel is not included in the rectangle.
|
||||
},
|
||||
["1.5x3"] = function()
|
||||
local width, height = BiomeMapGetSize()
|
||||
local topLeft, bottomRight = getBaseArea()
|
||||
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,
|
||||
}
|
||||
|
@ -188,6 +188,16 @@ function Modification.SetMemoryOptions(memory)
|
||||
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,
|
||||
@ -198,6 +208,66 @@ function Modification.SetMemoryOptions(memory)
|
||||
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] = {
|
||||
@ -258,6 +328,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.
|
||||
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)
|
||||
@ -265,6 +342,69 @@ 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.
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -343,6 +483,9 @@ function Modification.RequiredChanges()
|
||||
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_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
|
||||
-- Reset some values if there is no custom resolution requested.
|
||||
config["internal_size_w"] = "1280"
|
||||
|
12
go.mod
12
go.mod
@ -1,10 +1,10 @@
|
||||
module github.com/Dadido3/noita-mapcap
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1
|
||||
github.com/chai2010/webp v1.1.1
|
||||
github.com/Dadido3/go-libwebp v0.3.0
|
||||
github.com/cheggaaa/pb/v3 v3.1.4
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
|
||||
@ -35,9 +35,9 @@ require (
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.20.10 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.7 // indirect
|
||||
golang.org/x/image v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
star-tex.org/x/tex v0.4.0 // indirect
|
||||
)
|
||||
|
20
go.sum
20
go.sum
@ -4,6 +4,8 @@ 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/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/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/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
@ -16,8 +18,6 @@ github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2
|
||||
github.com/benoitkugler/textprocessing v0.0.3/go.mod h1:/4bLyCf1QYywunMK3Gf89Nhb50YI/9POewqrLxWhxd4=
|
||||
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
|
||||
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
|
||||
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
||||
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||
github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo=
|
||||
github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@ -87,11 +87,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/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -99,14 +99,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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
|
||||
|
2
scripts/dist/compress.go
vendored
2
scripts/dist/compress.go
vendored
@ -62,7 +62,7 @@ func addPathToZip(zipWriter *zip.Writer, srcPath, archiveBasePath string, ignore
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = archivePath
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
|
12
settings.lua
12
settings.lua
@ -1,4 +1,4 @@
|
||||
-- Copyright (c) 2022-2023 David Vogel
|
||||
-- Copyright (c) 2022-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
@ -67,9 +67,9 @@ modSettings = {
|
||||
{
|
||||
id = "capture-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.",
|
||||
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.",
|
||||
value_default = "live",
|
||||
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" } },
|
||||
values = { { "live", "Live" }, { "area", "Area" }, { "spiral", "Spiral" }, { "animation", "Animation"} },
|
||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||
},
|
||||
{
|
||||
@ -95,7 +95,7 @@ modSettings = {
|
||||
ui_name = " Rectangle",
|
||||
ui_description = "The area to be captured.\nSee documentation for more information.",
|
||||
value_default = "1x1",
|
||||
values = { { "1x1", "Base layout" }, { "1x3", "Main World" }, { "1.5x3", "Extended" }, { "custom", "Custom" } },
|
||||
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" } },
|
||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" end,
|
||||
},
|
||||
@ -143,7 +143,7 @@ modSettings = {
|
||||
value_default = "512",
|
||||
allowed_characters = "0123456789",
|
||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
|
||||
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
|
||||
},
|
||||
{
|
||||
id = "pixel-scale",
|
||||
@ -167,7 +167,7 @@ modSettings = {
|
||||
value_display_multiplier = 1,
|
||||
value_display_formatting = " $0 frames",
|
||||
scope = MOD_SETTING_SCOPE_RUNTIME,
|
||||
show_fn = function() return modSettings:GetNextValue("capture-mode") ~= "live" end,
|
||||
show_fn = function() return modSettings:GetNextValue("capture-mode") == "area" or modSettings:GetNextValue("capture-mode") == "spiral" end,
|
||||
},
|
||||
{
|
||||
id = "custom-resolution-live",
|
||||
|
Loading…
Reference in New Issue
Block a user