Compare commits
215 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 | |||
47d570014d | |||
421f897be7 | |||
9c728e0ae2 | |||
d9d8c9cd78 | |||
15e2b88ed5 | |||
9e51538f3f | |||
24a1615706 | |||
45df692b96 | |||
93a1283188 | |||
ace1ab145a | |||
4f3f5c594d | |||
f22ef05411 | |||
d82fda528a | |||
860b724bd0 | |||
8057b14d8e | |||
0e431c64d3 | |||
f2b1aba994 | |||
1a735c06bd | |||
69f5d1ccb3 | |||
44605b9633 | |||
d5cd88a30e | |||
4de83e3dcd | |||
d774cf373d | |||
9da52a3f70 | |||
e83aa6803a | |||
f7426f3ed5 | |||
478e1284fb | |||
8bb8adf1ba | |||
355521b144 | |||
b76124b2e4 | |||
f0ee3e2399 | |||
b9fc890581 | |||
a96431361f | |||
88507af167 | |||
7a6915480b | |||
915da73845 | |||
6d028d4064 | |||
a0d5c13557 | |||
3016919348 | |||
cbdd925c30 | |||
a0168df91f | |||
182373d3cc | |||
0454e29e34 | |||
a70a5a4d1a | |||
f5a3bad396 | |||
905f629d2c | |||
|
b11b27d6c3 | ||
|
6ef2f7d1d3 | ||
8f729d3829 | |||
a6a0cc14e1 | |||
2b0f6a25f6 | |||
d69177cd3b | |||
f992748443 | |||
22b5c1827d | |||
b22b42a8d1 | |||
f1a3010d72 | |||
|
ad50faebc9 | ||
|
c72574c55d | ||
9494588e7b | |||
7a85f646cb | |||
d8dab5c318 | |||
|
b1971bb4be | ||
|
1652b278cb | ||
486f8e642d | |||
f964f5d769 | |||
28a768a130 | |||
959b198e46 | |||
18682ed441 | |||
28c07dfd25 | |||
fcfd8c88ff | |||
|
6761492ea8 | ||
615faac8e4 | |||
72f8e92412 | |||
b2ed6f65d5 | |||
c9d2a37903 | |||
65f7cb4e60 | |||
c3f841a4ff | |||
f5693b96f1 | |||
df6c27924b | |||
3a73e13fb7 | |||
7a4dbeddf1 | |||
7d250d6405 | |||
cd1428706e | |||
1e5249d436 | |||
0044075cbf | |||
9406b598f8 | |||
6f2be8486e | |||
baea6292f1 | |||
85144f4b8f | |||
99fd8ce94f | |||
23ca6ac8c3 | |||
5884b49518 | |||
1b767f9465 | |||
403167b366 | |||
e863ba459b | |||
eb552537c2 | |||
3fa95de8e6 | |||
a2cb806ffa | |||
014cba54af | |||
62142101fc | |||
7ea4f058c8 | |||
6ab8903c9b | |||
9af974cb10 | |||
b19c70c9d0 | |||
2f8a8b2718 | |||
bb6fb51ef9 | |||
b4a0b26dfd | |||
58803cad1d | |||
83be64dd74 | |||
b81fcd8417 | |||
63dd11fd2d | |||
98f663f200 | |||
cc7aa35627 | |||
fd7fb31338 | |||
25a28c8469 | |||
321208ba8a | |||
c4e59156c8 | |||
f79d48fdc0 | |||
2cd9f1fc76 | |||
6becf72420 | |||
640a241d38 | |||
fac941a156 | |||
84dba8a9fa | |||
78b2812593 | |||
6a016ed0b9 | |||
5d7f258973 | |||
3208eed610 | |||
1eb6c10286 | |||
38f83d19c9 | |||
22d385df32 | |||
31fc11ef1b | |||
f0217ba856 | |||
a4314f3e91 | |||
6ca93b54d7 | |||
a2f5efc9e6 | |||
635085f923 | |||
96c2da8f78 | |||
bde0b2bbd8 | |||
4ee6c80bc6 | |||
175d5ba969 | |||
0126e706cb | |||
f2e582622e | |||
afaedf9159 | |||
931c4df18a | |||
0222350a7f | |||
98370f6737 | |||
98dfb5fbb0 | |||
994c44f1ba | |||
2618558942 | |||
508771c347 | |||
2acc4e7e93 | |||
8841a57185 | |||
3d25084536 | |||
aa99e101b4 | |||
a30c3b0cbe | |||
bc504e7399 | |||
43e265dc92 | |||
f7813c0da6 | |||
4b869c0944 | |||
0ec2776705 | |||
6cf06d42d9 | |||
9f4aa9b038 | |||
926aa5bca8 | |||
77bf19acf3 | |||
98f9c23064 | |||
8f3ecefa8b | |||
f58b005155 | |||
40f31011e8 | |||
e8c6c8bb8f | |||
cfe4193974 | |||
833ab41eeb | |||
861272187a | |||
303f1a9c90 | |||
79608d0518 | |||
4551948460 | |||
48a152a219 | |||
af890f4df1 |
13
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: Dadido3 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
36
.github/workflows/build-release.yml
vendored
@ -8,19 +8,37 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build and release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
goarch: ["amd64"]
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.22
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: wangyoucao577/go-release-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
extra_files: init.lua LICENSE compatibility.xml mod.xml README.md AREAS.md images/coordinates.png images/example1.png images/example2.png images/scale32_base-layout.png images/scale32_main-world.png images/scale32_extended.png data files capture-b/capture.dll capture-b/README.md stitch/stitch.exe stitch/README.md
|
||||
- name: Build stitch tool
|
||||
run: go build -v -ldflags="-X 'main.versionString=${{ github.event.release.tag_name }}'" .
|
||||
working-directory: ./bin/stitch
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: ${{ matrix.goos }}
|
||||
CGO_ENABLED: 1
|
||||
|
||||
- name: Create distribution archive
|
||||
run: go run -v ./scripts/dist
|
||||
|
||||
- name: Upload binary to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
file: dist/dist.zip
|
||||
asset_name: noita-mapcap-${{ matrix.goos }}-${{ matrix.goarch }}.zip
|
||||
overwrite: true
|
||||
|
17
.github/workflows/build-test.yml
vendored
@ -9,17 +9,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
id: go
|
||||
go-version: ^1.22
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./bin/stitch
|
||||
- name: Build stitch tool
|
||||
run: go build -v .
|
||||
working-directory: ./bin/stitch
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./bin/stitch
|
||||
- name: Test stitch tool
|
||||
run: go test -v .
|
||||
working-directory: ./bin/stitch
|
||||
|
10
.gitignore
vendored
@ -103,7 +103,11 @@ $RECYCLE.BIN/
|
||||
|
||||
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||
|
||||
/libs/
|
||||
/output/
|
||||
/distribution/
|
||||
/bin/stitch/output.png
|
||||
/dist/
|
||||
/bin/stitch/*.png
|
||||
/bin/stitch/*.dzi
|
||||
/bin/stitch/*_files/
|
||||
/files/magic-numbers/generated.xml
|
||||
|
||||
/bin/stitch/captures/*
|
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "files/libraries/luanxml"]
|
||||
path = files/libraries/luanxml
|
||||
url = https://github.com/zatherz/luanxml
|
95
.vscode/settings.json
vendored
@ -1,30 +1,121 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"aabb",
|
||||
"acidflow",
|
||||
"appdata",
|
||||
"autosetup",
|
||||
"backbuffer",
|
||||
"basicfont",
|
||||
"bytecode",
|
||||
"cheggaaa",
|
||||
"Dadido",
|
||||
"dofile",
|
||||
"dont",
|
||||
"Downscales",
|
||||
"downscaling",
|
||||
"DPMM",
|
||||
"executables",
|
||||
"framebuffer",
|
||||
"framebuffers",
|
||||
"Fullscreen",
|
||||
"goarch",
|
||||
"gridify",
|
||||
"hacky",
|
||||
"hilbertify",
|
||||
"Hitbox",
|
||||
"ipairs",
|
||||
"kbinani",
|
||||
"Lanczos",
|
||||
"lann",
|
||||
"ldflags",
|
||||
"libwebp",
|
||||
"linearize",
|
||||
"longleg",
|
||||
"lowram",
|
||||
"luanxml",
|
||||
"manifoldco",
|
||||
"mapcap",
|
||||
"Metamethods",
|
||||
"metaobject",
|
||||
"Metatable",
|
||||
"nfnt",
|
||||
"Niccoli",
|
||||
"noita",
|
||||
"prerender",
|
||||
"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",
|
||||
"ymin"
|
||||
"ymin",
|
||||
"Zatherz"
|
||||
],
|
||||
"Lua.runtime.version": "LuaJIT",
|
||||
"Lua.format.defaultConfig": {
|
||||
"max_line_length": "512"
|
||||
},
|
||||
"Lua.workspace.ignoreSubmodules": false,
|
||||
"cSpell.enabledLanguageIds": [
|
||||
"asciidoc",
|
||||
"c",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"css",
|
||||
"elixir",
|
||||
"erlang",
|
||||
"git-commit",
|
||||
"go",
|
||||
"graphql",
|
||||
"handlebars",
|
||||
"haskell",
|
||||
"html",
|
||||
"jade",
|
||||
"java",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"json",
|
||||
"jsonc",
|
||||
"jupyter",
|
||||
"latex",
|
||||
"less",
|
||||
"markdown",
|
||||
"php",
|
||||
"plaintext",
|
||||
"python",
|
||||
"pug",
|
||||
"restructuredtext",
|
||||
"rust",
|
||||
"scala",
|
||||
"scss",
|
||||
"scminput",
|
||||
"swift",
|
||||
"text",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"yaml",
|
||||
"yml",
|
||||
"lua"
|
||||
]
|
||||
}
|
19
AREAS.md
@ -1,7 +1,11 @@
|
||||
# Capture areas
|
||||
|
||||
A list of available capture areas.
|
||||
Coordinates are in in-game "virtual" pixels.
|
||||
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.
|
||||
|
||||
The dimensions of the capture rectangle are exactly:
|
||||
@ -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`.
|
||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2022 David Vogel
|
||||
Copyright (c) 2019-2023 David Vogel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
264
README.md
@ -1,117 +1,223 @@
|
||||
# Noita MapCapture addon [](https://travis-ci.com/Dadido3/noita-mapcap)
|
||||
# Noita map capture addon
|
||||
|
||||
Addon that captures a Noita world and saves it as image.
|
||||
A mod for Noita that can capture images of the world and stitch them into one large image.
|
||||
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) (Warning: 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
|
||||
|
||||
- Windows Vista, ..., 10. (64 bit OS for stitching)
|
||||
- Windows Vista, ..., 10, 11. (64 bit OS for stitching)
|
||||
- A few GB of free drive space.
|
||||
- 4 or more GB of RAM for gigapixel images. (But it works with less as long as the software doesn't run out of virtual memory)
|
||||
- A processor.
|
||||
- Optionally a monitor, keyboard and mouse to interact with the mod/software.
|
||||
- A sound card to listen to music while it's grabbing screenshots.
|
||||
Capturing and stitching the "extended" map will take about 180 minutes (160 + 20).
|
||||
|
||||
## Installation
|
||||
|
||||
1. Have Noita installed.
|
||||
2. Download the [latest release of the mod from this link](https://github.com/Dadido3/noita-mapcap/releases/latest) (The `noita-mapcap-windows-amd64.zip`, not the source code)
|
||||
3. Unpack it into your mods folder, so that you get the following file structure `.../Noita/mods/noita-mapcap/mod.xml`.
|
||||
You can open the mods folder by clicking `Open mod folder` from within the Noita mod menu.
|
||||
4. Refresh the mod list.
|
||||
5. Enable the `MapCapture` mod.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Have Noita installed.
|
||||
2. Download the [latest release of the mod from this link](https://github.com/Dadido3/noita-mapcap/releases/latest) (The `Windows.x86.7z`, not the source)
|
||||
3. Unpack it into your mods folder, so that you get the following file structure `.../Noita/mods/noita-mapcap/mod.xml`.
|
||||
4. Set your resolution to 1280x720, and use the `Windowed` mode. (Not `Fullscreen (Windowed)`!) If you have to use a different resolution, see [Advanced stuff](#advanced-stuff).
|
||||
5. Enable the mod and restart Noita.
|
||||
6. In the game you should see text on screen.
|
||||
- Either press `>> Start capturing map around view <<` to capture in a spiral around your current view.
|
||||
- Or press any other option to capture [specific areas](AREAS.md).
|
||||
7. The screen will jump around, and the game will take screenshots automatically.
|
||||
- Screenshots are saved in `.../Noita/mods/noita-mapcap/output/`.
|
||||
- Don't move the game window outside of screen space. You can cover it with other windows, and continue using your PC.
|
||||
- Don't minimize the game window.
|
||||
- If you need to pause, use the ESC menu.
|
||||
- Also, make sure that the console window isn't selected, as you will end up with screenshots of the console instead of the game. You can select and use any other window while it's capturing screenshots, though.
|
||||
- Noita may crash in the process or show error messages. If you encounter an `ASSERT FAILED!` message click on `Ignore always`. If Noita crashes you can restart it, load your save and start capturing again. It will continue from where it stopped. More information/details about this can be found [here](https://github.com/Dadido3/noita-mapcap/issues/7#issuecomment-723571110).
|
||||
8. When you think you are done, close Noita.
|
||||
9. Start `.../Noita/mods/noita-mapcap/bin/stitch/stitch.exe`.
|
||||
- Use the default values to create a complete stitch.
|
||||
- It will take the screenshots from the `output` folder.
|
||||
10. The result will be saved as `.../Noita/mods/noita-mapcap/bin/stitch/output.png` if not defined otherwise.
|
||||
You can use the mod with either the regular Noita version, or the dev build `noita_dev.exe` that is located in the game installation directory.
|
||||
Using `noita_dev.exe` has the advantage that you can freeze pixel and rigid body simulations. Also, it uses a different location for its savegames, which means you don't have to worry about any save you may have left unfinished on the regular build.
|
||||
|
||||
## How to do a full map capture with minimal trouble
|
||||
Every setting you want or need to change can be found inside the `mod settings` tab of the game options.
|
||||
By default the mod settings will be set to useful values.
|
||||
An explanation for every setting can be found in the [Mod settings](#mod-settings) section.
|
||||
|
||||
For the best experience and result, `noita_dev.exe` should be used.
|
||||
This has the advantage of disabling the fog of war, and it can speed up the capturing process quite a bit, as a larger screen can be captured.
|
||||
Here is a step by step explanation how to do so:
|
||||
Once you have changed the mod settings to your liking you can start or resume a game with the mod enabled.
|
||||
You may see message boxes that suggest actions.
|
||||
This can happen if the mod detects game settings that do not align with what you have set in the mod settings.
|
||||
All you need to do is follow the given instructions, like:
|
||||
|
||||
1. Have the mod installed and enabled as described in [Usage](#usage).
|
||||

|
||||
|
||||
2. Change the following values inside of `.../Noita/mods/noita-mapcap/files/magic_numbers.xml` to
|
||||
>  Most of the changes this mod does to Noita are non permanent and will be gone after a restart or a new game, except:
|
||||
>
|
||||
> - Window, internal and virtual resolutions, if requested.
|
||||
> - Screen shake intensity, which will always be disabled.
|
||||
>
|
||||
> You can always *right* click  to reset the above mentioned settings back to Noita's default.
|
||||
|
||||
``` xml
|
||||
<MagicNumbers
|
||||
VIRTUAL_RESOLUTION_X="1024"
|
||||
VIRTUAL_RESOLUTION_Y="1024"
|
||||
...
|
||||
>
|
||||
```
|
||||
After all issues have been resolved you are free to start capturing.
|
||||
|
||||
3. Change the following values inside of `.../Noita/save_shared/config.xml` (Not the one in AppData!) to
|
||||
To the top left of the window are 3 buttons:
|
||||
|
||||
``` xml
|
||||
<Config
|
||||
...
|
||||
backbuffer_height="1024"
|
||||
backbuffer_width="1024"
|
||||
internal_size_h="1024"
|
||||
internal_size_w="1024"
|
||||
window_h="1024"
|
||||
window_w="1024"
|
||||
fullscreen="0"
|
||||
...
|
||||
>
|
||||
```
|
||||
- / Starts/Stops the capturing process based on your mod settings.
|
||||
You can always restart a capture, and it will resume where it was stopped.
|
||||
|
||||
If that file doesn't exist do step 5, and come back here, and continue from step 3.
|
||||
-  Reveals the output directory in your file browser.
|
||||
This will contain raw screenshots and other recorded data that later can be stitched.
|
||||
|
||||
4. Patch your `.../Noita/noita_dev.exe` with [Large Address Aware](https://www.techpowerup.com/forums/threads/large-address-aware.112556/) or a similar tool.
|
||||
This is optional, but it prevents crashes from Noita running out of memory.
|
||||
-  Reveals the stitching tool directory in your file browser.
|
||||
|
||||
5. Start `.../Noita/noita_dev.exe`.
|
||||
To stitch the final result, click  to open the directory of the stitching tool.
|
||||
Start `stitch.exe` and proceed with the default values.
|
||||
After a few minutes the file `output.png` will be created.
|
||||
|
||||
6. When the game is loaded (When you can control your character):
|
||||
- Press `F5`, `F8` and `F12` (In that order).
|
||||
>  See [stitcher/README.md](bin/stitch/README.md) for more information about all stitcher parameters.
|
||||
|
||||
7. Press the `>> Start capturing extended map <<` button.
|
||||
## Mod settings
|
||||
|
||||
8. Wait a few hours until it's complete.
|
||||
>  Use *right* mouse button to reset any mod setting to their default.
|
||||
|
||||
9. Stitch the image as described in [Usage](#usage).
|
||||
- `Mode`: Defines what the mod captures, and how it captures it:
|
||||
|
||||
## Advanced stuff
|
||||
- `Live`: The mod will capture as you play along.
|
||||
The end result is a map with the path of your run.
|
||||
|
||||
If you use `noita_dev.exe`, you can enable the debug mode by pressing `F5`. Once in debug mode, you can use `F8` to toggle shaders (Includes fog of war), and you can use `F12` to disable the UI. There are some more options in the `F7` and `Shift + F7` menu.
|
||||
- `Area`: Captures a defined rectangle of the world.
|
||||
You can either use [predefined areas](AREAS.md), or enter custom coordinates.
|
||||
|
||||
You can capture in a different resolution if you want or need to. If you do so, you have to adjust some values inside of the mod.
|
||||
- `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 following two equations have to be true:
|
||||
- `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.
|
||||
|
||||
$$\begin{align*}
|
||||
\text{CAPTURE\\_PIXEL\\_SIZE} &= \frac{\text{SCREEN\\_RESOLUTION\\_X}}{\text{VIRTUAL\\_RESOLUTION\\_X}}\\
|
||||
\text{CAPTURE\\_PIXEL\\_SIZE} &= \frac{\text{SCREEN\\_RESOLUTION\\_Y}}{\text{VIRTUAL\\_RESOLUTION\\_Y}}
|
||||
\end{align*}$$
|
||||
### Advanced mod settings
|
||||
|
||||
- Where `CAPTURE_PIXEL_SIZE` can be found inside `.../Noita/mods/noita-mapcap/files/capture.lua`
|
||||
- `VIRTUAL_RESOLUTION_*` can be found inside `.../Noita/mods/noita-mapcap/files/magic_numbers.xml`
|
||||
- and `SCREEN_RESOLUTION_*` is the screen resolution you have set up in noita.
|
||||
- `World seed`: If non empty, this will set the next new game to this seed.
|
||||
|
||||
You can also change how much the tiles overlap by adjusting the `CAPTURE_GRID_SIZE` in `.../Noita/mods/noita-mapcap/files/capture.lua`. If you increase the grid size, you can capture more area per time. But on the other hand the stitcher may not be able to remove artifacts if the tiles don't overlap enough.
|
||||
- `Grid size`: The amount of world pixels the viewport will move between the screenshots.
|
||||
|
||||
The rectangles for the different capture modes are defined in `.../Noita/mods/noita-mapcap/files/capture.lua`.
|
||||
- `Pixel scale`: The resulting pixel size of the screenshots.
|
||||
If greater than 0, all screenshots will be rescaled to have the given pixel size.
|
||||
|
||||
As the resulting stitched image is really 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.
|
||||
- `Use custom resolution`: If enabled, the mod will change the game resolutions to the given values.
|
||||
|
||||
- `Capture interval`: Interval between screen captures, when in live mode.
|
||||
|
||||
- `Min. capture distance`: The distance in world pixels the viewport has to move to allow another screenshot.
|
||||
Only used in live mode.
|
||||
|
||||
- `Max. capture distance`: The distance in world pixels the viewport has to move to force another screenshot.
|
||||
Only used in live mode.
|
||||
|
||||
- `Capture entities`: If enabled, the mod will capture all entities, their children, parameters and components and write them into `output/entities.json`.
|
||||
>  This can slow down capturing a bit, it may also make Noita more unstable.
|
||||
|
||||
- `Disable parallax background`: Will replace the world background with black pixels.
|
||||
|
||||
- `Disable UI`: Will disable inventory UI.
|
||||
But the UI can still appear if triggered by mouse wheel or something similar.
|
||||
|
||||
- `Disable pixel and entity physics`: Will disable/stop pixel and rigid body simulation.
|
||||
Only works in dev build.
|
||||
|
||||
- `Disable post FX`: Disables most postprocessing effects like:
|
||||
- Dithering
|
||||
- Refraction
|
||||
- Lighting
|
||||
- Fog of war
|
||||
- Glow
|
||||
- Gamma correction
|
||||
|
||||
- `Disable shaders, GUI and AI`: Also disables all postprocessing, any in-game UI and will freeze all mobs.
|
||||
Only works in dev build.
|
||||
|
||||
- `Disable entity logic`: Will modify all encountered entities:
|
||||
- Disables AI
|
||||
- Disables falling
|
||||
- Disables hovering and rotation animations
|
||||
- Reduces explosions
|
||||
|
||||
>  This can slow down capturing a bit, it may also make Noita more unstable.
|
||||
|
||||
### Example settings
|
||||
|
||||
Use these settings if you want to capture your in-game action.
|
||||
The sliders are at their default values:
|
||||
|
||||

|
||||
|
||||
Use these settings to capture the [base layout](AREAS.md#base-layout) with the least amount of glitches and artifacts.
|
||||
The sliders are at their default values:
|
||||
|
||||

|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Noita crashes a lot
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
More information/details about this can be found [here](https://github.com/Dadido3/noita-mapcap/issues/7#issuecomment-723571110).
|
||||
|
||||
### I get "ASSERT FAILED!" messages
|
||||
|
||||
These can't really be prevented.
|
||||
All you can do is to click `Ignore always`.
|
||||
|
||||
Alternatively you can run the same capture in the regular Noita (non dev build), which has these messages disabled.
|
||||
With the exception that you can't disable the pixel and rigid body simulations, the mod works just as well as in the dev build.
|
||||
|
||||
### The mod messed up my game
|
||||
|
||||
Custom resolution settings are retained even if you restart Noita or start a new game.
|
||||
In the worst case they will cause the game world to not render correctly, or they will make your game only use a fraction of the window.
|
||||
|
||||
To reset any permanent settings that may have been set by the mod:
|
||||
|
||||
1. Enable the mod.
|
||||
2. Start a new game.
|
||||
3. *Right* click  and follow instructions.
|
||||
|
||||
>  If you have changed any resolutions in your game's `config.xml`, you may have to re-apply these changes.
|
||||
> This also applies if you use any mods that makes Noita work on ultra-wide screens.
|
||||
> For these mods to work again after a reset, you need to go through their installation steps again.
|
||||
|
||||
Alternatively, you can reset **all** game settings by deleting:
|
||||
|
||||
- `"%appdata%\..\LocalLow\Nolla_Games_Noita\save_shared\config.xml"` for the regular Noita.
|
||||
- `"...\Noita\save_shared\config.xml"` for the dev build.
|
||||
|
||||
### The objects in the stitched image are blurry
|
||||
|
||||
The stitcher uses median blending to remove any single frame artifacts and to correct for not rendered chunks.
|
||||
This will cause fast moving objects to completely disappear, and slow moving objects to get blurry.
|
||||
|
||||
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.
|
||||
|
||||
## 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] 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
|
||||
|
@ -1,8 +1,10 @@
|
||||
; Copyright (c) 2019-2020 David Vogel
|
||||
; Copyright (c) 2019-2024 David Vogel
|
||||
;
|
||||
; This software is released under the MIT License.
|
||||
; https://opensource.org/licenses/MIT
|
||||
|
||||
EnableExplicit
|
||||
|
||||
UsePNGImageEncoder()
|
||||
|
||||
Declare Worker(*Dummy)
|
||||
@ -11,46 +13,47 @@ Structure QueueElement
|
||||
img.i
|
||||
x.i
|
||||
y.i
|
||||
sx.i
|
||||
sy.i
|
||||
EndStructure
|
||||
|
||||
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
|
||||
Procedure EnumWindowsProc(hWnd.l, *lParam.Long)
|
||||
Protected lpProc.l
|
||||
GetWindowThreadProcessId_(hWnd, @lpProc)
|
||||
If *lParam\l = lpProc ; Check if current window's processID matches
|
||||
*lParam\l = hWnd ; Replace processID in the param With the hwnd As result
|
||||
ProcedureReturn #False ; Return false to stop iterating
|
||||
Structure GLViewportDims
|
||||
x.i
|
||||
y.i
|
||||
width.i
|
||||
height.i
|
||||
EndStructure
|
||||
|
||||
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
|
||||
|
||||
glGetIntegerv_(#GL_VIEWPORT, *dims)
|
||||
|
||||
ProcedureReturn #True
|
||||
EndProcedure
|
||||
|
||||
; 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
|
||||
; Returns the size of the main OpenGL rendering output as a windows RECT.
|
||||
ProcedureDLL GetRect(*rect.RECT)
|
||||
Protected hWnd.l = GetProcHwnd()
|
||||
If Not hWnd
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
If Not *rect
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
GetClientRect_(hWnd, *rect)
|
||||
Protected dims.GLViewportDims
|
||||
glGetIntegerv_(#GL_VIEWPORT, dims)
|
||||
|
||||
; A RECT consists basically of two POINT structures
|
||||
ClientToScreen_(hWnd, @*rect\left)
|
||||
ClientToScreen_(hWnd, @*rect\Right)
|
||||
*rect\left = dims\x
|
||||
*rect\top = dims\y
|
||||
*rect\right = dims\x + dims\width
|
||||
*rect\bottom = dims\y + dims\height
|
||||
|
||||
ProcedureReturn #True
|
||||
EndProcedure
|
||||
@ -62,13 +65,16 @@ ProcedureDLL AttachProcess(Instance)
|
||||
|
||||
CreateDirectory("mods/noita-mapcap/output/")
|
||||
|
||||
For i = 1 To 4
|
||||
CreateThread(@Worker(), #Null)
|
||||
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(*Dummy)
|
||||
Protected img, x, y
|
||||
Procedure Worker(*workerInfo.WorkerInfo)
|
||||
Protected img, x, y, sx, sy
|
||||
|
||||
Repeat
|
||||
WaitSemaphore(Semaphore)
|
||||
@ -78,67 +84,84 @@ Procedure Worker(*Dummy)
|
||||
img = Queue()\img
|
||||
x = Queue()\x
|
||||
y = Queue()\y
|
||||
sx = Queue()\sx
|
||||
sy = Queue()\sy
|
||||
DeleteElement(Queue())
|
||||
UnlockMutex(Mutex)
|
||||
|
||||
SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG)
|
||||
;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test
|
||||
If sx > 0 And sy > 0
|
||||
ResizeImage(img, sx, sy)
|
||||
EndIf
|
||||
|
||||
; Save image temporary, and only move it once it's fully exported.
|
||||
; This prevents images getting corrupted when the main process crashes.
|
||||
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)
|
||||
ForEver
|
||||
EndProcedure
|
||||
|
||||
ProcedureDLL Capture(px.i, py.i)
|
||||
Protected hWnd.l = GetProcHwnd()
|
||||
If Not hWnd
|
||||
; 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.
|
||||
; x and y defines the top left position of the captured rectangle in scaled world coordinates. The scale depends on the window to world pixel ratio.
|
||||
; sx and sy defines the final dimensions that the screenshot will be resized to. No resize will happen if set to 0.
|
||||
ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
|
||||
Protected viewportRect.RECT
|
||||
If Not GetRect(@viewportRect)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
Protected rect.RECT
|
||||
If Not GetRect(@rect)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
Protected imageID, hDC, *pixelBuffer
|
||||
|
||||
imageID = CreateImage(#PB_Any, rect\right-rect\left, rect\bottom-rect\top)
|
||||
; 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
|
||||
If *capRect\right < *capRect\left : *capRect\right = *capRect\left : 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
|
||||
Protected capHeight = *capRect\bottom - *capRect\top
|
||||
|
||||
imageID = CreateImage(#PB_Any, capWidth, capHeight)
|
||||
If Not imageID
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
; Get DC of whole screen
|
||||
windowDC = GetDC_(hWnd)
|
||||
If Not windowDC
|
||||
FreeImage(imageID)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
hDC = StartDrawing(ImageOutput(imageID))
|
||||
If Not hDC
|
||||
ReleaseDC_(hWnd, windowDC)
|
||||
FreeImage(imageID)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
If Not BitBlt_(hDC, 0, 0, rect\right-rect\left, rect\bottom-rect\top, windowDC, 0, 0, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes.
|
||||
|
||||
*pixelBuffer = DrawingBuffer()
|
||||
glReadPixels_(*capRect\left, *capRect\top, capWidth, capHeight, #GL_BGR_EXT, #GL_UNSIGNED_BYTE, *pixelBuffer)
|
||||
If glGetError_() <> #GL_NO_ERROR
|
||||
StopDrawing()
|
||||
ReleaseDC_(hWnd, windowDC)
|
||||
FreeImage(imageID)
|
||||
ProcedureReturn #False
|
||||
EndIf
|
||||
|
||||
StopDrawing()
|
||||
|
||||
ReleaseDC_(hWnd, windowDC)
|
||||
|
||||
LockMutex(Mutex)
|
||||
; Check if the queue has too many elements, if so, wait. (Simulate go's channels)
|
||||
While ListSize(Queue()) > 0
|
||||
; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
|
||||
While ListSize(Queue()) > 1
|
||||
UnlockMutex(Mutex)
|
||||
Delay(10)
|
||||
Delay(1)
|
||||
LockMutex(Mutex)
|
||||
Wend
|
||||
LastElement(Queue())
|
||||
AddElement(Queue())
|
||||
Queue()\img = imageID
|
||||
Queue()\x = px
|
||||
Queue()\y = py
|
||||
Queue()\x = x
|
||||
Queue()\y = y
|
||||
Queue()\sx = sx
|
||||
Queue()\sy = sy
|
||||
UnlockMutex(Mutex)
|
||||
|
||||
SignalSemaphore(Semaphore)
|
||||
@ -153,12 +176,14 @@ EndProcedure
|
||||
;Capture(123, 123)
|
||||
;Delay(1000)
|
||||
|
||||
; IDE Options = PureBasic 5.72 (Windows - x64)
|
||||
; IDE Options = PureBasic 6.04 LTS (Windows - x64)
|
||||
; ExecutableFormat = Shared dll
|
||||
; CursorPosition = 90
|
||||
; FirstLine = 77
|
||||
; Folding = --
|
||||
; CursorPosition = 99
|
||||
; FirstLine = 72
|
||||
; Folding = -
|
||||
; Optimizer
|
||||
; EnableThread
|
||||
; EnableXP
|
||||
; Executable = capture.dll
|
||||
; Compiler = PureBasic 5.71 LTS (Windows - x86)
|
||||
; DisableDebugger
|
||||
; Compiler = PureBasic 6.04 LTS - C Backend (Windows - x86)
|
@ -1 +0,0 @@
|
||||
go tool pprof -http=: ./stitch.exe cpu.prof
|
@ -17,7 +17,7 @@ The source images need to contain their coordinates in the filename, as this pro
|
||||
|
||||
example list of files:
|
||||
|
||||
``` Shell Session
|
||||
``` Text
|
||||
0,0.png
|
||||
512,0.png
|
||||
-512,0.png
|
||||
@ -29,11 +29,27 @@ example list of files:
|
||||
- Either run the program and follow the interactive prompt.
|
||||
- Or run the program with parameters:
|
||||
- `divide int`
|
||||
A downscaling factor. 2 will produce an image with half the side lengths. (default 1)
|
||||
A downscaling factor. 2 will produce an image with half the side lengths. Defaults to 1.
|
||||
- `blend-tile-limit int`
|
||||
Limits median blending to the n newest tiles by file modification time.
|
||||
If set to 0, all available tiles will be median blended.
|
||||
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. (default "..\\..\\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`
|
||||
The path to the player-path.json file. This contains the tracked path of the player. Defaults to "./../../output/player-path.json".
|
||||
- `output string`
|
||||
The path and filename of the resulting stitched image. (default "output.png")
|
||||
The path and filename of the resulting stitched image. Defaults to "output.png".
|
||||
Supported formats/file extensions: `.png`, `.webp`, `.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`
|
||||
Right bound of the output rectangle. This coordinate is not included in the output.
|
||||
- `xmin int`
|
||||
@ -42,10 +58,6 @@ example list of files:
|
||||
Lower bound of the output rectangle. This coordinate is not included in the output.
|
||||
- `ymin int`
|
||||
Upper bound of the output rectangle. This coordinate is included in the output.
|
||||
- `prerender`
|
||||
Pre renders the image in RAM before saving. Can speed things up if you have enough RAM.
|
||||
- `cleanup float`
|
||||
Enables cleanup mode with the given float as threshold. This will **DELETE** images from the input folder; no stitching will be done in this mode. A good value to start with is `0.999`, which deletes images where the sum of the min-max difference of each sub-pixel overlapping with other images is less than 99.9%% of the maximum possible sum of pixel differences.
|
||||
|
||||
To output the 100x100 area that is centered at the origin use:
|
||||
|
||||
@ -53,13 +65,13 @@ To output the 100x100 area that is centered at the origin use:
|
||||
./stitch -divide 1 -xmin -50 -xmax 50 -ymin -50 -ymax 50
|
||||
```
|
||||
|
||||
To remove images that would cause artifacts (You should recapture the deleted images afterwards):
|
||||
To output a [Deep Zoom Image (DZI)](https://en.wikipedia.org/wiki/Deep_Zoom), which can be used with [OpenSeadragon](https://openseadragon.github.io/examples/tilesource-dzi/), use:
|
||||
|
||||
``` Shell Session
|
||||
./stitch -cleanup 0.999
|
||||
./stitch -output capture.dzi
|
||||
```
|
||||
|
||||
To enter the parameters inside of the program:
|
||||
To start the program interactively:
|
||||
|
||||
``` Shell Session
|
||||
./stitch
|
||||
|
164
bin/stitch/blend-methods.go
Normal file
@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2022-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// BlendMethodMedian takes the given tiles and median blends them into destImage.
|
||||
type BlendMethodMedian struct {
|
||||
BlendTileLimit int // If larger than 0, limits median blending to the n newest tiles by file modification time.
|
||||
}
|
||||
|
||||
// Draw implements the StitchedImageBlendMethod interface.
|
||||
func (b BlendMethodMedian) Draw(tiles []*ImageTile, destImage *image.RGBA) {
|
||||
bounds := destImage.Bounds()
|
||||
|
||||
if b.BlendTileLimit > 0 {
|
||||
// Sort tiles by date.
|
||||
sort.Slice(tiles, func(i, j int) bool { return tiles[i].modTime.After(tiles[j].modTime) })
|
||||
}
|
||||
|
||||
// List of images corresponding with every tile.
|
||||
// Can contain empty/nil entries for images that failed to load.
|
||||
images := []*image.RGBA{}
|
||||
for _, tile := range tiles {
|
||||
images = append(images, tile.GetImage())
|
||||
}
|
||||
|
||||
// Create arrays to be reused every pixel.
|
||||
rListEmpty, gListEmpty, bListEmpty := make([]uint8, 0, len(tiles)), make([]uint8, 0, len(tiles)), make([]uint8, 0, len(tiles))
|
||||
|
||||
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
||||
point := image.Point{ix, iy}
|
||||
count := 0
|
||||
|
||||
// Iterate through all images and create a list of colors.
|
||||
for _, img := range images {
|
||||
if img != nil {
|
||||
if point.In(img.Bounds()) {
|
||||
col := img.RGBAAt(point.X, point.Y)
|
||||
rList, gList, bList = append(rList, col.R), append(gList, col.G), append(bList, col.B)
|
||||
count++
|
||||
// Limit number of tiles to median blend.
|
||||
// Will be ignored if the blend tile limit is 0.
|
||||
if count == b.BlendTileLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch count {
|
||||
case 0: // If there were no images to get data from, ignore the pixel.
|
||||
continue
|
||||
|
||||
case 1: // Only a single tile for this pixel.
|
||||
r, g, b := uint8(rList[0]), uint8(gList[0]), uint8(bList[0])
|
||||
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
||||
|
||||
default: // Multiple overlapping tiles, median blend them.
|
||||
var r, g, b uint8
|
||||
switch count % 2 {
|
||||
case 0: // Even.
|
||||
r = uint8((int(QuickSelectUInt8(rList, count/2-1)) + int(QuickSelectUInt8(rList, count/2))) / 2)
|
||||
g = uint8((int(QuickSelectUInt8(gList, count/2-1)) + int(QuickSelectUInt8(gList, count/2))) / 2)
|
||||
b = uint8((int(QuickSelectUInt8(bList, count/2-1)) + int(QuickSelectUInt8(bList, count/2))) / 2)
|
||||
default: // Odd.
|
||||
r = QuickSelectUInt8(rList, count/2)
|
||||
g = QuickSelectUInt8(gList, count/2)
|
||||
b = QuickSelectUInt8(bList, count/2)
|
||||
}
|
||||
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BlendMethodVoronoi maps every pixel to the tile with the closest center point distance.
|
||||
// The result is basically a Voronoi partitioning.
|
||||
type BlendMethodVoronoi struct {
|
||||
BlendTileLimit int // If larger than 0, limits blending to the n newest tiles by file modification time.
|
||||
}
|
||||
|
||||
// Draw implements the StitchedImageBlendMethod interface.
|
||||
func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
|
||||
bounds := destImage.Bounds()
|
||||
|
||||
if b.BlendTileLimit > 0 {
|
||||
// Sort tiles by date.
|
||||
sort.Slice(tiles, func(i, j int) bool { return tiles[i].modTime.After(tiles[j].modTime) })
|
||||
}
|
||||
|
||||
// List of images corresponding to the "tiles" list.
|
||||
// Can contain empty/nil entries for images that failed to load.
|
||||
images := []*image.RGBA{}
|
||||
for _, tile := range tiles {
|
||||
images = append(images, tile.GetImage())
|
||||
}
|
||||
|
||||
// Create color variables reused every pixel.
|
||||
var col color.RGBA
|
||||
var centerDistSqrMin int
|
||||
|
||||
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||
point := image.Point{ix, iy}
|
||||
count := 0
|
||||
centerDistSqrMin = math.MaxInt
|
||||
|
||||
// Iterate through all images and create a list of colors.
|
||||
for _, img := range images {
|
||||
if img != nil {
|
||||
if point.In(img.Bounds()) {
|
||||
center := img.Bounds().Min.Add(img.Bounds().Max).Div(2)
|
||||
centerDiff := point.Sub(center)
|
||||
distSqr := centerDiff.X*centerDiff.X + centerDiff.Y*centerDiff.Y
|
||||
if centerDistSqrMin > distSqr {
|
||||
centerDistSqrMin = distSqr
|
||||
col = img.RGBAAt(point.X, point.Y)
|
||||
}
|
||||
count++
|
||||
// Limit number of tiles to blend.
|
||||
// Will be ignored if the blend tile limit is 0.
|
||||
if count == b.BlendTileLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no images to get data from, ignore the pixel.
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
col.A = 255
|
||||
destImage.SetRGBA(ix, iy, col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
208
bin/stitch/dzi.go
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
type DZI struct {
|
||||
stitchedImage *StitchedImage
|
||||
|
||||
fileExtension string
|
||||
|
||||
tileSize int // The (maximum) width and height of a tile in pixels, not including the overlap.
|
||||
overlap int // The amount of additional pixels on every side of every tile. The real (max) width/height of an image is `2*overlap + tileSize`.
|
||||
|
||||
maxZoomLevel int // The maximum zoom level that is needed.
|
||||
}
|
||||
|
||||
// NewDZI creates a new DZI from the given StitchedImages.
|
||||
//
|
||||
// dziTileSize and dziOverlap define the size and overlap of the resulting DZI tiles.
|
||||
func NewDZI(stitchedImage *StitchedImage, dziTileSize, dziOverlap int) DZI {
|
||||
dzi := DZI{
|
||||
stitchedImage: stitchedImage,
|
||||
|
||||
fileExtension: ".webp",
|
||||
|
||||
overlap: dziOverlap,
|
||||
tileSize: dziTileSize,
|
||||
}
|
||||
|
||||
width, height := stitchedImage.bounds.Dx(), stitchedImage.bounds.Dy()
|
||||
|
||||
// Calculate max zoom level and stuff.
|
||||
neededLength := max(width, height)
|
||||
var sideLength int = 1
|
||||
var level int
|
||||
for sideLength < neededLength {
|
||||
level += 1
|
||||
sideLength *= 2
|
||||
}
|
||||
dzi.maxZoomLevel = level
|
||||
//dzi.maxZoomLevelLength = sideLength
|
||||
|
||||
return dzi
|
||||
}
|
||||
|
||||
// ExportDZIDescriptor exports the descriptive JSON file at the given path.
|
||||
func (d DZI) ExportDZIDescriptor(outputPath string) error {
|
||||
log.Printf("Creating DZI descriptor %q.", outputPath)
|
||||
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Prepare data that describes the layout of the image files.
|
||||
var dziDescriptor struct {
|
||||
Image struct {
|
||||
XMLNS string `json:"xmlns"`
|
||||
Format string
|
||||
Overlap string
|
||||
TileSize string
|
||||
Size struct {
|
||||
Width string
|
||||
Height string
|
||||
}
|
||||
TopLeft struct {
|
||||
X string
|
||||
Y string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dziDescriptor.Image.XMLNS = "http://schemas.microsoft.com/deepzoom/2008"
|
||||
dziDescriptor.Image.Format = "webp"
|
||||
dziDescriptor.Image.Overlap = strconv.Itoa(d.overlap)
|
||||
dziDescriptor.Image.TileSize = strconv.Itoa(d.tileSize)
|
||||
dziDescriptor.Image.Size.Width = strconv.Itoa(d.stitchedImage.bounds.Dx())
|
||||
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)
|
||||
return jsonEnc.Encode(dziDescriptor)
|
||||
}
|
||||
|
||||
// ExportDZITiles exports the single image tiles for every zoom level.
|
||||
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.
|
||||
|
||||
// The current stitched image we are working with.
|
||||
stitchedImage := d.stitchedImage
|
||||
|
||||
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
|
||||
|
||||
levelBasePath := filepath.Join(outputDir, fmt.Sprintf("%d", zoomLevel))
|
||||
if err := os.MkdirAll(levelBasePath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create zoom level base directory %q: %w", levelBasePath, err)
|
||||
}
|
||||
|
||||
// Store list of tiles, so that we can reuse them in the next step for the smaller zoom level.
|
||||
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)
|
||||
rect = rect.Add(stitchedImage.bounds.Min)
|
||||
rect = rect.Inset(-d.overlap)
|
||||
img := stitchedImage.SubStitchedImage(rect)
|
||||
filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension))
|
||||
|
||||
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(),
|
||||
scaleDivider: scaleDivider,
|
||||
image: image.Rect(DivideFloor(img.Bounds().Min.X, scaleDivider), DivideFloor(img.Bounds().Min.Y, scaleDivider), DivideCeil(img.Bounds().Max.X, scaleDivider), DivideCeil(img.Bounds().Max.Y, scaleDivider)),
|
||||
imageMutex: &sync.RWMutex{},
|
||||
invalidationChan: make(chan struct{}, 1),
|
||||
timeoutChan: make(chan struct{}, 1),
|
||||
})
|
||||
}
|
||||
}
|
||||
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(), BlendMethodFast{}, 128, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
63
bin/stitch/entities.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image"
|
||||
"os"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
type Entities []Entity
|
||||
|
||||
func LoadEntities(path string) (Entities, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Entities
|
||||
|
||||
jsonDec := json.NewDecoder(file)
|
||||
if err := jsonDec.Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Draw implements the StitchedImageOverlay interface.
|
||||
func (e Entities) Draw(destImage *image.RGBA) {
|
||||
destRect := destImage.Bounds()
|
||||
|
||||
// Same as destImage, but top left is translated to (0, 0).
|
||||
originImage := destImage.SubImage(destRect).(*image.RGBA)
|
||||
originImage.Rect = originImage.Rect.Sub(destRect.Min)
|
||||
|
||||
c := canvas.New(float64(destRect.Dx()), float64(destRect.Dy()))
|
||||
ctx := canvas.NewContext(c)
|
||||
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||
ctx.SetCoordRect(canvas.Rect{X: -float64(destRect.Min.X), Y: -float64(destRect.Min.Y), W: float64(destRect.Dx()), H: float64(destRect.Dy())}, float64(destRect.Dx()), float64(destRect.Dy()))
|
||||
|
||||
// Set drawing style.
|
||||
ctx.Style = playerPathDisplayStyle
|
||||
|
||||
for _, entity := range e {
|
||||
// Check if entity origin is near or around the current image rectangle.
|
||||
entityOrigin := image.Point{int(entity.Transform.X), int(entity.Transform.Y)}
|
||||
if entityOrigin.In(destRect.Inset(-512)) {
|
||||
entity.Draw(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
||||
r := rasterizer.FromImage(originImage, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
||||
c.RenderTo(r)
|
||||
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||
}
|
220
bin/stitch/entity.go
Normal file
@ -0,0 +1,220 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
//var entityDisplayFontFamily = canvas.NewFontFamily("times")
|
||||
//var entityDisplayFontFace *canvas.FontFace
|
||||
|
||||
var entityDisplayAreaDamageStyle = canvas.Style{
|
||||
Fill: canvas.Paint{Color: color.RGBA{100, 0, 0, 100}},
|
||||
Stroke: canvas.Paint{},
|
||||
StrokeWidth: 1.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
var entityDisplayMaterialAreaCheckerStyle = canvas.Style{
|
||||
Fill: canvas.Paint{Color: color.RGBA{0, 0, 127, 127}},
|
||||
Stroke: canvas.Paint{},
|
||||
StrokeWidth: 1.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
var entityDisplayTeleportStyle = canvas.Style{
|
||||
Fill: canvas.Paint{Color: color.RGBA{0, 127, 0, 127}},
|
||||
Stroke: canvas.Paint{},
|
||||
StrokeWidth: 1.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
var entityDisplayHitBoxStyle = canvas.Style{
|
||||
Fill: canvas.Paint{Color: color.RGBA{64, 64, 0, 64}},
|
||||
Stroke: canvas.Paint{Color: color.RGBA{0, 0, 0, 64}},
|
||||
StrokeWidth: 1.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
var entityDisplayCollisionTriggerStyle = canvas.Style{
|
||||
Fill: canvas.Paint{Color: color.RGBA{0, 64, 64, 64}},
|
||||
Stroke: canvas.Paint{Color: color.RGBA{0, 0, 0, 64}},
|
||||
StrokeWidth: 1.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
func init() {
|
||||
//fontName := "NimbusRoman-Regular"
|
||||
|
||||
//if err := entityDisplayFontFamily.LoadLocalFont(fontName, canvas.FontRegular); err != nil {
|
||||
// log.Printf("Couldn't load font %q: %v", fontName, err)
|
||||
//}
|
||||
|
||||
//entityDisplayFontFace = entityDisplayFontFamily.Face(48.0, canvas.White, canvas.FontRegular, canvas.FontNormal)
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
Filename string `json:"filename"`
|
||||
Transform EntityTransform `json:"transform"`
|
||||
Children []Entity `json:"children"`
|
||||
Components []Component `json:"components"`
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type EntityTransform struct {
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
ScaleX float32 `json:"scaleX"`
|
||||
ScaleY float32 `json:"scaleY"`
|
||||
Rotation float32 `json:"rotation"`
|
||||
}
|
||||
|
||||
type Component struct {
|
||||
TypeName string `json:"typeName"`
|
||||
Members map[string]any `json:"members"`
|
||||
}
|
||||
|
||||
func (e Entity) Draw(c *canvas.Context) {
|
||||
x, y := float64(e.Transform.X), float64(e.Transform.Y)
|
||||
|
||||
for _, component := range e.Components {
|
||||
switch component.TypeName {
|
||||
case "AreaDamageComponent": // Area damage like in cursed rock.
|
||||
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
||||
if member, ok := component.Members["aabb_min"]; ok {
|
||||
if aabbMin, ok := member.([]any); ok && len(aabbMin) == 2 {
|
||||
aabbMinX, _ = aabbMin[0].(float64)
|
||||
aabbMinY, _ = aabbMin[1].(float64)
|
||||
}
|
||||
}
|
||||
if member, ok := component.Members["aabb_max"]; ok {
|
||||
if aabbMax, ok := member.([]any); ok && len(aabbMax) == 2 {
|
||||
aabbMaxX, _ = aabbMax[0].(float64)
|
||||
aabbMaxY, _ = aabbMax[1].(float64)
|
||||
}
|
||||
}
|
||||
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
||||
c.Style = entityDisplayAreaDamageStyle
|
||||
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
||||
}
|
||||
if member, ok := component.Members["circle_radius"]; ok {
|
||||
if radius, ok := member.(float64); ok && radius > 0 {
|
||||
// Theoretically we need to clip the damage area to the intersection of the AABB and the circle, but meh.
|
||||
// TODO: Clip the area to the intersection of the box and the circle
|
||||
cx, cy := (aabbMinX+aabbMaxX)/2, (aabbMinY+aabbMaxY)/2
|
||||
c.Style = entityDisplayAreaDamageStyle
|
||||
c.DrawPath(x+cx, y+cy, canvas.Circle(radius))
|
||||
}
|
||||
}
|
||||
|
||||
case "MaterialAreaCheckerComponent": // Checks for materials in the given AABB.
|
||||
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
||||
if member, ok := component.Members["area_aabb"]; ok {
|
||||
if aabb, ok := member.([]any); ok && len(aabb) == 4 {
|
||||
aabbMinX, _ = aabb[0].(float64)
|
||||
aabbMinY, _ = aabb[1].(float64)
|
||||
aabbMaxX, _ = aabb[2].(float64)
|
||||
aabbMaxY, _ = aabb[3].(float64)
|
||||
}
|
||||
}
|
||||
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
||||
c.Style = entityDisplayMaterialAreaCheckerStyle
|
||||
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
||||
}
|
||||
|
||||
case "TeleportComponent":
|
||||
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
||||
if member, ok := component.Members["source_location_camera_aabb"]; ok {
|
||||
if aabb, ok := member.([]any); ok && len(aabb) == 4 {
|
||||
aabbMinX, _ = aabb[0].(float64)
|
||||
aabbMinY, _ = aabb[1].(float64)
|
||||
aabbMaxX, _ = aabb[2].(float64)
|
||||
aabbMaxY, _ = aabb[3].(float64)
|
||||
}
|
||||
}
|
||||
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
||||
c.Style = entityDisplayTeleportStyle
|
||||
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
||||
}
|
||||
|
||||
case "HitboxComponent": // General hit box component.
|
||||
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
||||
if member, ok := component.Members["aabb_min_x"]; ok {
|
||||
aabbMinX, _ = member.(float64)
|
||||
}
|
||||
if member, ok := component.Members["aabb_min_y"]; ok {
|
||||
aabbMinY, _ = member.(float64)
|
||||
}
|
||||
if member, ok := component.Members["aabb_max_x"]; ok {
|
||||
aabbMaxX, _ = member.(float64)
|
||||
}
|
||||
if member, ok := component.Members["aabb_max_y"]; ok {
|
||||
aabbMaxY, _ = member.(float64)
|
||||
}
|
||||
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
||||
c.Style = entityDisplayHitBoxStyle
|
||||
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
||||
}
|
||||
|
||||
case "CollisionTriggerComponent": // Checks if another entity is inside the given radius and box with the given width and height.
|
||||
var width, height float64
|
||||
path := &canvas.Path{}
|
||||
if member, ok := component.Members["width"]; ok {
|
||||
width, _ = member.(float64)
|
||||
}
|
||||
if member, ok := component.Members["height"]; ok {
|
||||
height, _ = member.(float64)
|
||||
}
|
||||
if width > 0 && height > 0 {
|
||||
path = canvas.Rectangle(width, height).Translate(-width/2, -height/2)
|
||||
}
|
||||
// Theoretically we need to clip the area to the intersection of the box and the circle, but meh.
|
||||
// TODO: Clip the area to the intersection of the box and the circle
|
||||
//if member, ok := component.Members["radius"]; ok {
|
||||
// if radius, ok := member.(float64); ok && radius > 0 {
|
||||
// path = path.Append(canvas.Circle(radius))
|
||||
// path.And()
|
||||
// }
|
||||
//}
|
||||
if !path.Empty() {
|
||||
c.Style = entityDisplayCollisionTriggerStyle
|
||||
c.DrawPath(x, y, path)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
c.SetFillColor(color.RGBA{255, 255, 255, 128})
|
||||
c.SetStrokeColor(color.RGBA{255, 0, 0, 255})
|
||||
c.DrawPath(x, y, canvas.Circle(3))
|
||||
|
||||
//text := canvas.NewTextLine(entityDisplayFontFace, fmt.Sprintf("%s\n%s", e.Name, e.Filename), canvas.Left)
|
||||
//c.DrawText(x, y, text)
|
||||
}
|
40
bin/stitch/export-dzi.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
dzi := NewDZI(stitchedImage, dziTileSize, dziOverlap)
|
||||
|
||||
// Create base directory of all DZI files.
|
||||
if err := os.MkdirAll(outputTilesPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Export DZI descriptor.
|
||||
if err := dzi.ExportDZIDescriptor(descriptorPath); err != nil {
|
||||
return fmt.Errorf("failed to export DZI descriptor: %w", err)
|
||||
}
|
||||
|
||||
// Export DZI tiles.
|
||||
if err := dzi.ExportDZITiles(outputTilesPath, bar, webPLevel); err != nil {
|
||||
return fmt.Errorf("failed to export DZI tiles: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
67
bin/stitch/export-jpeg.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportJPEGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) 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 exportJPEG(stitchedImage, outputPath)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
options := &jpeg.Options{
|
||||
Quality: 80,
|
||||
}
|
||||
|
||||
if err := jpeg.Encode(f, img, options); err != nil {
|
||||
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
67
bin/stitch/export-png.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
func exportPNGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) 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 exportPNG(stitchedImage, outputPath)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := png.Encoder{
|
||||
CompressionLevel: png.DefaultCompression,
|
||||
}
|
||||
|
||||
if err := encoder.Encode(f, img); err != nil {
|
||||
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
73
bin/stitch/export-webp.go
Normal file
@ -0,0 +1,73 @@
|
||||
// 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
|
||||
}
|
216
bin/stitch/image-tile.go
Normal file
@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
_ "image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nfnt/resize"
|
||||
)
|
||||
|
||||
var ImageTileFileRegex = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
||||
|
||||
type ImageTile struct {
|
||||
fileName string
|
||||
modTime time.Time
|
||||
|
||||
scaleDivider int // Downscales the coordinates and images on the fly.
|
||||
|
||||
image image.Image // Either a rectangle or an RGBA image. The bounds of this image are determined by the filename.
|
||||
imageMutex *sync.RWMutex
|
||||
|
||||
invalidationChan chan struct{} // Used to send invalidation requests to the tile's goroutine.
|
||||
timeoutChan chan struct{} // Used to determine whether the tile is still being accessed or not.
|
||||
}
|
||||
|
||||
// NewImageTile returns an image tile object that represents the image at the given path.
|
||||
// The filename will be used to determine the top left x and y coordinate of the tile.
|
||||
// This will not load the image into RAM.
|
||||
func NewImageTile(path string, scaleDivider int) (ImageTile, error) {
|
||||
if scaleDivider < 1 {
|
||||
return ImageTile{}, fmt.Errorf("invalid scale of %v", scaleDivider)
|
||||
}
|
||||
|
||||
baseName := filepath.Base(path)
|
||||
result := ImageTileFileRegex.FindStringSubmatch(baseName)
|
||||
var x, y int
|
||||
if parsed, err := strconv.ParseInt(result[1], 10, 0); err == nil {
|
||||
x = int(parsed)
|
||||
} else {
|
||||
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[1], err)
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
||||
y = int(parsed)
|
||||
} else {
|
||||
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[2], err)
|
||||
}
|
||||
|
||||
width, height, err := GetImageFileDimension(path)
|
||||
if err != nil {
|
||||
return ImageTile{}, err
|
||||
}
|
||||
|
||||
var modTime time.Time
|
||||
fileInfo, err := os.Lstat(path)
|
||||
if err == nil {
|
||||
modTime = fileInfo.ModTime()
|
||||
}
|
||||
|
||||
return ImageTile{
|
||||
fileName: path,
|
||||
modTime: modTime,
|
||||
scaleDivider: scaleDivider,
|
||||
image: image.Rect(DivideFloor(x, scaleDivider), DivideFloor(y, scaleDivider), DivideCeil(x+width, scaleDivider), DivideCeil(y+height, scaleDivider)),
|
||||
imageMutex: &sync.RWMutex{},
|
||||
invalidationChan: make(chan struct{}, 1),
|
||||
timeoutChan: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetImage returns an image.Image that contains the tile pixel data.
|
||||
// This will not return errors in case something went wrong, but will just return nil.
|
||||
// All errors are written to stdout.
|
||||
func (it *ImageTile) GetImage() *image.RGBA {
|
||||
it.imageMutex.RLock()
|
||||
|
||||
// Clear the timeout chan to signal that the image is still being used.
|
||||
select {
|
||||
case <-it.timeoutChan:
|
||||
default:
|
||||
}
|
||||
|
||||
// Check if the image is already loaded.
|
||||
if img, ok := it.image.(*image.RGBA); ok {
|
||||
it.imageMutex.RUnlock()
|
||||
return img
|
||||
}
|
||||
|
||||
it.imageMutex.RUnlock()
|
||||
// It's possible that the image got changed in between here.
|
||||
it.imageMutex.Lock()
|
||||
defer it.imageMutex.Unlock()
|
||||
|
||||
// Check again if the image is already loaded.
|
||||
if img, ok := it.image.(*image.RGBA); ok {
|
||||
return img
|
||||
}
|
||||
|
||||
// Store rectangle of the old image.
|
||||
oldRect := it.image.Bounds()
|
||||
|
||||
file, err := os.Open(it.fileName)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't load file %q: %v.", it.fileName, err)
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't decode image %q: %v.", it.fileName, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if it.scaleDivider > 1 {
|
||||
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
imgRGBA.Rect = imgRGBA.Rect.Add(oldRect.Min)
|
||||
|
||||
it.image = imgRGBA
|
||||
|
||||
// Clear any old invalidation request.
|
||||
select {
|
||||
case <-it.invalidationChan:
|
||||
default:
|
||||
}
|
||||
|
||||
// Fill timeout channel with one element.
|
||||
// This is needed, as the ticker doesn't send a tick on initialization.
|
||||
select {
|
||||
case it.timeoutChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
// Free the image after some time or if requested externally.
|
||||
go func() {
|
||||
// Set up watchdog that checks if the image is being used.
|
||||
ticker := time.NewTicker(5000 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Try to send to the timeout channel.
|
||||
select {
|
||||
case it.timeoutChan <- struct{}{}:
|
||||
default:
|
||||
// Timeout channel is full because the tile image wasn't queried recently.
|
||||
break loop
|
||||
}
|
||||
case <-it.invalidationChan:
|
||||
// An invalidation was requested externally.
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Free image and other stuff.
|
||||
it.imageMutex.Lock()
|
||||
defer it.imageMutex.Unlock()
|
||||
it.image = it.image.Bounds()
|
||||
}()
|
||||
|
||||
return imgRGBA
|
||||
}
|
||||
|
||||
// Clears the cached image.
|
||||
func (it *ImageTile) Invalidate() {
|
||||
it.imageMutex.RLock()
|
||||
defer it.imageMutex.RUnlock()
|
||||
|
||||
// Try to send invalidation request.
|
||||
select {
|
||||
case it.invalidationChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// The scaled image boundaries.
|
||||
// This matches exactly to what GetImage() returns.
|
||||
func (it *ImageTile) Bounds() image.Rectangle {
|
||||
it.imageMutex.RLock()
|
||||
defer it.imageMutex.RUnlock()
|
||||
|
||||
return it.image.Bounds()
|
||||
}
|
||||
|
||||
func (it *ImageTile) String() string {
|
||||
return fmt.Sprintf("{ImageTile: %q}", it.fileName)
|
||||
}
|
63
bin/stitch/image-tiles.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2019-2023 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type ImageTiles []ImageTile
|
||||
|
||||
// LoadImageTiles "loads" all images in the directory at the given path.
|
||||
func LoadImageTiles(path string, scaleDivider int) (ImageTiles, error) {
|
||||
if scaleDivider < 1 {
|
||||
return nil, fmt.Errorf("invalid scale of %v", scaleDivider)
|
||||
}
|
||||
|
||||
var imageTiles ImageTiles
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(path, "*.png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
imageTile, err := NewImageTile(file, scaleDivider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageTiles = append(imageTiles, imageTile)
|
||||
}
|
||||
|
||||
return imageTiles, nil
|
||||
}
|
||||
|
||||
// InvalidateAboveY invalidates all cached images that have no pixel at the given y coordinate or below.
|
||||
func (it ImageTiles) Bounds() image.Rectangle {
|
||||
totalBounds := image.Rectangle{}
|
||||
for i, tile := range it {
|
||||
if i == 0 {
|
||||
totalBounds = tile.Bounds()
|
||||
} else {
|
||||
totalBounds = totalBounds.Union(tile.Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
return totalBounds
|
||||
}
|
||||
|
||||
// InvalidateAboveY invalidates all cached images that have no pixel at the given y coordinate or below.
|
||||
func (it ImageTiles) InvalidateAboveY(y int) {
|
||||
for i := range it {
|
||||
tile := &it[i] // Need to copy a reference.
|
||||
if tile.Bounds().Max.Y <= y {
|
||||
tile.Invalidate()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nfnt/resize"
|
||||
)
|
||||
|
||||
type imageTile struct {
|
||||
fileName string
|
||||
|
||||
scaleDivider int // Downscales the coordinates and images on the fly.
|
||||
|
||||
offset image.Point // Correction offset of the image, so that it aligns pixel perfect with other images. Determined by image matching.
|
||||
|
||||
image image.Image // Either a rectangle or an RGBA image. The bounds of this image are determined by the filename.
|
||||
imageMutex *sync.RWMutex //
|
||||
imageUsedFlag bool // Flag signalling, that the image was used recently
|
||||
|
||||
pixelErrorSum uint64 // Sum of the difference between the (sub)pixels of all overlapping images. 0 Means that all overlapping images are identical.
|
||||
}
|
||||
|
||||
func (it *imageTile) GetImage() (*image.RGBA, error) {
|
||||
it.imageMutex.RLock()
|
||||
|
||||
it.imageUsedFlag = true // Race condition may happen on this flag, but doesn't matter here.
|
||||
|
||||
// Check if the image is already loaded
|
||||
if img, ok := it.image.(*image.RGBA); ok {
|
||||
it.imageMutex.RUnlock()
|
||||
return img, nil
|
||||
}
|
||||
|
||||
it.imageMutex.RUnlock()
|
||||
// It's possible that the image got changed in between here
|
||||
it.imageMutex.Lock()
|
||||
defer it.imageMutex.Unlock()
|
||||
|
||||
// Check again if the image is already loaded
|
||||
if img, ok := it.image.(*image.RGBA); ok {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Store rectangle of the old image
|
||||
oldRect := it.image.Bounds()
|
||||
|
||||
file, err := os.Open(it.fileName)
|
||||
if err != nil {
|
||||
return &image.RGBA{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return &image.RGBA{}, err
|
||||
}
|
||||
|
||||
if it.scaleDivider > 1 {
|
||||
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
||||
}
|
||||
|
||||
imgRGBA, ok := img.(*image.RGBA)
|
||||
if !ok {
|
||||
return &image.RGBA{}, fmt.Errorf("expected an RGBA image, got %T instead", img)
|
||||
}
|
||||
|
||||
// Restore the position of the image rectangle
|
||||
imgRGBA.Rect = imgRGBA.Rect.Add(oldRect.Min)
|
||||
|
||||
it.image = imgRGBA
|
||||
|
||||
// Free the image after some time
|
||||
go func() {
|
||||
for it.imageUsedFlag {
|
||||
time.Sleep(1 * time.Second)
|
||||
it.imageUsedFlag = false
|
||||
}
|
||||
|
||||
it.imageMutex.Lock()
|
||||
defer it.imageMutex.Unlock()
|
||||
it.image = it.image.Bounds()
|
||||
}()
|
||||
|
||||
return imgRGBA, nil
|
||||
}
|
||||
|
||||
func (it *imageTile) OffsetBounds() image.Rectangle {
|
||||
it.imageMutex.RLock()
|
||||
defer it.imageMutex.RUnlock()
|
||||
|
||||
return it.image.Bounds().Add(it.offset)
|
||||
}
|
||||
|
||||
func (it *imageTile) Bounds() image.Rectangle {
|
||||
it.imageMutex.RLock()
|
||||
defer it.imageMutex.RUnlock()
|
||||
|
||||
return it.image.Bounds()
|
||||
}
|
||||
|
||||
func (it *imageTile) String() string {
|
||||
return fmt.Sprintf("<ImageTile \"%v\">", it.fileName)
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
||||
|
||||
func loadImages(path string, scaleDivider int) ([]imageTile, error) {
|
||||
var imageTiles []imageTile
|
||||
|
||||
if scaleDivider < 1 {
|
||||
return nil, fmt.Errorf("invalid scale of %v", scaleDivider)
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(path, "*.png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
baseName := filepath.Base(file)
|
||||
result := regexFileParse.FindStringSubmatch(baseName)
|
||||
var x, y int
|
||||
if parsed, err := strconv.ParseInt(result[1], 10, 0); err == nil {
|
||||
x = int(parsed)
|
||||
} else {
|
||||
return nil, fmt.Errorf("error parsing %v to integer: %w", result[1], err)
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
||||
y = int(parsed)
|
||||
} else {
|
||||
return nil, fmt.Errorf("error parsing %v to integer: %w", result[2], err)
|
||||
}
|
||||
|
||||
width, height, err := getImageFileDimension(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageTiles = append(imageTiles, imageTile{
|
||||
fileName: file,
|
||||
scaleDivider: scaleDivider,
|
||||
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
||||
imageMutex: &sync.RWMutex{},
|
||||
})
|
||||
}
|
||||
|
||||
return imageTiles, nil
|
||||
}
|
||||
|
||||
// Stitch takes a list of tiles and stitches them together.
|
||||
// The destImage shouldn't be too large, or it gets too slow.
|
||||
func Stitch(tiles []imageTile, destImage *image.RGBA) error {
|
||||
//intersectTiles := []*imageTile{}
|
||||
images := []*image.RGBA{}
|
||||
|
||||
// Get only the tiles that intersect with the destination image bounds.
|
||||
// Ignore alignment here, doesn't matter if an image overlaps a few pixels anyways.
|
||||
for i, tile := range tiles {
|
||||
if tile.OffsetBounds().Overlaps(destImage.Bounds()) {
|
||||
tilePtr := &tiles[i]
|
||||
img, err := tilePtr.GetImage()
|
||||
if err != nil {
|
||||
log.Printf("couldn't load image tile %s: %v", tile.String(), err)
|
||||
continue
|
||||
}
|
||||
//intersectTiles = append(intersectTiles, tilePtr)
|
||||
imgCopy := *img
|
||||
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
||||
images = append(images, &imgCopy)
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("intersectTiles: %v", intersectTiles)
|
||||
|
||||
/*for _, intersectTile := range intersectTiles {
|
||||
intersectTile.loadImage()
|
||||
draw.Draw(destImage, destImage.Bounds(), intersectTile.image, destImage.Bounds().Min, draw.Over)
|
||||
}*/
|
||||
|
||||
/*for _, intersectTile := range intersectTiles {
|
||||
drawLabel(destImage, intersectTile.image.Bounds().Min.X, intersectTile.image.Bounds().Min.Y, fmt.Sprintf("%v", intersectTile.fileName))
|
||||
}*/
|
||||
|
||||
drawMedianBlended(images, destImage)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StitchGrid calls Stitch, but divides the workload into a grid of chunks.
|
||||
// Additionally it runs the workload multithreaded.
|
||||
func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int, bar *pb.ProgressBar) (errResult error) {
|
||||
//workloads := gridifyRectangle(destImage.Bounds(), gridSize)
|
||||
workloads, err := hilbertifyRectangle(destImage.Bounds(), gridSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bar != nil {
|
||||
bar.SetTotal(int64(len(workloads))).Start()
|
||||
}
|
||||
|
||||
// Start worker threads
|
||||
wc := make(chan image.Rectangle)
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < runtime.NumCPU()*2; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for workload := range wc {
|
||||
if err := Stitch(tiles, destImage.SubImage(workload).(*image.RGBA)); err != nil {
|
||||
errResult = err // This will not stop execution, but at least one of any errors is returned.
|
||||
}
|
||||
if bar != nil {
|
||||
bar.Increment()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Push workload to worker threads
|
||||
for _, workload := range workloads {
|
||||
wc <- workload
|
||||
}
|
||||
|
||||
// Wait until all worker threads are done
|
||||
close(wc)
|
||||
wg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func drawMedianBlended(images []*image.RGBA, destImage *image.RGBA) {
|
||||
bounds := destImage.Bounds()
|
||||
|
||||
// Create arrays to be reused every pixel
|
||||
rListEmpty, gListEmpty, bListEmpty := make([]int, 0, len(images)), make([]int, 0, len(images)), make([]int, 0, len(images))
|
||||
|
||||
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
||||
point := image.Point{ix, iy}
|
||||
found := false
|
||||
|
||||
// Iterate through all images and create a list of colors.
|
||||
for _, img := range images {
|
||||
if point.In(img.Bounds()) {
|
||||
col := img.RGBAAt(point.X, point.Y)
|
||||
rList, gList, bList = append(rList, int(col.R)), append(gList, int(col.G)), append(bList, int(col.B))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no images to get data from, ignore the pixel.
|
||||
if !found {
|
||||
//destImage.SetRGBA(ix, iy, color.RGBA{})
|
||||
continue
|
||||
}
|
||||
|
||||
// Sort colors.
|
||||
sort.Ints(rList)
|
||||
sort.Ints(gList)
|
||||
sort.Ints(bList)
|
||||
|
||||
// Take the middle element of each color.
|
||||
var r, g, b uint8
|
||||
if len(rList)%2 == 0 {
|
||||
// Even
|
||||
r = uint8((rList[len(rList)/2-1] + rList[len(rList)/2]) / 2)
|
||||
} else {
|
||||
// Odd
|
||||
r = uint8(rList[(len(rList)-1)/2])
|
||||
}
|
||||
if len(gList)%2 == 0 {
|
||||
// Even
|
||||
g = uint8((gList[len(gList)/2-1] + gList[len(gList)/2]) / 2)
|
||||
} else {
|
||||
// Odd
|
||||
g = uint8(gList[(len(gList)-1)/2])
|
||||
}
|
||||
if len(bList)%2 == 0 {
|
||||
// Even
|
||||
b = uint8((bList[len(bList)/2-1] + bList[len(bList)/2]) / 2)
|
||||
} else {
|
||||
// Odd
|
||||
b = uint8(bList[(len(bList)-1)/2])
|
||||
}
|
||||
|
||||
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare takes a list of tiles and compares them pixel by pixel.
|
||||
// The resulting pixel difference sum is stored in each tile.
|
||||
func Compare(tiles []imageTile, bounds image.Rectangle) error {
|
||||
intersectTiles := []*imageTile{}
|
||||
images := []*image.RGBA{}
|
||||
|
||||
// Get only the tiles that intersect with the bounds.
|
||||
// Ignore alignment here, doesn't matter if an image overlaps a few pixels anyways.
|
||||
for i, tile := range tiles {
|
||||
if tile.OffsetBounds().Overlaps(bounds) {
|
||||
tilePtr := &tiles[i]
|
||||
img, err := tilePtr.GetImage()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't load image tile %s: %v", tile.String(), err)
|
||||
continue
|
||||
}
|
||||
intersectTiles = append(intersectTiles, tilePtr)
|
||||
imgCopy := *img
|
||||
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
||||
images = append(images, &imgCopy)
|
||||
}
|
||||
}
|
||||
|
||||
tempTilesEmpty := make([]*imageTile, 0, len(intersectTiles))
|
||||
|
||||
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||
var rMin, rMax, gMin, gMax, bMin, bMax uint8
|
||||
point := image.Point{ix, iy}
|
||||
found := false
|
||||
tempTiles := tempTilesEmpty
|
||||
|
||||
// Iterate through all images and find min and max subpixel values.
|
||||
for i, img := range images {
|
||||
if point.In(img.Bounds()) {
|
||||
tempTiles = append(tempTiles, intersectTiles[i])
|
||||
col := img.RGBAAt(point.X, point.Y)
|
||||
if !found {
|
||||
found = true
|
||||
rMin, rMax, gMin, gMax, bMin, bMax = col.R, col.R, col.G, col.G, col.B, col.B
|
||||
} else {
|
||||
if rMin > col.R {
|
||||
rMin = col.R
|
||||
}
|
||||
if rMax < col.R {
|
||||
rMax = col.R
|
||||
}
|
||||
if gMin > col.G {
|
||||
gMin = col.G
|
||||
}
|
||||
if gMax < col.G {
|
||||
gMax = col.G
|
||||
}
|
||||
if bMin > col.B {
|
||||
bMin = col.B
|
||||
}
|
||||
if bMax < col.B {
|
||||
bMax = col.B
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no images to get data from, ignore the pixel.
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
// Write the error value back into the tiles (Only those that contain the point point)
|
||||
for _, tile := range tempTiles {
|
||||
tile.pixelErrorSum += uint64(rMax-rMin) + uint64(gMax-gMin) + uint64(bMax-bMin)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompareGrid calls Compare, but divides the workload into a grid of chunks.
|
||||
// Additionally it runs the workload multithreaded.
|
||||
func CompareGrid(tiles []imageTile, bounds image.Rectangle, gridSize int, bar *pb.ProgressBar) (errResult error) {
|
||||
//workloads := gridifyRectangle(destImage.Bounds(), gridSize)
|
||||
workloads, err := hilbertifyRectangle(bounds, gridSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bar != nil {
|
||||
bar.SetTotal(int64(len(workloads))).Start()
|
||||
}
|
||||
|
||||
// Start worker threads
|
||||
wc := make(chan image.Rectangle)
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < runtime.NumCPU()*2; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for workload := range wc {
|
||||
if err := Compare(tiles, workload); err != nil {
|
||||
errResult = err // This will not stop execution, but at least one of any errors is returned.
|
||||
}
|
||||
if bar != nil {
|
||||
bar.Increment()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Push workload to worker threads
|
||||
for _, workload := range workloads {
|
||||
wc <- workload
|
||||
}
|
||||
|
||||
// Wait until all worker threads are done
|
||||
close(wc)
|
||||
wg.Wait()
|
||||
|
||||
return
|
||||
}
|
356
bin/stitch/main.go
Normal file
@ -0,0 +1,356 @@
|
||||
// Copyright (c) 2019-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1lann/promptui"
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
)
|
||||
|
||||
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 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 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 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.")
|
||||
var flagYMax = flag.Int("ymax", 0, "Lower bound of the output rectangle. This coordinate is not included in the output.")
|
||||
|
||||
func main() {
|
||||
log.Printf("Noita MapCapture stitching tool v%s.", version)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var overlays []StitchedImageOverlay
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter downscaling factor:",
|
||||
Default: fmt.Sprint(*flagScaleDivider),
|
||||
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 larger than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
fmt.Sscanf(result, "%d", flagScaleDivider)
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter blend tile limit:",
|
||||
Default: fmt.Sprint(*flagBlendTileLimit),
|
||||
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", flagBlendTileLimit)
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter input path:",
|
||||
Default: *flagInputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
*flagInputPath = result
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter \"entities.json\" path:",
|
||||
Default: *flagEntitiesInputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
*flagEntitiesInputPath = result
|
||||
}
|
||||
|
||||
// Load entities if requested.
|
||||
entities, err := LoadEntities(*flagEntitiesInputPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load entities: %v.", err)
|
||||
}
|
||||
if len(entities) > 0 {
|
||||
log.Printf("Got %v entities.", len(entities))
|
||||
overlays = append(overlays, entities) // Add entities to overlay drawing list.
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter \"player-path.json\" path:",
|
||||
Default: *flagPlayerPathInputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
*flagPlayerPathInputPath = result
|
||||
}
|
||||
|
||||
// Load player path if requested.
|
||||
playerPath, err := LoadPlayerPath(*flagPlayerPathInputPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load player path: %v.", err)
|
||||
}
|
||||
if len(playerPath) > 0 {
|
||||
log.Printf("Got %v player path entries.", len(playerPath))
|
||||
overlays = append(overlays, playerPath) // Add player path to overlay drawing list.
|
||||
}
|
||||
|
||||
log.Printf("Starting to read tile information at %q.", *flagInputPath)
|
||||
tiles, err := LoadImageTiles(*flagInputPath, *flagScaleDivider)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
if len(tiles) == 0 {
|
||||
log.Panicf("Got no image tiles from %q.", *flagInputPath)
|
||||
}
|
||||
log.Printf("Got %v tiles.", len(tiles))
|
||||
|
||||
totalBounds := image.Rectangle{}
|
||||
for i, tile := range tiles {
|
||||
if i == 0 {
|
||||
totalBounds = tile.Bounds()
|
||||
} else {
|
||||
totalBounds = totalBounds.Union(tile.Bounds())
|
||||
}
|
||||
}
|
||||
log.Printf("Total size of the possible output space is %v.", totalBounds)
|
||||
|
||||
// If the output rect is empty, use the rectangle that encloses all tiles.
|
||||
outputRect := image.Rect(*flagXMin, *flagYMin, *flagXMax, *flagYMax)
|
||||
if outputRect.Empty() {
|
||||
outputRect = totalBounds
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter output rectangle (xMin,yMin;xMax,yMax):",
|
||||
Default: fmt.Sprintf("%d,%d;%d,%d", outputRect.Min.X, outputRect.Min.Y, outputRect.Max.X, outputRect.Max.Y),
|
||||
AllowEdit: true,
|
||||
Validate: func(s string) error {
|
||||
var xMin, yMin, xMax, yMax int
|
||||
_, err := fmt.Sscanf(s, "%d,%d;%d,%d", &xMin, &yMin, &xMax, &yMax)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rect := image.Rect(xMin, yMin, xMax, yMax)
|
||||
if rect.Empty() {
|
||||
return fmt.Errorf("rectangle must not be empty")
|
||||
}
|
||||
|
||||
outputRect = rect
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
var xMin, yMin, xMax, yMax int
|
||||
fmt.Sscanf(result, "%d,%d;%d,%d", &xMin, &yMin, &xMax, &yMax)
|
||||
outputRect = image.Rect(xMin, yMin, xMax, yMax)
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter output filename and path:",
|
||||
Default: *flagOutputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v.", err)
|
||||
}
|
||||
*flagOutputPath = result
|
||||
}
|
||||
|
||||
fileExtension := strings.ToLower(filepath.Ext(*flagOutputPath))
|
||||
|
||||
// Query the user, if there were no cmd arguments given.
|
||||
if flag.NFlag() == 0 && fileExtension == ".dzi" {
|
||||
prompt := promptui.Prompt{
|
||||
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{
|
||||
BlendTileLimit: *flagBlendTileLimit, // Limit median blending to the n newest tiles by file modification time.
|
||||
}
|
||||
|
||||
stitchedImage, err := NewStitchedImage(tiles, outputRect, blendMethod, 128, overlays)
|
||||
if err != nil {
|
||||
log.Panicf("NewStitchedImage() failed: %v.", err)
|
||||
}
|
||||
|
||||
bar := pb.Full.New(0)
|
||||
|
||||
switch fileExtension {
|
||||
case ".png":
|
||||
if err := exportPNGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil {
|
||||
log.Panicf("Export of PNG file failed: %v", err)
|
||||
}
|
||||
case ".jpg", ".jpeg":
|
||||
if err := exportJPEGStitchedImage(stitchedImage, *flagOutputPath, bar); err != nil {
|
||||
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":
|
||||
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)
|
||||
}
|
||||
|
||||
log.Printf("Created output in %v.", time.Since(bar.StartTime()))
|
||||
|
||||
//fmt.Println("Press the enter key to terminate the console screen!")
|
||||
//fmt.Scanln()
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
// Copyright (c) 2019-2020 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// MedianBlendedImageRowHeight defines the height of the cached output image.
|
||||
const MedianBlendedImageRowHeight = 256
|
||||
|
||||
// MedianBlendedImage combines several imageTile to a single RGBA image.
|
||||
type MedianBlendedImage struct {
|
||||
tiles []imageTile
|
||||
bounds image.Rectangle
|
||||
|
||||
cachedRow *image.RGBA
|
||||
queryCounter int
|
||||
}
|
||||
|
||||
// NewMedianBlendedImage creates a new image from several single image tiles.
|
||||
func NewMedianBlendedImage(tiles []imageTile, bounds image.Rectangle) *MedianBlendedImage {
|
||||
return &MedianBlendedImage{
|
||||
tiles: tiles,
|
||||
bounds: bounds,
|
||||
cachedRow: &image.RGBA{},
|
||||
}
|
||||
}
|
||||
|
||||
// ColorModel returns the Image's color model.
|
||||
func (mbi *MedianBlendedImage) ColorModel() color.Model {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// Bounds returns the domain for which At can return non-zero color.
|
||||
// The bounds do not necessarily contain the point (0, 0).
|
||||
func (mbi *MedianBlendedImage) Bounds() image.Rectangle {
|
||||
return mbi.bounds
|
||||
}
|
||||
|
||||
// At returns the color of the pixel at (x, y).
|
||||
// At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
|
||||
// At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
|
||||
func (mbi *MedianBlendedImage) At(x, y int) color.Color {
|
||||
p := image.Point{x, y}
|
||||
|
||||
// Assume that every pixel is only queried once
|
||||
mbi.queryCounter++
|
||||
|
||||
if !p.In(mbi.cachedRow.Bounds()) {
|
||||
// Need to create a new row image
|
||||
rect := mbi.Bounds()
|
||||
rect.Min.Y = divideFloor(y, MedianBlendedImageRowHeight) * MedianBlendedImageRowHeight
|
||||
rect.Max.Y = rect.Min.Y + MedianBlendedImageRowHeight
|
||||
|
||||
if !p.In(rect) {
|
||||
return color.RGBA{}
|
||||
}
|
||||
|
||||
mbi.cachedRow = image.NewRGBA(rect)
|
||||
|
||||
// TODO: Don't use hilbert curve here
|
||||
if err := StitchGrid(mbi.tiles, mbi.cachedRow, 512, nil); err != nil {
|
||||
return color.RGBA{}
|
||||
}
|
||||
}
|
||||
|
||||
return mbi.cachedRow.RGBAAt(x, y)
|
||||
}
|
||||
|
||||
// Opaque returns whether the image is fully opaque.
|
||||
//
|
||||
// For more speed and smaller filesizes, MedianBlendedImage will be marked as non-transparent.
|
||||
// This will speed up image saving by 2x, as there is no need to iterate over the whole image to find a single non opaque pixel.
|
||||
func (mbi *MedianBlendedImage) Opaque() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Progress returns the approximate progress of any process that scans the image from top to bottom.
|
||||
func (mbi *MedianBlendedImage) Progress() (value, max int) {
|
||||
size := mbi.Bounds().Size()
|
||||
|
||||
return mbi.queryCounter, size.X * size.Y
|
||||
}
|
101
bin/stitch/player-path.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
var playerPathDisplayStyle = canvas.Style{
|
||||
Fill: canvas.Paint{},
|
||||
//Stroke: canvas.Paint{Color: color.RGBA{0, 0, 0, 127}},
|
||||
StrokeWidth: 3.0,
|
||||
StrokeCapper: canvas.ButtCap,
|
||||
StrokeJoiner: canvas.MiterJoin,
|
||||
DashOffset: 0.0,
|
||||
Dashes: []float64{},
|
||||
FillRule: canvas.NonZero,
|
||||
}
|
||||
|
||||
type PlayerPathElement struct {
|
||||
From [2]float64 `json:"from"`
|
||||
To [2]float64 `json:"to"`
|
||||
HP float64 `json:"hp"`
|
||||
MaxHP float64 `json:"maxHP"`
|
||||
Polymorphed bool `json:"polymorphed"`
|
||||
}
|
||||
|
||||
type PlayerPath []PlayerPathElement
|
||||
|
||||
func LoadPlayerPath(path string) (PlayerPath, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result PlayerPath
|
||||
|
||||
jsonDec := json.NewDecoder(file)
|
||||
if err := jsonDec.Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Draw implements the StitchedImageOverlay interface.
|
||||
func (p PlayerPath) Draw(destImage *image.RGBA) {
|
||||
destRect := destImage.Bounds()
|
||||
|
||||
// Same as destImage, but top left is translated to (0, 0).
|
||||
originImage := destImage.SubImage(destRect).(*image.RGBA)
|
||||
originImage.Rect = originImage.Rect.Sub(destRect.Min)
|
||||
|
||||
c := canvas.New(float64(destRect.Dx()), float64(destRect.Dy()))
|
||||
ctx := canvas.NewContext(c)
|
||||
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||
ctx.SetCoordRect(canvas.Rect{X: -float64(destRect.Min.X), Y: -float64(destRect.Min.Y), W: float64(destRect.Dx()), H: float64(destRect.Dy())}, float64(destRect.Dx()), float64(destRect.Dy()))
|
||||
|
||||
// Set drawing style.
|
||||
ctx.Style = playerPathDisplayStyle
|
||||
|
||||
for _, pathElement := range p {
|
||||
from, to := pathElement.From, pathElement.To
|
||||
|
||||
// Only draw if the path may cross the image rectangle.
|
||||
pathRect := image.Rectangle{image.Point{int(from[0]), int(from[1])}, image.Point{int(to[0]), int(to[1])}}.Canon().Inset(int(-playerPathDisplayStyle.StrokeWidth) - 1)
|
||||
if pathRect.Overlaps(destRect) {
|
||||
path := &canvas.Path{}
|
||||
path.MoveTo(from[0], from[1])
|
||||
path.LineTo(to[0], to[1])
|
||||
|
||||
if pathElement.Polymorphed {
|
||||
// Set stroke color to typically polymorph color.
|
||||
ctx.Style.Stroke.Color = color.RGBA{127, 50, 83, 127}
|
||||
} else {
|
||||
// Set stroke color depending on HP level.
|
||||
hpFactor := math.Max(math.Min(pathElement.HP/pathElement.MaxHP, 1), 0)
|
||||
hpFactorInv := 1 - hpFactor
|
||||
r, g, b, a := uint8((0*hpFactor+1*hpFactorInv)*127), uint8((1*hpFactor+0*hpFactorInv)*127), uint8(0), uint8(127)
|
||||
ctx.Style.Stroke.Color = color.RGBA{r, g, b, a}
|
||||
}
|
||||
|
||||
ctx.DrawPath(0, 0, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
||||
r := rasterizer.FromImage(originImage, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
||||
c.RenderTo(r)
|
||||
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||
}
|
21
bin/stitch/profiling.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
func init() {
|
||||
/*port := 1234
|
||||
|
||||
go func() {
|
||||
http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||
}()
|
||||
log.Printf("Profiler web server listening on port %d. Visit http://localhost:%d/debug/pprof", port, port)
|
||||
log.Printf("To profile the next 10 seconds and view the profile interactively:\n go tool pprof -http :8080 http://localhost:%d/debug/pprof/profile?seconds=10", port)
|
||||
*/
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/manifoldco/promptui"
|
||||
)
|
||||
|
||||
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
|
||||
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.")
|
||||
var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
|
||||
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.")
|
||||
var flagYMax = flag.Int("ymax", 0, "Lower bound of the output rectangle. This coordinate is not included in the output.")
|
||||
var flagPrerender = flag.Bool("prerender", false, "Pre renders the image in RAM before saving. Can speed things up if you have enough RAM.")
|
||||
var flagCleanupThreshold = flag.Float64("cleanup", 0, "Enable cleanup mode with the given threshold. This will DELETE images from the input folder, no stitching will be done in this mode. A good value to start with is 0.999, which deletes images where the sum of the min-max difference of each sub-pixel overlapping with other images is less than 99.9%% of the maximum possible sum of pixel differences.")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Query the user, if there were no cmd arguments given
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter downscaling factor:",
|
||||
Default: fmt.Sprint(*flagScaleDivider),
|
||||
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 larger than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v", err)
|
||||
}
|
||||
fmt.Sscanf(result, "%d", flagScaleDivider)
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter input path:",
|
||||
Default: *flagInputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v", err)
|
||||
}
|
||||
*flagInputPath = result
|
||||
}
|
||||
|
||||
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
||||
tiles, err := loadImages(*flagInputPath, *flagScaleDivider)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
if len(tiles) == 0 {
|
||||
log.Panicf("Got no tiles inside of %v", *flagInputPath)
|
||||
}
|
||||
log.Printf("Got %v tiles", len(tiles))
|
||||
|
||||
totalBounds := image.Rectangle{}
|
||||
for i, tile := range tiles {
|
||||
if i == 0 {
|
||||
totalBounds = tile.Bounds()
|
||||
} else {
|
||||
totalBounds = totalBounds.Union(tile.Bounds())
|
||||
}
|
||||
}
|
||||
log.Printf("Total size of the possible output space is %v", totalBounds)
|
||||
|
||||
/*profFile, err := os.Create("cpu.prof")
|
||||
if err != nil {
|
||||
log.Panicf("could not create CPU profile: %v", err)
|
||||
}
|
||||
defer profFile.Close()
|
||||
if err := pprof.StartCPUProfile(profFile); err != nil {
|
||||
log.Panicf("could not start CPU profile: %v", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()*/
|
||||
|
||||
// If the output rect is empty, use the rectangle that encloses all tiles
|
||||
outputRect := image.Rect(*flagXMin, *flagYMin, *flagXMax, *flagYMax)
|
||||
if outputRect.Empty() {
|
||||
outputRect = totalBounds
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter output rectangle (xMin,yMin;xMax,yMax):",
|
||||
Default: fmt.Sprintf("%d,%d;%d,%d", outputRect.Min.X, outputRect.Min.Y, outputRect.Max.X, outputRect.Max.Y),
|
||||
AllowEdit: true,
|
||||
Validate: func(s string) error {
|
||||
var xMin, yMin, xMax, yMax int
|
||||
_, err := fmt.Sscanf(s, "%d,%d;%d,%d", &xMin, &yMin, &xMax, &yMax)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rect := image.Rect(xMin, yMin, xMax, yMax)
|
||||
if rect.Empty() {
|
||||
return fmt.Errorf("rectangle must not be empty")
|
||||
}
|
||||
|
||||
outputRect = rect
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v", err)
|
||||
}
|
||||
var xMin, yMin, xMax, yMax int
|
||||
fmt.Sscanf(result, "%d,%d;%d,%d", &xMin, &yMin, &xMax, &yMax)
|
||||
outputRect = image.Rect(xMin, yMin, xMax, yMax)
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given
|
||||
/*if flag.NFlag() == 0 {
|
||||
fmt.Println("\nYou can now define a cleanup threshold. This mode will DELETE input images based on their similarity with other overlapping input images. The range is from 0, where no images are deleted, to 1 where all images will be deleted. A good value to get rid of most artifacts is 0.999. If you enter a threshold above 0, the program will not stitch, but DELETE some of your input images. If you want to stitch, enter 0.")
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter cleanup threshold:",
|
||||
Default: strconv.FormatFloat(*flagCleanupThreshold, 'f', -1, 64),
|
||||
AllowEdit: true,
|
||||
Validate: func(s string) error {
|
||||
result, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result < 0 || result > 1 {
|
||||
return fmt.Errorf("Number %v outside of valid range [0;1]", result)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v", err)
|
||||
}
|
||||
*flagCleanupThreshold, err = strconv.ParseFloat(result, 64)
|
||||
if err != nil {
|
||||
log.Panicf("Error while parsing user input: %v", err)
|
||||
}
|
||||
}*/
|
||||
|
||||
if *flagCleanupThreshold < 0 || *flagCleanupThreshold > 1 {
|
||||
log.Panicf("Cleanup threshold (%v) outside of valid range [0;1]", *flagCleanupThreshold)
|
||||
}
|
||||
if *flagCleanupThreshold > 0 {
|
||||
bar := pb.Full.New(0)
|
||||
|
||||
log.Printf("Cleaning up %v tiles at %v", len(tiles), outputRect)
|
||||
if err := CompareGrid(tiles, outputRect, 512, bar); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
bar.Finish()
|
||||
|
||||
for _, tile := range tiles {
|
||||
pixelErrorSumNormalized := float64(tile.pixelErrorSum) / float64(tile.Bounds().Size().X*tile.Bounds().Size().Y*3*255)
|
||||
if 1-pixelErrorSumNormalized <= *flagCleanupThreshold {
|
||||
os.Remove(tile.fileName)
|
||||
log.Printf("Tile %v has matching factor of %f. Deleted file!", &tile, 1-pixelErrorSumNormalized)
|
||||
} else {
|
||||
log.Printf("Tile %v has matching factor of %f", &tile, 1-pixelErrorSumNormalized)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Query the user, if there were no cmd arguments given
|
||||
if flag.NFlag() == 0 {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Enter output filename and path:",
|
||||
Default: *flagOutputPath,
|
||||
AllowEdit: true,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
if err != nil {
|
||||
log.Panicf("Error while getting user input: %v", err)
|
||||
}
|
||||
*flagOutputPath = result
|
||||
}
|
||||
|
||||
var outputImage image.Image
|
||||
bar := pb.Full.New(0)
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan bool)
|
||||
|
||||
if *flagPrerender {
|
||||
log.Printf("Creating output image with a size of %v", outputRect.Size())
|
||||
tempImage := image.NewRGBA(outputRect)
|
||||
|
||||
log.Printf("Stitching %v tiles into an image at %v", len(tiles), tempImage.Bounds())
|
||||
if err := StitchGrid(tiles, tempImage, 512, bar); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
bar.Finish()
|
||||
|
||||
outputImage = tempImage
|
||||
} else {
|
||||
tempImage := NewMedianBlendedImage(tiles, outputRect)
|
||||
_, max := tempImage.Progress()
|
||||
bar.SetTotal(int64(max)).Start().SetRefreshRate(1 * time.Second)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
value, _ := tempImage.Progress()
|
||||
bar.SetCurrent(int64(value))
|
||||
bar.Finish()
|
||||
return
|
||||
case <-ticker.C:
|
||||
value, _ := tempImage.Progress()
|
||||
bar.SetCurrent(int64(value))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
outputImage = tempImage
|
||||
}
|
||||
|
||||
log.Printf("Creating output file \"%v\"", *flagOutputPath)
|
||||
f, err := os.Create(*flagOutputPath)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if err := png.Encode(f, outputImage); err != nil {
|
||||
f.Close()
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if !*flagPrerender {
|
||||
done <- true
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
log.Printf("Created output file \"%v\"", *flagOutputPath)
|
||||
|
||||
}
|
156
bin/stitch/stitched-image-cache.go
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2022-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// StitchedImageCache contains part of the actual image data of a stitched image.
|
||||
// This can be regenerated or invalidated at will.
|
||||
type StitchedImageCache struct {
|
||||
sync.Mutex
|
||||
|
||||
stitchedImage *StitchedImage // The parent object.
|
||||
|
||||
rect image.Rectangle // Position and size of the cached area.
|
||||
image *image.RGBA // Cached RGBA image. The bounds of this image are determined by the filename.
|
||||
|
||||
idleCounter byte // Is incremented when this cache object idles, and reset every time the cache is used.
|
||||
}
|
||||
|
||||
func NewStitchedImageCache(stitchedImage *StitchedImage, rect image.Rectangle) StitchedImageCache {
|
||||
return StitchedImageCache{
|
||||
stitchedImage: stitchedImage,
|
||||
rect: rect,
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateAuto invalidates this cache object when it had idled for too long.
|
||||
// The cache will be invalidated after `threshold + 1` calls to InvalidateAuto.
|
||||
func (sic *StitchedImageCache) InvalidateAuto(threshold byte) {
|
||||
sic.Lock()
|
||||
defer sic.Unlock()
|
||||
|
||||
if sic.image != nil {
|
||||
if sic.idleCounter >= threshold {
|
||||
sic.image = nil
|
||||
return
|
||||
}
|
||||
sic.idleCounter++
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate clears the cached image.
|
||||
func (sic *StitchedImageCache) Invalidate() {
|
||||
sic.Lock()
|
||||
defer sic.Unlock()
|
||||
sic.image = nil
|
||||
}
|
||||
|
||||
// Regenerate refills the cache image with valid image data.
|
||||
// This will block until there is a valid image, and it will *always* return a valid image.
|
||||
func (sic *StitchedImageCache) Regenerate() *image.RGBA {
|
||||
sic.Lock()
|
||||
defer sic.Unlock()
|
||||
|
||||
sic.idleCounter = 0
|
||||
|
||||
// Check if there is already a cache image.
|
||||
if sic.image != nil {
|
||||
return sic.image
|
||||
}
|
||||
|
||||
si := sic.stitchedImage
|
||||
|
||||
// Create new image with default background color.
|
||||
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.
|
||||
intersectingTiles := []*ImageTile{}
|
||||
for i := range si.tiles {
|
||||
tile := &si.tiles[i]
|
||||
if tile.Bounds().Overlaps(sic.rect) {
|
||||
intersectingTiles = append(intersectingTiles, tile)
|
||||
}
|
||||
}
|
||||
|
||||
// Start worker threads.
|
||||
workerQueue := make(chan image.Rectangle)
|
||||
waitGroup := sync.WaitGroup{}
|
||||
workers := (runtime.NumCPU() + 1) / 2
|
||||
for i := 0; i < workers; i++ {
|
||||
waitGroup.Add(1)
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
for workload := range workerQueue {
|
||||
// List of tiles that intersect with the workload chunk.
|
||||
workloadTiles := []*ImageTile{}
|
||||
|
||||
// Get only the tiles that intersect with the workload bounds.
|
||||
for _, tile := range intersectingTiles {
|
||||
if tile.Bounds().Overlaps(workload) {
|
||||
workloadTiles = append(workloadTiles, tile)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw blended tiles into cache image.
|
||||
// Restricted by the workload rectangle.
|
||||
si.blendMethod.Draw(workloadTiles, cacheImage.SubImage(workload).(*image.RGBA))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Divide rect into chunks and push to workers.
|
||||
for _, chunk := range GridifyRectangle(sic.rect, StitchedImageCacheGridSize) {
|
||||
workerQueue <- chunk
|
||||
}
|
||||
close(workerQueue)
|
||||
|
||||
// Wait until all worker threads are done.
|
||||
waitGroup.Wait()
|
||||
|
||||
// Draw overlays.
|
||||
for _, overlay := range si.overlays {
|
||||
if overlay != nil {
|
||||
overlay.Draw(cacheImage)
|
||||
}
|
||||
}
|
||||
|
||||
// Update cached image.
|
||||
sic.image = cacheImage
|
||||
return cacheImage
|
||||
}
|
||||
|
||||
// Returns the pixel color at x and y.
|
||||
func (sic *StitchedImageCache) RGBAAt(x, y int) color.RGBA {
|
||||
// Fast path: The image is loaded.
|
||||
sic.Lock()
|
||||
if sic.image != nil {
|
||||
defer sic.Unlock()
|
||||
sic.idleCounter = 0
|
||||
return sic.image.RGBAAt(x, y)
|
||||
}
|
||||
sic.Unlock()
|
||||
|
||||
// Slow path: The image data needs to be generated first.
|
||||
// This will block until the cache is regenerated.
|
||||
return sic.Regenerate().RGBAAt(x, y)
|
||||
}
|
||||
|
||||
// Returns the pixel color at x and y.
|
||||
func (sic *StitchedImageCache) At(x, y int) color.Color {
|
||||
return sic.RGBAAt(x, y)
|
||||
}
|
||||
|
||||
func (sic *StitchedImageCache) Bounds() image.Rectangle {
|
||||
return sic.rect
|
||||
}
|
166
bin/stitch/stitched-image.go
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2022-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"sync/atomic"
|
||||
"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.
|
||||
var StitchedImageCacheGridSize = 256
|
||||
|
||||
// StitchedImageBlendMethod defines how tiles are blended together.
|
||||
type StitchedImageBlendMethod interface {
|
||||
Draw(tiles []*ImageTile, destImage *image.RGBA) // Draw is called when a new cache image is generated.
|
||||
}
|
||||
|
||||
// StitchedImageOverlay defines an interface for arbitrary overlays that can be drawn over the stitched image.
|
||||
type StitchedImageOverlay interface {
|
||||
Draw(*image.RGBA)
|
||||
}
|
||||
|
||||
// StitchedImage combines several ImageTile objects into a single RGBA image.
|
||||
// The way the images are combined/blended is defined by the blendFunc.
|
||||
type StitchedImage struct {
|
||||
tiles ImageTiles
|
||||
bounds image.Rectangle
|
||||
blendMethod StitchedImageBlendMethod
|
||||
overlays []StitchedImageOverlay
|
||||
|
||||
cacheRowHeight int
|
||||
cacheRows []StitchedImageCache
|
||||
cacheRowYOffset int // Defines the pixel offset of the first cache row.
|
||||
|
||||
oldCacheRowIndex int
|
||||
queryCounter atomic.Int64
|
||||
}
|
||||
|
||||
// NewStitchedImage creates a new image from several single image tiles.
|
||||
func NewStitchedImage(tiles ImageTiles, bounds image.Rectangle, blendMethod StitchedImageBlendMethod, cacheRowHeight int, overlays []StitchedImageOverlay) (*StitchedImage, error) {
|
||||
if bounds.Empty() {
|
||||
return nil, fmt.Errorf("given boundaries are empty")
|
||||
}
|
||||
if blendMethod == nil {
|
||||
return nil, fmt.Errorf("no blending method given")
|
||||
}
|
||||
if cacheRowHeight <= 0 {
|
||||
return nil, fmt.Errorf("invalid cache row height of %d pixels", cacheRowHeight)
|
||||
}
|
||||
|
||||
stitchedImage := &StitchedImage{
|
||||
tiles: tiles,
|
||||
bounds: bounds,
|
||||
blendMethod: blendMethod,
|
||||
overlays: overlays,
|
||||
}
|
||||
|
||||
// Generate cache image rows.
|
||||
maxRow := (bounds.Dy() - 1) / cacheRowHeight
|
||||
var cacheRows []StitchedImageCache
|
||||
for i := 0; i <= maxRow; i++ {
|
||||
rect := image.Rect(bounds.Min.X, bounds.Min.Y+i*cacheRowHeight, bounds.Max.X, bounds.Min.Y+(i+1)*cacheRowHeight)
|
||||
cacheRows = append(cacheRows, NewStitchedImageCache(stitchedImage, rect.Intersect(bounds)))
|
||||
}
|
||||
stitchedImage.cacheRowHeight = cacheRowHeight
|
||||
stitchedImage.cacheRowYOffset = -bounds.Min.Y
|
||||
stitchedImage.cacheRows = cacheRows
|
||||
|
||||
// Start ticker to automatically invalidate caches.
|
||||
// Due to this, the stitchedImage object is not composable, as this goroutine will always have a reference.
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
for range ticker.C {
|
||||
for rowIndex := range stitchedImage.cacheRows {
|
||||
stitchedImage.cacheRows[rowIndex].InvalidateAuto(3) // Invalidate cache row after 3 seconds of being idle.
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stitchedImage, nil
|
||||
}
|
||||
|
||||
// ColorModel returns the Image's color model.
|
||||
func (si *StitchedImage) ColorModel() color.Model {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// Bounds returns the domain for which At can return non-zero color.
|
||||
// The bounds do not necessarily contain the point (0, 0).
|
||||
func (si *StitchedImage) Bounds() image.Rectangle {
|
||||
return si.bounds
|
||||
}
|
||||
|
||||
func (si *StitchedImage) At(x, y int) color.Color {
|
||||
return si.RGBAAt(x, y)
|
||||
}
|
||||
|
||||
// At returns the color of the pixel at (x, y).
|
||||
//
|
||||
// This is optimized to be read line by line (scanning), it will be much slower with random access.
|
||||
//
|
||||
// For the `Progress()` method to work correctly, every pixel should be queried exactly once.
|
||||
//
|
||||
// At(Bounds().Min.X, Bounds().Min.Y) // returns the top-left pixel of the image.
|
||||
// At(Bounds().Max.X-1, Bounds().Max.Y-1) // returns the bottom-right pixel.
|
||||
func (si *StitchedImage) RGBAAt(x, y int) color.RGBA {
|
||||
// Assume that every pixel is only queried once.
|
||||
si.queryCounter.Add(1)
|
||||
|
||||
// Determine the cache rowIndex index.
|
||||
rowIndex := (y + si.cacheRowYOffset) / si.cacheRowHeight
|
||||
if rowIndex < 0 || rowIndex >= len(si.cacheRows) {
|
||||
return colorBackground
|
||||
}
|
||||
|
||||
// Check if we advanced/changed the row index.
|
||||
// This doesn't happen a lot, so stuff inside this can be a bit more expensive.
|
||||
if si.oldCacheRowIndex != rowIndex {
|
||||
// Pre generate the new row asynchronously.
|
||||
newRowIndex := rowIndex + 1
|
||||
if newRowIndex >= 0 && newRowIndex < len(si.cacheRows) {
|
||||
go si.cacheRows[newRowIndex].Regenerate()
|
||||
}
|
||||
|
||||
// Invalidate all tiles that are above the next row.
|
||||
si.tiles.InvalidateAboveY((rowIndex+1)*si.cacheRowHeight - si.cacheRowYOffset)
|
||||
|
||||
si.oldCacheRowIndex = rowIndex
|
||||
}
|
||||
|
||||
return si.cacheRows[rowIndex].RGBAAt(x, y)
|
||||
}
|
||||
|
||||
// Opaque returns whether the image is fully opaque.
|
||||
//
|
||||
// For more speed and smaller file size, StitchedImage will be marked as non-transparent.
|
||||
// This will speed up image saving by 2x, as there is no need to iterate over the whole image just to find a single non opaque pixel.
|
||||
func (si *StitchedImage) Opaque() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Progress returns the approximate progress of any process that scans the image from top to bottom.
|
||||
func (si *StitchedImage) Progress() (value, max int) {
|
||||
size := si.Bounds().Size()
|
||||
|
||||
return int(si.queryCounter.Load()), size.X * size.Y
|
||||
}
|
||||
|
||||
// SubStitchedImage returns an image representing the portion of the image p visible through r.
|
||||
// The returned image references to the original stitched image, and therefore reuses its cache.
|
||||
func (si *StitchedImage) SubStitchedImage(r image.Rectangle) SubStitchedImage {
|
||||
return SubStitchedImage{
|
||||
StitchedImage: si,
|
||||
bounds: si.Bounds().Intersect(r),
|
||||
}
|
||||
}
|
36
bin/stitch/sub-stitched-image.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2023-2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
type SubStitchedImage struct {
|
||||
*StitchedImage // The original stitched image.
|
||||
|
||||
bounds image.Rectangle // The new bounds of the cropped image.
|
||||
}
|
||||
|
||||
// Bounds returns the domain for which At can return non-zero color.
|
||||
// The bounds do not necessarily contain the point (0, 0).
|
||||
func (s SubStitchedImage) Bounds() image.Rectangle {
|
||||
return s.bounds
|
||||
}
|
||||
|
||||
func (s SubStitchedImage) At(x, y int) color.Color {
|
||||
return s.RGBAAt(x, y)
|
||||
}
|
||||
|
||||
func (s SubStitchedImage) RGBAAt(x, y int) color.RGBA {
|
||||
point := image.Point{X: x, Y: y}
|
||||
if !point.In(s.bounds) {
|
||||
return colorBackground
|
||||
}
|
||||
|
||||
return s.StitchedImage.RGBAAt(x, y)
|
||||
}
|
@ -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
|
||||
@ -8,20 +8,46 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/google/hilbert"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// QuickSelect returns the kth smallest element of the given unsorted list.
|
||||
// This is faster than sorting the list and then selecting the wanted element.
|
||||
//
|
||||
// Source: https://rosettacode.org/wiki/Quickselect_algorithm#Go
|
||||
func QuickSelectUInt8(list []uint8, k int) uint8 {
|
||||
for {
|
||||
// Partition.
|
||||
px := len(list) / 2
|
||||
pv := list[px]
|
||||
last := len(list) - 1
|
||||
list[px], list[last] = list[last], list[px]
|
||||
|
||||
i := 0
|
||||
for j := 0; j < last; j++ {
|
||||
if list[j] < pv {
|
||||
list[i], list[j] = list[j], list[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Select.
|
||||
if i == k {
|
||||
return pv
|
||||
}
|
||||
if k < i {
|
||||
list = list[:i]
|
||||
} else {
|
||||
list[i], list[last] = list[last], list[i]
|
||||
list = list[i+1:]
|
||||
k -= i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Source: https://gist.github.com/sergiotapia/7882944
|
||||
func getImageFileDimension(imagePath string) (int, int, error) {
|
||||
func GetImageFileDimension(imagePath string) (int, int, error) {
|
||||
file, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("can't open file %v: %w", imagePath, err)
|
||||
@ -36,41 +62,13 @@ func getImageFileDimension(imagePath string) (int, int, error) {
|
||||
return image.Width, image.Height, nil
|
||||
}
|
||||
|
||||
// getImageDifferenceValue returns the average quadratic difference of the (sub)pixels.
|
||||
// 0 means the images are identical, +inf means that the images don't intersect.
|
||||
func getImageDifferenceValue(a, b *image.RGBA, offsetA image.Point) float64 {
|
||||
intersection := a.Bounds().Add(offsetA).Intersect(b.Bounds())
|
||||
|
||||
if intersection.Empty() {
|
||||
return math.Inf(1)
|
||||
}
|
||||
|
||||
aSub := a.SubImage(intersection.Sub(offsetA)).(*image.RGBA)
|
||||
bSub := b.SubImage(intersection).(*image.RGBA)
|
||||
|
||||
intersectionWidth := intersection.Dx() * 4
|
||||
intersectionHeight := intersection.Dy()
|
||||
|
||||
var value int64
|
||||
|
||||
for iy := 0; iy < intersectionHeight; iy++ {
|
||||
aSlice := aSub.Pix[iy*aSub.Stride : iy*aSub.Stride+intersectionWidth]
|
||||
bSlice := bSub.Pix[iy*bSub.Stride : iy*bSub.Stride+intersectionWidth]
|
||||
for ix := 0; ix < intersectionWidth; ix += 3 {
|
||||
diff := int64(aSlice[ix]) - int64(bSlice[ix])
|
||||
value += diff * diff
|
||||
}
|
||||
}
|
||||
|
||||
return float64(value) / float64(intersectionWidth*intersectionHeight)
|
||||
}
|
||||
|
||||
func gridifyRectangle(rect image.Rectangle, gridSize int) (result []image.Rectangle) {
|
||||
for y := divideFloor(rect.Min.Y, gridSize); y < divideCeil(rect.Max.Y, gridSize); y++ {
|
||||
for x := divideFloor(rect.Min.X, gridSize); x < divideCeil(rect.Max.X, gridSize); x++ {
|
||||
func GridifyRectangle(rect image.Rectangle, gridSize int) (result []image.Rectangle) {
|
||||
for y := DivideFloor(rect.Min.Y, gridSize); y <= DivideCeil(rect.Max.Y-1, gridSize); y++ {
|
||||
for x := DivideFloor(rect.Min.X, gridSize); x <= DivideCeil(rect.Max.X-1, gridSize); x++ {
|
||||
tempRect := image.Rect(x*gridSize, y*gridSize, (x+1)*gridSize, (y+1)*gridSize)
|
||||
if tempRect.Overlaps(rect) {
|
||||
result = append(result, tempRect)
|
||||
intersection := tempRect.Intersect(rect)
|
||||
if !intersection.Empty() {
|
||||
result = append(result, intersection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,63 +76,8 @@ func gridifyRectangle(rect image.Rectangle, gridSize int) (result []image.Rectan
|
||||
return
|
||||
}
|
||||
|
||||
func hilbertifyRectangle(rect image.Rectangle, gridSize int) ([]image.Rectangle, error) {
|
||||
grid := gridifyRectangle(rect, gridSize)
|
||||
|
||||
gridX := divideFloor(rect.Min.X, gridSize)
|
||||
gridY := divideFloor(rect.Min.Y, gridSize)
|
||||
|
||||
// Size of the grid in chunks
|
||||
gridWidth := divideCeil(rect.Max.X, gridSize) - divideFloor(rect.Min.X, gridSize)
|
||||
gridHeight := divideCeil(rect.Max.Y, gridSize) - divideFloor(rect.Min.Y, gridSize)
|
||||
|
||||
s, err := hilbert.NewHilbert(int(math.Pow(2, math.Ceil(math.Log2(math.Max(float64(gridWidth), float64(gridHeight)))))))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(grid, func(i, j int) bool {
|
||||
// Ignore out of range errors, as they shouldn't happen.
|
||||
hilbertIndexA, _ := s.MapInverse(grid[i].Min.X/gridSize-gridX, grid[i].Min.Y/gridSize-gridY)
|
||||
hilbertIndexB, _ := s.MapInverse(grid[j].Min.X/gridSize-gridX, grid[j].Min.Y/gridSize-gridY)
|
||||
return hilbertIndexA < hilbertIndexB
|
||||
})
|
||||
|
||||
return grid, nil
|
||||
}
|
||||
|
||||
func drawLabel(img *image.RGBA, x, y int, label string) {
|
||||
col := color.RGBA{200, 100, 0, 255}
|
||||
point := fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}
|
||||
|
||||
d := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.NewUniform(col),
|
||||
Face: basicfont.Face7x13,
|
||||
Dot: point,
|
||||
}
|
||||
d.DrawString(label)
|
||||
}
|
||||
|
||||
func intAbs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func pointAbs(p image.Point) image.Point {
|
||||
if p.X < 0 {
|
||||
p.X = -p.X
|
||||
}
|
||||
if p.Y < 0 {
|
||||
p.Y = -p.Y
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Integer division that rounds to the next integer towards negative infinity.
|
||||
func divideFloor(a, b int) int {
|
||||
func DivideFloor(a, b int) int {
|
||||
temp := a / b
|
||||
|
||||
if ((a ^ b) < 0) && (a%b != 0) {
|
||||
@ -145,7 +88,7 @@ func divideFloor(a, b int) int {
|
||||
}
|
||||
|
||||
// Integer division that rounds to the next integer towards positive infinity.
|
||||
func divideCeil(a, b int) int {
|
||||
func DivideCeil(a, b int) int {
|
||||
temp := a / b
|
||||
|
||||
if ((a ^ b) >= 0) && (a%b != 0) {
|
||||
@ -155,9 +98,43 @@ func divideCeil(a, b int) int {
|
||||
return temp
|
||||
}
|
||||
|
||||
func maxInt(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
// 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() }
|
||||
|
29
bin/stitch/version.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// versionString contains the semantic version of the software as a string.
|
||||
//
|
||||
// This variable is only used to transfer the correct version information into the build.
|
||||
// Don't use this variable in the software, use `version` instead.
|
||||
//
|
||||
// When building the software, the default `v0.0.0-development` is used.
|
||||
// To compile the program with the correct version information, compile the following way:
|
||||
//
|
||||
// `go build -ldflags="-X 'main.versionString=x.y.z'"`, where `x.y.z` is the correct and valid version from the git tag.
|
||||
// This variable may or may not contain the prefix v.
|
||||
var versionString = "0.0.0-development"
|
||||
|
||||
// version of the program.
|
||||
//
|
||||
// When converted into a string, this will not contain the v prefix.
|
||||
var version = semver.Must(semver.NewVersion(strings.TrimPrefix(versionString, "v")))
|
@ -1,229 +0,0 @@
|
||||
<Entity
|
||||
tags="mortal,hittable,teleportable_NOT,homing_target,enemy,worm"
|
||||
name="$animal_worm"
|
||||
>
|
||||
<_Transform
|
||||
position.x="0"
|
||||
position.y="0"
|
||||
rotation="0"
|
||||
scale.x="1"
|
||||
scale.y="1" >
|
||||
</_Transform>
|
||||
|
||||
<WormComponent
|
||||
acceleration="0.5"
|
||||
gravity="0"
|
||||
tail_gravity="0"
|
||||
part_distance="10"
|
||||
ground_check_offset="1"
|
||||
hitbox_radius="5"
|
||||
target_kill_radius="7"
|
||||
target_kill_ragdoll_force="8"
|
||||
ragdoll_filename=""
|
||||
eat_anim_wait_mult="0.05 "
|
||||
jump_cam_shake="6"
|
||||
>
|
||||
</WormComponent>
|
||||
|
||||
<WormAIComponent
|
||||
speed="0"
|
||||
speed_hunt="0"
|
||||
direction_adjust_speed="0.012"
|
||||
direction_adjust_speed_hunt="0.06"
|
||||
hunt_box_radius="256"
|
||||
random_target_box_radius="128"
|
||||
new_hunt_target_check_every="120"
|
||||
new_random_target_check_every="120"
|
||||
give_up_area_radius="60"
|
||||
give_up_time_frames="250"
|
||||
>
|
||||
</WormAIComponent>
|
||||
|
||||
<CellEaterComponent
|
||||
radius="6" >
|
||||
</CellEaterComponent>
|
||||
|
||||
<DamageModelComponent
|
||||
air_needed="0"
|
||||
falling_damages="0"
|
||||
fire_damage_amount="0.2"
|
||||
fire_probability_of_ignition="0.5"
|
||||
blood_material="blood_worm"
|
||||
blood_spray_material="blood_worm"
|
||||
hp="20"
|
||||
is_on_fire="0"
|
||||
mAirAreWeInWater="0"
|
||||
mFallCount="0"
|
||||
mFallHighestY="3.40282e+038"
|
||||
mFallIsOnGround="0"
|
||||
mFireProbability="100"
|
||||
mIsOnFire="0"
|
||||
mLastCheckTime="0"
|
||||
mLastCheckX="0"
|
||||
mLastCheckY="0"
|
||||
materials_damage="1"
|
||||
materials_how_much_damage="0.1"
|
||||
materials_that_damage="acid"
|
||||
ragdoll_filenames_file=""
|
||||
ragdoll_material="meat_worm"
|
||||
ragdoll_offset_y="-6"
|
||||
blood_sprite_directional="data/particles/bloodsplatters/bloodsplatter_directional_yellow_$[1-3].xml"
|
||||
blood_sprite_large="data/particles/bloodsplatters/bloodsplatter_yellow_$[1-3].xml"
|
||||
>
|
||||
<damage_multipliers
|
||||
drill="0.4"
|
||||
>
|
||||
</damage_multipliers>
|
||||
</DamageModelComponent>
|
||||
|
||||
<PathFindingGridMarkerComponent
|
||||
marker_offset_y="0"
|
||||
marker_work_flag="16" >
|
||||
</PathFindingGridMarkerComponent>
|
||||
|
||||
<GenomeDataComponent
|
||||
_enabled="1"
|
||||
herd_id="worm"
|
||||
food_chain_rank="6"
|
||||
is_predator="1" >
|
||||
</GenomeDataComponent>
|
||||
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_head.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.5"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.49"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.48"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.47"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.46"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tail.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="15"
|
||||
offset_y="6"
|
||||
update_transform="0"
|
||||
z_index="-0.45"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="0">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="1">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="2">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="3">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="4">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="5">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
|
||||
<StatusEffectDataComponent>
|
||||
</StatusEffectDataComponent>
|
||||
|
||||
<CameraBoundComponent
|
||||
max_count="10"
|
||||
distance="2000">
|
||||
</CameraBoundComponent>
|
||||
|
||||
<ItemChestComponent level="2" enemy_drop="1" >
|
||||
</ItemChestComponent>
|
||||
|
||||
<MusicEnergyAffectorComponent
|
||||
energy_target="1">
|
||||
</MusicEnergyAffectorComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals/worm" >
|
||||
</AudioComponent>
|
||||
|
||||
<AudioLoopComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_name="animals/worm/movement_loop"
|
||||
set_speed_parameter="1"
|
||||
auto_play="1">
|
||||
</AudioLoopComponent>
|
||||
|
||||
</Entity>
|
@ -1,299 +0,0 @@
|
||||
<Entity
|
||||
tags="mortal,hittable,teleportable_NOT,homing_target,enemy,worm"
|
||||
name="$animal_worm_big"
|
||||
>
|
||||
<_Transform
|
||||
position.x="0"
|
||||
position.y="0"
|
||||
rotation="0"
|
||||
scale.x="1"
|
||||
scale.y="1" >
|
||||
</_Transform>
|
||||
|
||||
<WormComponent
|
||||
acceleration="1.5"
|
||||
gravity="0"
|
||||
tail_gravity="0"
|
||||
part_distance="16"
|
||||
ground_check_offset="8"
|
||||
hitbox_radius="9"
|
||||
target_kill_radius="10"
|
||||
target_kill_ragdoll_force="10"
|
||||
ragdoll_filename=""
|
||||
eat_anim_wait_mult="0.05 "
|
||||
jump_cam_shake="6"
|
||||
>
|
||||
</WormComponent>
|
||||
|
||||
<WormAIComponent
|
||||
speed="0"
|
||||
speed_hunt="0"
|
||||
direction_adjust_speed="0.003"
|
||||
direction_adjust_speed_hunt="0.04"
|
||||
hunt_box_radius="256"
|
||||
random_target_box_radius="128"
|
||||
new_hunt_target_check_every="240"
|
||||
new_random_target_check_every="240"
|
||||
give_up_area_radius="150"
|
||||
give_up_time_frames="250"
|
||||
>
|
||||
</WormAIComponent>
|
||||
|
||||
<CellEaterComponent
|
||||
radius="9" >
|
||||
</CellEaterComponent>
|
||||
|
||||
<DamageModelComponent
|
||||
_enabled="1"
|
||||
air_needed="0"
|
||||
|
||||
falling_damages="0"
|
||||
fire_damage_amount="0.2"
|
||||
|
||||
fire_probability_of_ignition="0.5"
|
||||
blood_material="blood_worm"
|
||||
blood_spray_material="blood_worm"
|
||||
hp="140"
|
||||
is_on_fire="0"
|
||||
mAirAreWeInWater="0"
|
||||
mFallCount="0"
|
||||
mFallHighestY="3.40282e+038"
|
||||
mFallIsOnGround="0"
|
||||
mFireProbability="100"
|
||||
mIsOnFire="0"
|
||||
mLastCheckTime="0"
|
||||
mLastCheckX="0"
|
||||
mLastCheckY="0"
|
||||
materials_damage="1"
|
||||
materials_how_much_damage="0.0001,0.0001"
|
||||
materials_that_damage="acid,lava"
|
||||
ragdoll_filenames_file=""
|
||||
ragdoll_material="meat_worm"
|
||||
ragdoll_offset_y="-6"
|
||||
blood_sprite_directional="data/particles/bloodsplatters/bloodsplatter_directional_yellow_$[1-3].xml"
|
||||
blood_sprite_large="data/particles/bloodsplatters/bloodsplatter_large_yellow_$[1-3].xml"
|
||||
>
|
||||
<damage_multipliers
|
||||
drill="0.4"
|
||||
>
|
||||
</damage_multipliers>
|
||||
</DamageModelComponent>
|
||||
|
||||
<PathFindingGridMarkerComponent
|
||||
marker_offset_y="0"
|
||||
marker_work_flag="16" >
|
||||
</PathFindingGridMarkerComponent>
|
||||
|
||||
<GenomeDataComponent
|
||||
_enabled="1"
|
||||
herd_id="worm"
|
||||
food_chain_rank="6"
|
||||
is_predator="1" >
|
||||
</GenomeDataComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_head.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.5"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.4"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.39"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.38"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.37"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.36"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_big_tail.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
next_rect_animation=""
|
||||
offset_x="22"
|
||||
offset_y="12"
|
||||
update_transform="0"
|
||||
z_index="-0.35"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="0">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="1">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="2">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="3">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="4">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="5">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="6">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
|
||||
<SpriteComponent
|
||||
_tags="health_bar_back,ui,no_hitbox"
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
has_special_scale="1"
|
||||
image_file="data/ui_gfx/health_slider_back.png"
|
||||
is_text_sprite="0"
|
||||
next_rect_animation=""
|
||||
offset_x="12"
|
||||
offset_y="42"
|
||||
rect_animation=""
|
||||
special_scale_x="1"
|
||||
special_scale_y="1"
|
||||
ui_is_parent="0"
|
||||
update_transform="1"
|
||||
visible="1"
|
||||
z_index="-9000"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_tags="health_bar,ui,no_hitbox"
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
has_special_scale="1"
|
||||
image_file="data/ui_gfx/health_slider_front.png"
|
||||
is_text_sprite="0"
|
||||
next_rect_animation=""
|
||||
offset_x="11"
|
||||
offset_y="42"
|
||||
rect_animation=""
|
||||
special_scale_x="1"
|
||||
special_scale_y="1"
|
||||
ui_is_parent="0"
|
||||
update_transform="1"
|
||||
visible="1"
|
||||
z_index="-9000"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<HealthBarComponent>
|
||||
</HealthBarComponent>
|
||||
|
||||
<CameraBoundComponent
|
||||
max_count="10"
|
||||
distance="2000">
|
||||
</CameraBoundComponent>
|
||||
|
||||
<ItemChestComponent level="3" enemy_drop="1" > </ItemChestComponent>
|
||||
|
||||
<LuaComponent
|
||||
script_death="data/scripts/animals/worm_death.lua"
|
||||
>
|
||||
</LuaComponent>
|
||||
|
||||
<MusicEnergyAffectorComponent
|
||||
energy_target="1">
|
||||
</MusicEnergyAffectorComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals/worm">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioLoopComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_name="animals/worm/movement_loop_big"
|
||||
set_speed_parameter="1"
|
||||
auto_play="1">
|
||||
</AudioLoopComponent>
|
||||
|
||||
</Entity>
|
@ -1,317 +0,0 @@
|
||||
<Entity
|
||||
tags="mortal,hittable,teleportable_NOT,homing_target,enemy,worm"
|
||||
name="$animal_worm_end"
|
||||
>
|
||||
<_Transform
|
||||
position.x="0"
|
||||
position.y="0"
|
||||
rotation="0"
|
||||
scale.x="1"
|
||||
scale.y="1" >
|
||||
</_Transform>
|
||||
|
||||
<WormComponent
|
||||
acceleration="0.5"
|
||||
gravity="0"
|
||||
tail_gravity="0"
|
||||
part_distance="12"
|
||||
ground_check_offset="8"
|
||||
hitbox_radius="9"
|
||||
bite_damage="1"
|
||||
target_kill_radius="10"
|
||||
target_kill_ragdoll_force="10"
|
||||
ragdoll_filename="data/ragdolls/worm_skull/filenames.txt"
|
||||
eat_anim_wait_mult="0.05 "
|
||||
jump_cam_shake="2" >
|
||||
</WormComponent>
|
||||
|
||||
<WormAIComponent
|
||||
speed="0"
|
||||
speed_hunt="0"
|
||||
direction_adjust_speed="0.005"
|
||||
direction_adjust_speed_hunt="0.04"
|
||||
hunt_box_radius="256"
|
||||
random_target_box_radius="128"
|
||||
new_hunt_target_check_every="240"
|
||||
new_random_target_check_every="240"
|
||||
give_up_area_radius="150"
|
||||
give_up_time_frames="250"
|
||||
>
|
||||
</WormAIComponent>
|
||||
|
||||
<DamageModelComponent
|
||||
_enabled="1"
|
||||
air_needed="0"
|
||||
falling_damages="0"
|
||||
fire_damage_amount="0.2"
|
||||
|
||||
fire_probability_of_ignition="0"
|
||||
hp="25"
|
||||
ragdoll_fx_forced="DISINTEGRATED"
|
||||
is_on_fire="0"
|
||||
mAirAreWeInWater="0"
|
||||
mFallCount="0"
|
||||
mFallHighestY="3.40282e+038"
|
||||
mFallIsOnGround="0"
|
||||
mFireProbability="100"
|
||||
mIsOnFire="0"
|
||||
mLastCheckTime="0"
|
||||
mLastCheckX="0"
|
||||
mLastCheckY="0"
|
||||
materials_damage="1"
|
||||
materials_how_much_damage="0.1"
|
||||
materials_that_damage="acid"
|
||||
ragdoll_filenames_file=""
|
||||
ragdoll_offset_y="-6"
|
||||
blood_material="lava"
|
||||
blood_spray_material="lava"
|
||||
ragdoll_material="lava"
|
||||
blood_sprite_directional="data/particles/bloodsplatters/bloodsplatter_directional_orange_$[1-3].xml"
|
||||
blood_sprite_large="data/particles/bloodsplatters/bloodsplatter_orange_$[1-3].xml"
|
||||
>
|
||||
<damage_multipliers
|
||||
drill="0.4"
|
||||
projectile="0.4"
|
||||
>
|
||||
</damage_multipliers>
|
||||
</DamageModelComponent>
|
||||
|
||||
<PathFindingGridMarkerComponent
|
||||
marker_offset_y="0"
|
||||
marker_work_flag="16" >
|
||||
</PathFindingGridMarkerComponent>
|
||||
|
||||
<GenomeDataComponent
|
||||
_enabled="1"
|
||||
herd_id="worm"
|
||||
food_chain_rank="20"
|
||||
is_predator="1" >
|
||||
</GenomeDataComponent>
|
||||
|
||||
<CellEaterComponent
|
||||
radius="9" >
|
||||
</CellEaterComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_head.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-4"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.9"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.8"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.7"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.6"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.5"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.4"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.3"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.2"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.1"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.0"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.9"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.8"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_middle.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.7"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/endworm_tail.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="29"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.6"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<CameraBoundComponent
|
||||
max_count="10"
|
||||
distance="2000">
|
||||
</CameraBoundComponent>
|
||||
|
||||
|
||||
<MusicEnergyAffectorComponent
|
||||
energy_target="1">
|
||||
</MusicEnergyAffectorComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals/worm">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioLoopComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_name="animals/worm/movement_loop_big"
|
||||
set_speed_parameter="1"
|
||||
auto_play="1">
|
||||
</AudioLoopComponent>
|
||||
|
||||
</Entity>
|
@ -1,330 +0,0 @@
|
||||
<Entity
|
||||
tags="mortal,hittable,teleportable_NOT,homing_target,enemy,worm"
|
||||
name="$animal_worm_skull"
|
||||
>
|
||||
<_Transform
|
||||
position.x="0"
|
||||
position.y="0"
|
||||
rotation="0"
|
||||
scale.x="1"
|
||||
scale.y="1" >
|
||||
</_Transform>
|
||||
|
||||
<WormComponent
|
||||
acceleration="0.5"
|
||||
gravity="0"
|
||||
tail_gravity="0"
|
||||
part_distance="12"
|
||||
ground_check_offset="8"
|
||||
hitbox_radius="9"
|
||||
bite_damage="1"
|
||||
target_kill_radius="10"
|
||||
target_kill_ragdoll_force="10"
|
||||
ragdoll_filename="data/ragdolls/worm_skull/filenames.txt"
|
||||
eat_anim_wait_mult="0.05 "
|
||||
jump_cam_shake="2" >
|
||||
</WormComponent>
|
||||
|
||||
<WormAIComponent
|
||||
speed="0"
|
||||
speed_hunt="0"
|
||||
direction_adjust_speed="0.005"
|
||||
direction_adjust_speed_hunt="0.04"
|
||||
hunt_box_radius="256"
|
||||
random_target_box_radius="128"
|
||||
new_hunt_target_check_every="240"
|
||||
new_random_target_check_every="240"
|
||||
give_up_area_radius="150"
|
||||
give_up_time_frames="250"
|
||||
>
|
||||
</WormAIComponent>
|
||||
|
||||
<DamageModelComponent
|
||||
_enabled="1"
|
||||
air_needed="0"
|
||||
falling_damages="0"
|
||||
fire_damage_amount="0.2"
|
||||
|
||||
fire_probability_of_ignition="0.5"
|
||||
hp="25"
|
||||
ragdoll_fx_forced="DISINTEGRATED"
|
||||
is_on_fire="0"
|
||||
mAirAreWeInWater="0"
|
||||
mFallCount="0"
|
||||
mFallHighestY="3.40282e+038"
|
||||
mFallIsOnGround="0"
|
||||
mFireProbability="100"
|
||||
mIsOnFire="0"
|
||||
mLastCheckTime="0"
|
||||
mLastCheckX="0"
|
||||
mLastCheckY="0"
|
||||
materials_damage="1"
|
||||
materials_how_much_damage="0.1"
|
||||
materials_that_damage="acid"
|
||||
ragdoll_filenames_file=""
|
||||
ragdoll_offset_y="-6"
|
||||
blood_material="plasma_fading"
|
||||
blood_spray_material="plasma_fading"
|
||||
ragdoll_material="plasma_fading"
|
||||
blood_sprite_directional="data/particles/bloodsplatters/bloodsplatter_directional_blue_$[1-3].xml"
|
||||
blood_sprite_large="data/particles/bloodsplatters/bloodsplatter_blue_$[1-3].xml"
|
||||
>
|
||||
<damage_multipliers
|
||||
drill="0.1"
|
||||
>
|
||||
</damage_multipliers>
|
||||
</DamageModelComponent>
|
||||
|
||||
<PathFindingGridMarkerComponent
|
||||
marker_offset_y="0"
|
||||
marker_work_flag="16" >
|
||||
</PathFindingGridMarkerComponent>
|
||||
|
||||
<GenomeDataComponent
|
||||
_enabled="1"
|
||||
herd_id="ghost"
|
||||
food_chain_rank="20"
|
||||
is_predator="1" >
|
||||
</GenomeDataComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_head.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-4.0"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="22"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.9"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.8"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.7"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.6"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.5"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.4"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.3"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.2"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.1"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-3.0"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.9"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.8"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_body.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.7"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_skull_tail.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
emissive="1"
|
||||
additive="1"
|
||||
next_rect_animation=""
|
||||
offset_x="22"
|
||||
offset_y="16"
|
||||
update_transform="0"
|
||||
z_index="-2.6"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<CameraBoundComponent
|
||||
max_count="10"
|
||||
distance="2000">
|
||||
</CameraBoundComponent>
|
||||
|
||||
<MusicEnergyAffectorComponent
|
||||
energy_target="1">
|
||||
</MusicEnergyAffectorComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals/ghost" >
|
||||
</AudioComponent>
|
||||
|
||||
</Entity>
|
@ -1,248 +0,0 @@
|
||||
<Entity
|
||||
tags="mortal,hittable,teleportable_NOT,homing_target,enemy,worm"
|
||||
name="$animal_worm_tiny"
|
||||
>
|
||||
<_Transform
|
||||
position.x="0"
|
||||
position.y="0"
|
||||
rotation="0"
|
||||
scale.x="1"
|
||||
scale.y="1" >
|
||||
</_Transform>
|
||||
|
||||
<WormComponent
|
||||
acceleration="0.3"
|
||||
gravity="0"
|
||||
tail_gravity="0"
|
||||
part_distance="6"
|
||||
ground_check_offset="1"
|
||||
hitbox_radius="2"
|
||||
bite_damage="0.3"
|
||||
target_kill_radius="7"
|
||||
target_kill_ragdoll_force="8"
|
||||
ragdoll_filename="data/ragdolls/worm_tiny/filenames.txt"
|
||||
eat_anim_wait_mult="0.05 "
|
||||
jump_cam_shake="0"
|
||||
>
|
||||
</WormComponent>
|
||||
|
||||
<WormAIComponent
|
||||
speed="0"
|
||||
speed_hunt="0"
|
||||
direction_adjust_speed="0.010"
|
||||
direction_adjust_speed_hunt="0.04"
|
||||
hunt_box_radius="256"
|
||||
random_target_box_radius="128"
|
||||
new_hunt_target_check_every="120"
|
||||
new_random_target_check_every="120"
|
||||
give_up_area_radius="60"
|
||||
give_up_time_frames="250"
|
||||
>
|
||||
</WormAIComponent>
|
||||
|
||||
<CellEaterComponent
|
||||
radius="3"
|
||||
only_stain="1" >
|
||||
</CellEaterComponent>
|
||||
|
||||
<DamageModelComponent
|
||||
_enabled="1"
|
||||
air_needed="0"
|
||||
|
||||
falling_damages="0"
|
||||
fire_damage_amount="0.2"
|
||||
|
||||
fire_probability_of_ignition="0.5"
|
||||
hp="3"
|
||||
blood_material="blood_worm"
|
||||
blood_spray_material="blood_worm"
|
||||
is_on_fire="0"
|
||||
mAirAreWeInWater="0"
|
||||
mFallCount="0"
|
||||
mFallHighestY="3.40282e+038"
|
||||
mFallIsOnGround="0"
|
||||
mFireProbability="100"
|
||||
mIsOnFire="0"
|
||||
mLastCheckTime="0"
|
||||
mLastCheckX="0"
|
||||
mLastCheckY="0"
|
||||
materials_damage="1"
|
||||
materials_how_much_damage="0.1"
|
||||
materials_that_damage="acid"
|
||||
ragdoll_filenames_file=""
|
||||
ragdoll_material="meat_worm"
|
||||
ragdoll_offset_y="-6"
|
||||
blood_sprite_directional="data/particles/bloodsplatters/bloodsplatter_directional_yellow_$[1-3].xml"
|
||||
blood_sprite_large="data/particles/bloodsplatters/bloodsplatter_yellow_$[1-3].xml"
|
||||
>
|
||||
<damage_multipliers
|
||||
drill="0.4"
|
||||
>
|
||||
</damage_multipliers>
|
||||
</DamageModelComponent>
|
||||
|
||||
<PathFindingGridMarkerComponent
|
||||
marker_offset_y="0"
|
||||
marker_work_flag="16" >
|
||||
</PathFindingGridMarkerComponent>
|
||||
|
||||
<GenomeDataComponent
|
||||
_enabled="1"
|
||||
herd_id="worm"
|
||||
food_chain_rank="18"
|
||||
is_predator="1" >
|
||||
</GenomeDataComponent>
|
||||
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_head.xml"
|
||||
rect_animation="eat"
|
||||
next_rect_animation="eat"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-4.0"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.9"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.8"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.7"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.6"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_body.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.5"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="1"
|
||||
alpha="1"
|
||||
image_file="data/enemies_gfx/worm_tiny_tail.xml"
|
||||
rect_animation="stand"
|
||||
next_rect_animation="stand"
|
||||
offset_x="9"
|
||||
offset_y="4.5"
|
||||
update_transform="0"
|
||||
z_index="-3.4"
|
||||
>
|
||||
</SpriteComponent>
|
||||
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="0">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="1">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="2">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="3">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="4">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="5">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
<SpriteStainsComponent
|
||||
fade_stains_towards_srite_top="0"
|
||||
sprite_id="6">
|
||||
</SpriteStainsComponent>
|
||||
|
||||
|
||||
<CameraBoundComponent
|
||||
max_count="10"
|
||||
distance="2000">
|
||||
</CameraBoundComponent>
|
||||
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_root="animals/worm">
|
||||
</AudioComponent>
|
||||
|
||||
<AudioLoopComponent
|
||||
file="data/audio/Desktop/animals.snd"
|
||||
event_name="animals/worm/movement_loop_small"
|
||||
set_speed_parameter="1"
|
||||
auto_play="1">
|
||||
</AudioLoopComponent>
|
||||
|
||||
</Entity>
|
70
data/entities/base_custom_card.xml
Normal file
@ -0,0 +1,70 @@
|
||||
<Entity name="card" tags="card_action">
|
||||
|
||||
<ItemComponent
|
||||
_tags="enabled_in_world"
|
||||
play_spinning_animation="0"
|
||||
preferred_inventory="FULL"
|
||||
></ItemComponent>
|
||||
|
||||
<HitboxComponent
|
||||
_tags="enabled_in_world"
|
||||
aabb_min_x="-4"
|
||||
aabb_max_x="4"
|
||||
aabb_min_y="-3"
|
||||
aabb_max_y="3"
|
||||
></HitboxComponent>
|
||||
|
||||
<SimplePhysicsComponent
|
||||
_tags="enabled_in_world"
|
||||
></SimplePhysicsComponent>
|
||||
|
||||
<VelocityComponent
|
||||
_tags="enabled_in_world"
|
||||
></VelocityComponent>
|
||||
|
||||
<!-- <SpriteComponent
|
||||
_tags="enabled_in_world,item_unlocked"
|
||||
image_file="data/ui_gfx/gun_actions/empty.png"
|
||||
offset_x="8"
|
||||
offset_y="8"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_tags="enabled_in_world,item_locked"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="8"
|
||||
></SpriteComponent> -->
|
||||
|
||||
<SpriteComponent
|
||||
_tags="enabled_in_world,item_identified"
|
||||
image_file="data/ui_gfx/gun_actions/empty.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_unidentified"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_bg"
|
||||
image_file="data/ui_gfx/inventory/item_bg_projectile.png"
|
||||
offset_x="10"
|
||||
offset_y="19"
|
||||
z_index="-1.5"
|
||||
></SpriteComponent>
|
||||
|
||||
<ItemActionComponent
|
||||
_tags="enabled_in_world"
|
||||
action_id=""
|
||||
></ItemActionComponent>
|
||||
|
||||
</Entity>
|
70
data/entities/misc/custom_cards/action.xml
Normal file
@ -0,0 +1,70 @@
|
||||
<Entity tags="card_action">
|
||||
|
||||
<ItemComponent
|
||||
_tags="enabled_in_world"
|
||||
play_spinning_animation="0"
|
||||
preferred_inventory="FULL"
|
||||
></ItemComponent>
|
||||
|
||||
<HitboxComponent
|
||||
_tags="enabled_in_world"
|
||||
aabb_min_x="-4"
|
||||
aabb_max_x="4"
|
||||
aabb_min_y="-3"
|
||||
aabb_max_y="3"
|
||||
></HitboxComponent>
|
||||
|
||||
<SimplePhysicsComponent
|
||||
_tags="enabled_in_world"
|
||||
></SimplePhysicsComponent>
|
||||
|
||||
<VelocityComponent
|
||||
_tags="enabled_in_world"
|
||||
></VelocityComponent>
|
||||
|
||||
<!-- <SpriteComponent
|
||||
_tags="enabled_in_world,item_unlocked"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="8"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_tags="enabled_in_world,item_locked"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="8"
|
||||
></SpriteComponent> -->
|
||||
|
||||
<SpriteComponent
|
||||
_tags="enabled_in_world,item_identified"
|
||||
image_file="data/ui_gfx/gun_actions/empty.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_unidentified"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51"
|
||||
></SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_bg"
|
||||
image_file="data/ui_gfx/inventory/item_bg_projectile.png"
|
||||
offset_x="10"
|
||||
offset_y="19"
|
||||
z_index="-1.5"
|
||||
></SpriteComponent>
|
||||
|
||||
<ItemActionComponent
|
||||
_tags="enabled_in_world"
|
||||
action_id=""
|
||||
></ItemActionComponent>
|
||||
|
||||
</Entity>
|
153
data/entities/misc/custom_cards/energy_shield.xml
Normal file
@ -0,0 +1,153 @@
|
||||
<Entity tags="card_action,energy_shield">
|
||||
<ItemComponent
|
||||
_tags="enabled_in_world"
|
||||
play_spinning_animation="0"
|
||||
preferred_inventory="FULL"
|
||||
></ItemComponent>
|
||||
|
||||
<HitboxComponent
|
||||
_tags="enabled_in_world"
|
||||
aabb_min_x="-4"
|
||||
aabb_max_x="4"
|
||||
aabb_min_y="-3"
|
||||
aabb_max_y="3"
|
||||
></HitboxComponent>
|
||||
|
||||
<SimplePhysicsComponent
|
||||
_tags="enabled_in_world"
|
||||
></SimplePhysicsComponent>
|
||||
|
||||
<VelocityComponent
|
||||
_tags="enabled_in_world"
|
||||
></VelocityComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_tags="enabled_in_world,item_identified"
|
||||
image_file="data/ui_gfx/gun_actions/energy_shield.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51" >
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_unidentified"
|
||||
image_file="data/ui_gfx/gun_actions/unidentified.png"
|
||||
offset_x="8"
|
||||
offset_y="17"
|
||||
z_index="-1.51" >
|
||||
</SpriteComponent>
|
||||
|
||||
<SpriteComponent
|
||||
_enabled="0"
|
||||
_tags="enabled_in_world,item_bg"
|
||||
image_file="data/ui_gfx/inventory/item_bg_projectile.png"
|
||||
offset_x="10"
|
||||
offset_y="19"
|
||||
z_index="-1.5"
|
||||
></SpriteComponent>
|
||||
|
||||
<ItemActionComponent
|
||||
_tags="enabled_in_world"
|
||||
action_id="ENERGY_SHIELD" >
|
||||
</ItemActionComponent>
|
||||
|
||||
<InheritTransformComponent
|
||||
_tags="enabled_in_hand"
|
||||
use_root_parent="1">
|
||||
<Transform
|
||||
position.x="0"
|
||||
position.y="-4" >
|
||||
</Transform>
|
||||
</InheritTransformComponent>
|
||||
|
||||
<EnergyShieldComponent
|
||||
_tags="enabled_in_hand,item_identified__LEGACY"
|
||||
recharge_speed="0.25"
|
||||
radius="16.0"
|
||||
>
|
||||
</EnergyShieldComponent>
|
||||
|
||||
<ParticleEmitterComponent
|
||||
_tags="character,enabled_in_hand,item_identified__LEGACY"
|
||||
emitted_material_name="plasma_fading"
|
||||
gravity.y="0.0"
|
||||
lifetime_min="0.1"
|
||||
lifetime_max="0.5"
|
||||
count_min="2"
|
||||
count_max="4"
|
||||
render_on_grid="1"
|
||||
fade_based_on_lifetime="1"
|
||||
area_circle_radius.max="16"
|
||||
cosmetic_force_create="0"
|
||||
airflow_force="0.5"
|
||||
airflow_time="0.1"
|
||||
airflow_scale="0.5"
|
||||
emission_interval_min_frames="1"
|
||||
emission_interval_max_frames="1"
|
||||
emit_cosmetic_particles="1"
|
||||
is_emitting="1" >
|
||||
</ParticleEmitterComponent>
|
||||
|
||||
<ParticleEmitterComponent
|
||||
_tags="character,enabled_in_hand,item_identified__LEGACY,shield_ring"
|
||||
emitted_material_name="plasma_fading"
|
||||
gravity.y="0.0"
|
||||
lifetime_min="0.02"
|
||||
lifetime_max="0.05"
|
||||
count_min="90"
|
||||
count_max="100"
|
||||
render_on_grid="1"
|
||||
fade_based_on_lifetime="1"
|
||||
area_circle_radius.min="16"
|
||||
area_circle_radius.max="16"
|
||||
cosmetic_force_create="0"
|
||||
airflow_force="0.3"
|
||||
airflow_time="0.01"
|
||||
airflow_scale="0.05"
|
||||
emission_interval_min_frames="0"
|
||||
emission_interval_max_frames="0"
|
||||
emit_cosmetic_particles="1"
|
||||
is_emitting="1" >
|
||||
</ParticleEmitterComponent>
|
||||
|
||||
<ParticleEmitterComponent
|
||||
_tags="character,enabled_in_hand,item_identified__LEGACY,shield_hit"
|
||||
emitted_material_name="plasma_fading"
|
||||
gravity.y="0.0"
|
||||
lifetime_min="0.3"
|
||||
lifetime_max="1"
|
||||
count_min="300"
|
||||
count_max="360"
|
||||
render_on_grid="1"
|
||||
fade_based_on_lifetime="1"
|
||||
area_circle_radius.min="16"
|
||||
area_circle_radius.max="16"
|
||||
cosmetic_force_create="0"
|
||||
airflow_force="2.8"
|
||||
airflow_time="0.03"
|
||||
airflow_scale="0.8"
|
||||
emission_interval_min_frames="0"
|
||||
emission_interval_max_frames="0"
|
||||
emit_cosmetic_particles="1"
|
||||
is_emitting="0" >
|
||||
</ParticleEmitterComponent>
|
||||
|
||||
<LightComponent
|
||||
_tags="enabled_in_hand,item_identified"
|
||||
_enabled="1"
|
||||
radius="80"
|
||||
fade_out_time="1.5"
|
||||
r="150"
|
||||
g="190"
|
||||
b="230" >
|
||||
</LightComponent>
|
||||
|
||||
<AudioComponent
|
||||
_tags="enabled_in_hand,item_identified"
|
||||
file="data/audio/Desktop/projectiles.bank"
|
||||
event_root="player_projectiles/shield"
|
||||
set_latest_event_position="1" >
|
||||
</AudioComponent>
|
||||
|
||||
</Entity>
|
1000
files/capture.lua
135
files/check.lua
Normal file
@ -0,0 +1,135 @@
|
||||
-- Copyright (c) 2019-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Check if everything is alright.
|
||||
-- This does mainly trigger user messages and suggest actions.
|
||||
|
||||
-----------------------
|
||||
-- Load global stuff --
|
||||
-----------------------
|
||||
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
local Coords = require("coordinates")
|
||||
local ScreenCap = require("screen-capture")
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
local Utils = require("noita-api.utils")
|
||||
|
||||
----------
|
||||
-- Code --
|
||||
----------
|
||||
|
||||
---Runs a list of checks at addon startup.
|
||||
function Check:Startup()
|
||||
if Utils.FileExists("mods/noita-mapcap/output/nonempty") then
|
||||
Message:ShowOutputNonEmpty()
|
||||
end
|
||||
|
||||
if not Utils.FileExists("mods/noita-mapcap/bin/capture-b/capture.dll") then
|
||||
Message:ShowGeneralInstallationProblem("`capture.dll` is missing.", "Make sure you have installed the mod correctly.")
|
||||
end
|
||||
|
||||
if not Utils.FileExists("mods/noita-mapcap/bin/stitch/stitch.exe") then
|
||||
Message:ShowGeneralInstallationProblem("`stitch.exe` is missing.", "Make sure you have installed the mod correctly.", " ", "You can still use the mod to capture, though.")
|
||||
end
|
||||
end
|
||||
|
||||
---Regularly runs a list of checks.
|
||||
---@param interval integer -- Check interval in frames.
|
||||
function Check:Regular(interval)
|
||||
interval = interval or 60
|
||||
self.Counter = (self.Counter or 0) - 1
|
||||
if self.Counter > 0 then return end
|
||||
self.Counter = interval
|
||||
|
||||
-- Remove some messages, so they will automatically disappear when the problem is solved.
|
||||
Message:CloseAutoClose()
|
||||
|
||||
-- Compare Noita config and actual window resolution.
|
||||
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
|
||||
if topLeft and bottomRight then
|
||||
local actual = bottomRight - topLeft
|
||||
if actual ~= Coords:InternalRectSize() then
|
||||
Message:ShowWrongResolution(Modification.AutoSet, string.format("Internal rectangle size is %s. Current resolution is %s.", Coords:InternalRectSize(), actual))
|
||||
end
|
||||
else
|
||||
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
|
||||
end
|
||||
|
||||
-- Check if we have the required settings.
|
||||
local config, magic, patches = Modification.RequiredChanges()
|
||||
if config["fullscreen"] then
|
||||
local expected = tonumber(config["fullscreen"])
|
||||
if expected ~= Coords.FullscreenMode then
|
||||
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
|
||||
end
|
||||
end
|
||||
if config["window_w"] and config["window_h"] then
|
||||
local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"]))
|
||||
if expected ~= Coords.WindowResolution then
|
||||
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Window resolution is %s. Expected %s.", Coords.WindowResolution, expected))
|
||||
end
|
||||
end
|
||||
if config["internal_size_w"] and config["internal_size_h"] then
|
||||
local expected = Vec2(tonumber(config["internal_size_w"]), tonumber(config["internal_size_h"]))
|
||||
if expected ~= Coords.InternalResolution then
|
||||
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Internal resolution is %s. Expected %s.", Coords.InternalResolution, expected))
|
||||
end
|
||||
end
|
||||
if config["screenshake_intensity"] then
|
||||
local expected = config.screenshake_intensity
|
||||
if expected ~= self.StartupConfig.screenshake_intensity then
|
||||
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Screen shake intensity is %s, expected %s.", self.StartupConfig.screenshake_intensity, expected))
|
||||
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.
|
||||
if magic["VIRTUAL_RESOLUTION_X"] and magic["VIRTUAL_RESOLUTION_Y"] then
|
||||
local expected = Vec2(tonumber(magic["VIRTUAL_RESOLUTION_X"]), tonumber(magic["VIRTUAL_RESOLUTION_Y"]))
|
||||
if expected ~= Coords.VirtualResolution then
|
||||
Message:ShowRequestRestart(string.format("Virtual resolution is %s. Expected %s.", Coords.VirtualResolution, expected))
|
||||
end
|
||||
end
|
||||
if magic["VIRTUAL_RESOLUTION_OFFSET_X"] and magic["VIRTUAL_RESOLUTION_OFFSET_Y"] then
|
||||
local expected = Vec2(tonumber(magic["VIRTUAL_RESOLUTION_OFFSET_X"]), tonumber(magic["VIRTUAL_RESOLUTION_OFFSET_Y"]))
|
||||
if expected ~= Coords.VirtualOffset then
|
||||
Message:ShowRequestRestart(string.format("Virtual offset is %s. Expected %s.", Coords.VirtualOffset, expected))
|
||||
end
|
||||
end
|
||||
|
||||
-- Request a restart if the user has changed specific mod settings.
|
||||
local restartModSettings = {"disable-background", "disable-ui", "disable-physics", "disable-postfx"}
|
||||
for i, v in ipairs(restartModSettings) do
|
||||
local settingID = "noita-mapcap." .. v
|
||||
if ModSettingGetNextValue(settingID) ~= ModSettingGet(settingID) then
|
||||
Message:ShowRequestRestart(string.format("Setting %s got changed from %s to %s.", v, tostring(ModSettingGet(settingID)), tostring(ModSettingGetNextValue(settingID))))
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if capture grid size is smaller than the virtual resolution.
|
||||
-- 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 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.",
|
||||
" ",
|
||||
"Apply either of the following in the mod settings:",
|
||||
string.format("- Set the grid size to at most %s.", math.min(Coords.VirtualResolution.x, Coords.VirtualResolution.y)),
|
||||
string.format("- Increase the custom resolutions to at least %s in any direction.", captureGridSize),
|
||||
"- Change capture mode to `live`."
|
||||
)
|
||||
end
|
||||
|
||||
end
|
@ -1,39 +0,0 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Some code to make noita's lua more conform to standard lua
|
||||
|
||||
-- Globally overwrite print function to behave more like expected
|
||||
local oldPrint = print
|
||||
function print(...)
|
||||
local arg = {...}
|
||||
|
||||
stringArgs = {}
|
||||
|
||||
for i, v in ipairs(arg) do
|
||||
table.insert(stringArgs, tostring(v))
|
||||
end
|
||||
|
||||
oldPrint(unpack(stringArgs))
|
||||
end
|
||||
|
||||
-- Overwrite print to copy its output into a file
|
||||
--[[local logFile = io.open("lualog.txt", "w")
|
||||
function print(...)
|
||||
local arg = {...}
|
||||
|
||||
stringArgs = {}
|
||||
|
||||
local result = ""
|
||||
for i, v in ipairs(arg) do
|
||||
table.insert(stringArgs, tostring(v))
|
||||
result = result .. tostring(v) .. "\t"
|
||||
end
|
||||
result = result .. "\n"
|
||||
logFile:write(result)
|
||||
logFile:flush()
|
||||
|
||||
oldPrint(unpack(stringArgs))
|
||||
end]]
|
92
files/config.lua
Normal file
@ -0,0 +1,92 @@
|
||||
-- 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.
|
||||
-- This is only used when modifying entities, not when capturing/storing them.
|
||||
Config.ComponentsToDisable = {
|
||||
"AnimalAIComponent",
|
||||
"SimplePhysicsComponent",
|
||||
"CharacterPlatformingComponent",
|
||||
"WormComponent",
|
||||
"WormAIComponent",
|
||||
--"CameraBoundComponent", -- This is already removed when capturing/storing entities. Not needed when we only modify entities.
|
||||
--"PhysicsBodyCollisionDamageComponent",
|
||||
--"ExplodeOnDamageComponent",
|
||||
--"DamageModelComponent",
|
||||
--"SpriteOffsetAnimatorComponent",
|
||||
--"MaterialInventoryComponent",
|
||||
--"LuaComponent",
|
||||
--"PhysicsBody2Component", -- Disabling will hide barrels and similar stuff, also triggers an assertion.
|
||||
--"PhysicsBodyComponent",
|
||||
--"VelocityComponent", -- Disabling this component may cause a "...\component_updators\advancedfishai_system.cpp at line 107" exception.
|
||||
--"SpriteComponent",
|
||||
--"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"] = getBaseArea,
|
||||
|
||||
-- Main world: The main world with 3 parts: sky, normal and hell.
|
||||
["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"] = 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,
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local ffi = ffi or _G.ffi or require("ffi")
|
||||
|
||||
local status, caplib = pcall(ffi.load, "mods/noita-mapcap/bin/capture-b/capture")
|
||||
if not status then
|
||||
print("Error loading capture lib: " .. cap)
|
||||
end
|
||||
ffi.cdef [[
|
||||
typedef long LONG;
|
||||
typedef struct {
|
||||
LONG left;
|
||||
LONG top;
|
||||
LONG right;
|
||||
LONG bottom;
|
||||
} RECT;
|
||||
|
||||
bool GetRect(RECT* rect);
|
||||
bool Capture(int x, int y);
|
||||
|
||||
int SetThreadExecutionState(int esFlags);
|
||||
]]
|
||||
|
||||
function TriggerCapture(x, y)
|
||||
return caplib.Capture(x, y)
|
||||
end
|
||||
|
||||
-- Get the client rectangle of the "Main" window of this process in screen coordinates
|
||||
function GetRect()
|
||||
local rect = ffi.new("RECT", 0, 0, 0, 0)
|
||||
if not caplib.GetRect(rect) then
|
||||
return nil
|
||||
end
|
||||
|
||||
return rect
|
||||
end
|
||||
|
||||
-- Reset computer and monitor standby timer
|
||||
function ResetStandbyTimer()
|
||||
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
||||
end
|
@ -1,16 +0,0 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
if not async then
|
||||
dofile("data/scripts/lib/coroutines.lua")
|
||||
end
|
||||
dofile("data/scripts/perks/perk_list.lua")
|
||||
|
||||
dofile("mods/noita-mapcap/files/compatibility.lua")
|
||||
dofile("mods/noita-mapcap/files/util.lua")
|
||||
dofile("mods/noita-mapcap/files/hilbert.lua")
|
||||
dofile("mods/noita-mapcap/files/external.lua")
|
||||
dofile("mods/noita-mapcap/files/capture.lua")
|
||||
dofile("mods/noita-mapcap/files/ui.lua")
|
278
files/libraries/coordinates.lua
Normal file
@ -0,0 +1,278 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Pixel perfect viewport coordinates transformation (world <-> window) for Noita.
|
||||
|
||||
-- For it to work, you have to:
|
||||
-- - Put Coords:ReadResolutions() inside of the OnMagicNumbersAndWorldSeedInitialized() hook.
|
||||
|
||||
-- Some general information on how Noita does that stuff internally:
|
||||
-- - The base for all calculations is the window rectangle (window client area).
|
||||
-- - Inside the window there is the internal rectangle that is fit to be fully contained and centered inside the window.
|
||||
-- - Inside the internal rectangle there is the virtual rectangle that is aligned to the top, and scaled to fit horizontally.
|
||||
-- - Everything outside the internal rectangle is black.
|
||||
-- - Everything outside the virtual rectangle is not rendered correctly.
|
||||
-- - A positive virtual offset moves the rendered world to the top left.
|
||||
-- - GameGetCameraBounds returned coordinates are off by a few pixels, also it doesn't have sub pixel precision.
|
||||
-- - The mouse cursor coordinates in the dev build use the wrong rounding method (They are rounded towards zero, instead of being rounded towards negative infinity).
|
||||
-- - Integer world coordinates map exactly to pixel borders.
|
||||
-- - The default image ratios of the virtual and internal rectangles don't exactly match, which causes a small line of not correctly rendered pixels at the bottom window.
|
||||
-- - The GRID_RENDER_BORDER magic number adds the given amount of world pixels to the virtual rectangle's width. This happens after fitting, so a positive value will make it wider than the internal rectangle. The virtual rectangle will always be aligned to start at the left side of the internal rectangle, though.
|
||||
-- - The virtual offset needs to be [-GRID_RENDER_BORDER, 0] for the viewport center to be exactly centered to the window or virtual rectangle.
|
||||
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
local CameraAPI = require("noita-api.camera")
|
||||
local NXML = require("luanxml.nxml")
|
||||
local Utils = require("noita-api.utils")
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
----------
|
||||
-- Code --
|
||||
----------
|
||||
|
||||
---@class Coords
|
||||
---@field InternalResolution Vec2 -- Size of the internal rectangle in window pixels.
|
||||
---@field WindowResolution Vec2 -- Size of the window client area in window pixels.
|
||||
---@field VirtualResolution Vec2 -- Size of the virtual rectangle in world/virtual pixels.
|
||||
---@field VirtualOffset Vec2 -- Offset of the virtual rectangle in world/virtual pixels.
|
||||
---@field VirtualBorder number -- The magic number "GRID_RENDER_BORDER" in world/virtual pixels.
|
||||
---@field FullscreenMode integer -- The fullscreen mode the game is in. 0 is windowed.
|
||||
local Coords = {
|
||||
InternalResolution = Vec2(0, 0),
|
||||
WindowResolution = Vec2(0, 0),
|
||||
VirtualResolution = Vec2(0, 0),
|
||||
VirtualOffset = Vec2(0, 0),
|
||||
VirtualBorder = 0,
|
||||
FullscreenMode = 0,
|
||||
}
|
||||
|
||||
---Reads and updates the internal, window and virtual resolutions from Noita's config files and API.
|
||||
---@return any error
|
||||
function Coords:ReadResolutions()
|
||||
local filename = Utils.GetSpecialDirectory("save-shared") .. "config.xml"
|
||||
|
||||
local f, err = io.open(filename, "r")
|
||||
if not f then return err end
|
||||
|
||||
local xml = NXML.parse(f:read("*a"))
|
||||
|
||||
self.WindowResolution = Vec2(tonumber(xml.attr["window_w"]), tonumber(xml.attr["window_h"]))
|
||||
self.InternalResolution = Vec2(tonumber(xml.attr["internal_size_w"]), tonumber(xml.attr["internal_size_h"]))
|
||||
self.VirtualResolution = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y")))
|
||||
self.VirtualOffset = Vec2(tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_X")), tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_OFFSET_Y")))
|
||||
self.VirtualBorder = tonumber(MagicNumbersGetValue("GRID_RENDER_BORDER")) or 0
|
||||
self.FullscreenMode = tonumber(xml.attr["fullscreen"]) or 0
|
||||
|
||||
f:close()
|
||||
return nil
|
||||
end
|
||||
|
||||
---Returns the size of the internal rectangle in window/screen coordinates.
|
||||
---The internal rect is always uniformly scaled to fit inside the window rectangle.
|
||||
---@return Vec2
|
||||
function Coords:InternalRectSize()
|
||||
return self.InternalResolution * math.min(self.WindowResolution.x / self.InternalResolution.x, self.WindowResolution.y / self.InternalResolution.y)
|
||||
end
|
||||
|
||||
---Returns the window coordinates of the internal rectangle in window/screen coordinates.
|
||||
---This rectangle is centered and scaled to fit exactly into the window rectangle.
|
||||
---@return Vec2 topLeft
|
||||
---@return Vec2 bottomRight -- These coordinates are outside of the rectangle.
|
||||
function Coords:InternalRect()
|
||||
local internalRectSize = self:InternalRectSize()
|
||||
|
||||
-- Center rectangle and return corner points.
|
||||
|
||||
---@type Vec2
|
||||
local halfDifference = (self.WindowResolution - internalRectSize) / 2
|
||||
return halfDifference, internalRectSize + halfDifference
|
||||
end
|
||||
|
||||
---Returns the virtual rectangle coordinates in window/screen coordinates.
|
||||
---This is the rectangle that has all chunks and terrain rendered correctly.
|
||||
---The rectangle may be larger than the screen, though.
|
||||
---@return Vec2 topLeft
|
||||
---@return Vec2 bottomRight -- These coordinates are outside of the rectangle.
|
||||
function Coords:VirtualRect()
|
||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||
|
||||
return internalTopLeft, internalTopLeft + self.VirtualResolution * self:PixelScale()
|
||||
end
|
||||
|
||||
---Returns the rectangle that contains valid rendered terrain and chunks.
|
||||
---This is cropped to the internal rectangle, and can be used to determine the usable area of window screenshots.
|
||||
---@return Vec2 topLeft
|
||||
---@return Vec2 bottomRight -- These coordinates are outside of the rectangle.
|
||||
function Coords:ValidRenderingRect()
|
||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||
local virtualTopLeft, virtualBottomRight = self:VirtualRect()
|
||||
|
||||
return virtualTopLeft, Vec2(math.min(virtualBottomRight.x, internalBottomRight.x), math.min(virtualBottomRight.y, internalBottomRight.y))
|
||||
end
|
||||
|
||||
---Returns the ratio of window pixels per world pixels.
|
||||
---As pixels are always square, this returns just a single number.
|
||||
---@return number
|
||||
function Coords:PixelScale()
|
||||
local internalRectSize = self:InternalRectSize()
|
||||
|
||||
-- The virtual rectangle is always scaled to fit horizontally.
|
||||
return internalRectSize.x / self.VirtualResolution.x
|
||||
end
|
||||
|
||||
---Converts the given virtual/world coordinates into window/screen coordinates.
|
||||
---@param world Vec2 -- World coordinate, origin is near the cave entrance.
|
||||
---@param viewportCenter Vec2|nil -- Result of `GameGetCameraPos()`. Will be queried automatically if set to nil.
|
||||
---@return Vec2 window
|
||||
function Coords:ToWindow(world, viewportCenter)
|
||||
viewportCenter = viewportCenter or CameraAPI.GetPos()
|
||||
|
||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||
local pixelScale = self:PixelScale()
|
||||
|
||||
return internalTopLeft + (self.VirtualResolution / 2 + world - viewportCenter + Vec2(self.VirtualBorder, 0) + self.VirtualOffset) * pixelScale
|
||||
end
|
||||
|
||||
---Converts the given window coordinates into world/virtual coordinates.
|
||||
---@param window Vec2 -- In screen pixels, origin is at the top left of the window client area.
|
||||
---@param viewportCenter Vec2|nil -- Result of `GameGetCameraPos()`. Will be queried automatically if set to nil.
|
||||
---@return Vec2 world
|
||||
function Coords:ToWorld(window, viewportCenter)
|
||||
viewportCenter = viewportCenter or CameraAPI.GetPos()
|
||||
|
||||
local internalTopLeft, internalBottomRight = self:InternalRect()
|
||||
local pixelScale = self:PixelScale()
|
||||
|
||||
return viewportCenter - self.VirtualResolution / 2 + (window - internalTopLeft) / pixelScale - Vec2(self.VirtualBorder, 0) - self.VirtualOffset
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Testing --
|
||||
-------------
|
||||
|
||||
---Values to test the coordinate transformations.
|
||||
---
|
||||
--- Configuration (`...\save_shared\config.xml`) parameters:
|
||||
--- - `backbuffer_width`, `backbuffer_height`: The resolution for the final pixel shader, or something like that. Lowering this will not change the coordinate system, but make everything look more pixelated. The backbuffer size should be set at least to the internal resolution, Noita sets it to the window resolution.
|
||||
--- - `internal_size_w`, `internal_size_h`: The rectangle that all window content will be displayed in. If the window ratio is different than the internal size ratio, there will be black bars either at the top and bottom, or left and right.
|
||||
--- - `window_w`, `window_h`: The window client area size in pixels, duh.
|
||||
---
|
||||
--- Magic numbers (`.\mods\noita-mapcap\files\magic_numbers.xml`):
|
||||
--- - `VIRTUAL_RESOLUTION_X`, `VIRTUAL_RESOLUTION_X`: The resolution of the rendered world.
|
||||
--- - `VIRTUAL_RESOLUTION_OFFSET_X`, `VIRTUAL_RESOLUTION_OFFSET_Y`: Offset of the world/virtual coordinate system, has to be set to `-2, 0` to map pixel perfect to the screen.
|
||||
---
|
||||
--- Table contents:
|
||||
---
|
||||
--- - `InternalRes`, `WindowRes`, `VirtualRes`, `VirtualBorder` -- are the settings from the above mentioned config files.
|
||||
--- - `WindowTopLeft` contains the resulting world coordinates of the window's top left pixel with GameSetCameraPos(0, 0).
|
||||
--- - `WindowCenter` contains the resulting world coordinates of the window's center pixel with GameSetCameraPos(0, 0).
|
||||
--- - `RenderedTopLeft`, `RenderedBottomRight` describe the rectangle in world coordinates that contains correctly rendered chunks. Everything outside this rectangle may either just be a blank background image or completely black.
|
||||
local testTable = {
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -512), WindowCenter = Vec2(0, 0), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 512) },
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(512, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-256, -512), WindowCenter = Vec2(0, -256), RenderedTopLeft = Vec2(-256, -512), RenderedBottomRight = Vec2(256, 0) },
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(1024, 512), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -256), WindowCenter = Vec2(0, 256), RenderedTopLeft = Vec2(-512, -256), RenderedBottomRight = Vec2(512, 256) },
|
||||
{ InternalRes = Vec2(512, 1024), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-1024, -512), WindowCenter = Vec2(0, 512), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 512) },
|
||||
{ InternalRes = Vec2(1024, 512), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -768), WindowCenter = Vec2(0, -256), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 0) },
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(1024, 2048), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -1024), WindowCenter = Vec2(0, 0), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 512) },
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(2048, 1024), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-1024, -512), WindowCenter = Vec2(0, 0), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 512) },
|
||||
{ InternalRes = Vec2(1024, 512), WindowRes = Vec2(1024, 512), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -512), WindowCenter = Vec2(0, -256), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 0) },
|
||||
{ InternalRes = Vec2(2048, 1024), WindowRes = Vec2(2048, 1024), VirtualRes = Vec2(1024, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -512), WindowCenter = Vec2(0, -256), RenderedTopLeft = Vec2(-512, -512), RenderedBottomRight = Vec2(512, 0) },
|
||||
{ InternalRes = Vec2(2048, 1024), WindowRes = Vec2(2048, 1024), VirtualRes = Vec2(2048, 1024), VirtualBorder = 2, WindowTopLeft = Vec2(-1024, -512), WindowCenter = Vec2(0, 0), RenderedTopLeft = Vec2(-1024, -512), RenderedBottomRight = Vec2(1024, 512) },
|
||||
{ InternalRes = Vec2(2048, 1024), WindowRes = Vec2(2048, 1024), VirtualRes = Vec2(512, 2048), VirtualBorder = 2, WindowTopLeft = Vec2(-256, -1024), WindowCenter = Vec2(0, -896), RenderedTopLeft = Vec2(-256, -1024), RenderedBottomRight = Vec2(256, -768) },
|
||||
{ InternalRes = Vec2(2048, 1024), WindowRes = Vec2(2048, 1024), VirtualRes = Vec2(1024, 16), VirtualBorder = 2, WindowTopLeft = Vec2(-512, -8), WindowCenter = Vec2(0, 248), RenderedTopLeft = Vec2(-512, -8), RenderedBottomRight = Vec2(512, 8) },
|
||||
{ InternalRes = Vec2(1024, 1024), WindowRes = Vec2(1024, 1024), VirtualRes = Vec2(32, 16), VirtualBorder = 2, WindowTopLeft = Vec2(-16, -8), WindowCenter = Vec2(0, 8), RenderedTopLeft = Vec2(-16, -8), RenderedBottomRight = Vec2(16, 8) },
|
||||
{ InternalRes = Vec2(1280, 720), WindowRes = Vec2(1920, 1080), VirtualRes = Vec2(427, 242), VirtualBorder = 2, WindowTopLeft = Vec2(-213.5, -121), WindowCenter = Vec2(0, -0.90625), RenderedTopLeft = Vec2(-213.5, -121), RenderedBottomRight = Vec2(213.5, 119.1875) },
|
||||
{ InternalRes = Vec2(1280, 720), WindowRes = Vec2(1920, 1200), VirtualRes = Vec2(427, 242), VirtualBorder = 2, WindowTopLeft = Vec2(-213.5, -134.34375), WindowCenter = Vec2(0, -0.90625), RenderedTopLeft = Vec2(-213.5, -121), RenderedBottomRight = Vec2(213.5, 119.1875) },
|
||||
{ InternalRes = Vec2(1280, 720), WindowRes = Vec2(2048, 1080), VirtualRes = Vec2(427, 242), VirtualBorder = 2, WindowTopLeft = Vec2(-227.73333, -121), WindowCenter = Vec2(0, -0.90625), RenderedTopLeft = Vec2(-213.5, -121), RenderedBottomRight = Vec2(213.5, 119.1875) },
|
||||
}
|
||||
|
||||
---Tests all possible test cases.
|
||||
---Throws an error in case any test fails.
|
||||
local function testToWindow()
|
||||
for i, v in ipairs(testTable) do
|
||||
Coords.InternalResolution, Coords.WindowResolution, Coords.VirtualResolution, Coords.VirtualBorder = v.InternalRes, v.WindowRes, v.VirtualRes, v.VirtualBorder
|
||||
|
||||
---@type Vec2
|
||||
local viewportCenter = Vec2(0, 0)
|
||||
|
||||
---@type Vec2, Vec2
|
||||
local world, expected = v.WindowTopLeft, Vec2(0, 0)
|
||||
local window = Coords:ToWindow(world, viewportCenter)
|
||||
assert(window:EqualTo(expected, 0.001), string.format("test case %d: Coords:ToWindow(%q) failed. Got %q, expected %q", i, tostring(world), tostring(window), tostring(expected)))
|
||||
|
||||
---@type Vec2, Vec2
|
||||
local world, expected = v.WindowCenter, v.WindowRes / 2
|
||||
local window = Coords:ToWindow(world, viewportCenter)
|
||||
assert(window:EqualTo(expected, 0.001), string.format("test case %d: Coords:ToWindow(%q) failed. Got %q, expected %q", i, tostring(world), tostring(window), tostring(expected)))
|
||||
end
|
||||
end
|
||||
|
||||
---Tests all possible test cases.
|
||||
---Throws an error in case any test fails.
|
||||
local function testToWorld()
|
||||
for i, v in ipairs(testTable) do
|
||||
Coords.InternalResolution, Coords.WindowResolution, Coords.VirtualResolution, Coords.VirtualBorder = v.InternalRes, v.WindowRes, v.VirtualRes, v.VirtualBorder
|
||||
|
||||
---@type Vec2
|
||||
local viewportCenter = Vec2(0, 0)
|
||||
|
||||
---@type Vec2, Vec2
|
||||
local window, expected = Vec2(0, 0), v.WindowTopLeft
|
||||
local world = Coords:ToWorld(window, viewportCenter)
|
||||
assert(world:EqualTo(expected, 0.001), string.format("test case %d: Coords:ToWorld(%q) failed. Got %q, expected %q", i, tostring(window), tostring(world), tostring(expected)))
|
||||
|
||||
---@type Vec2, Vec2
|
||||
local window, expected = v.WindowRes / 2, v.WindowCenter
|
||||
local world = Coords:ToWorld(window, viewportCenter)
|
||||
assert(world:EqualTo(expected, 0.001), string.format("test case %d: Coords:ToWorld(%q) failed. Got %q, expected %q", i, tostring(window), tostring(world), tostring(expected)))
|
||||
end
|
||||
end
|
||||
|
||||
---Tests all possible test cases.
|
||||
---Throws an error in case any test fails.
|
||||
local function testValidRenderingRect()
|
||||
for i, v in ipairs(testTable) do
|
||||
Coords.InternalResolution, Coords.WindowResolution, Coords.VirtualResolution, Coords.VirtualBorder = v.InternalRes, v.WindowRes, v.VirtualRes, v.VirtualBorder
|
||||
|
||||
---@type Vec2
|
||||
local viewportCenter = Vec2(0, 0)
|
||||
|
||||
local expectedTopLeft, expectedBottomRight = v.RenderedTopLeft, v.RenderedBottomRight
|
||||
|
||||
---@type Vec2, Vec2
|
||||
local validTopLeft, validBottomRight = Coords:ValidRenderingRect()
|
||||
local validTopLeftWorld, validBottomRightWorld = Coords:ToWorld(validTopLeft, viewportCenter), Coords:ToWorld(validBottomRight, viewportCenter)
|
||||
assert(validTopLeftWorld:EqualTo(expectedTopLeft, 0.001) and validBottomRightWorld:EqualTo(expectedBottomRight, 0.001),
|
||||
string.format("test case %d: Coords:ValidRenderingRect() failed. Got %q - %q, expected %q - %q",
|
||||
i, tostring(validTopLeftWorld), tostring(validBottomRightWorld), tostring(expectedTopLeft), tostring(expectedBottomRight)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---Runs all tests of this module.
|
||||
local function testAll()
|
||||
local ok, err = pcall(testToWindow)
|
||||
if not ok then
|
||||
print(string.format("testToWindow failed: %s.", err))
|
||||
end
|
||||
|
||||
local ok, err = pcall(testToWorld)
|
||||
if not ok then
|
||||
print(string.format("testToWorld failed: %s.", err))
|
||||
end
|
||||
|
||||
local ok, err = pcall(testValidRenderingRect)
|
||||
if not ok then
|
||||
print(string.format("testValidRenderingRect failed: %s.", err))
|
||||
end
|
||||
end
|
||||
|
||||
--testAll()
|
||||
|
||||
return Coords
|
@ -1,8 +1,18 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
-- Copyright (c) 2019-2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local Hilbert = {}
|
||||
|
||||
---Rotate/flip quadrant.
|
||||
---@param n integer
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param rx boolean
|
||||
---@param ry boolean
|
||||
---@return integer
|
||||
---@return integer
|
||||
local function hilbertRotate(n, x, y, rx, ry)
|
||||
if not ry then
|
||||
if rx then
|
||||
@ -15,13 +25,17 @@ local function hilbertRotate(n, x, y, rx, ry)
|
||||
return x, y
|
||||
end
|
||||
|
||||
-- Maps a variable t to a hilbert curve with the side length of 2^potSize (Power of two size)
|
||||
function mapHilbert(t, potSize)
|
||||
---Maps t in the range of [0, (2^potSize)^2-1] to a position on the hilbert curve with the side length of 2^potSize (Power of two size).
|
||||
---@param t integer
|
||||
---@param potSize integer
|
||||
---@return integer
|
||||
---@return integer
|
||||
function Hilbert.Map(t, potSize)
|
||||
local size = math.pow(2, potSize)
|
||||
local x, y = 0, 0
|
||||
|
||||
if t < 0 or t >= size * size then
|
||||
error("Variable t is outside of the range")
|
||||
error("variable t is outside of the range")
|
||||
end
|
||||
|
||||
for i = 0, potSize - 1, 1 do
|
||||
@ -46,3 +60,5 @@ function mapHilbert(t, potSize)
|
||||
|
||||
return x, y
|
||||
end
|
||||
|
||||
return Hilbert
|
1
files/libraries/luanxml
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 03d28907ccced296e5b2f8b16303a312ab4eaa3b
|
58
files/libraries/memory.lua
Normal file
@ -0,0 +1,58 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local ffi = require("ffi")
|
||||
|
||||
local Memory = {}
|
||||
|
||||
if ffi.abi'64bit' then
|
||||
ffi.cdef([[
|
||||
typedef uint64_t __uint3264;
|
||||
]])
|
||||
else
|
||||
ffi.cdef([[
|
||||
typedef uint32_t __uint3264;
|
||||
]])
|
||||
end
|
||||
|
||||
ffi.cdef([[
|
||||
typedef void VOID;
|
||||
typedef VOID *LPVOID;
|
||||
typedef int BOOL;
|
||||
typedef __uint3264 ULONG_PTR, *PULONG_PTR;
|
||||
typedef ULONG_PTR SIZE_T, *PSIZE_T;
|
||||
typedef unsigned long DWORD;
|
||||
typedef DWORD *PDWORD;
|
||||
|
||||
BOOL VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
|
||||
]])
|
||||
|
||||
Memory.PAGE_NOACCESS = 0x01
|
||||
Memory.PAGE_READONLY = 0x02
|
||||
Memory.PAGE_READWRITE = 0x04
|
||||
Memory.PAGE_WRITECOPY = 0x08
|
||||
Memory.PAGE_EXECUTE = 0x10
|
||||
Memory.PAGE_EXECUTE_READ = 0x20
|
||||
Memory.PAGE_EXECUTE_READWRITE = 0x40
|
||||
Memory.PAGE_EXECUTE_WRITECOPY = 0x80
|
||||
Memory.PAGE_GUARD = 0x100
|
||||
Memory.PAGE_NOCACHE = 0x200
|
||||
Memory.PAGE_WRITECOMBINE = 0x400
|
||||
|
||||
---Changes the protection on a region of committed pages in the virtual address space of the calling process.
|
||||
---@param addr ffi.cdata*
|
||||
---@param size integer
|
||||
---@param newProtect integer
|
||||
---@return ffi.cdata* oldProtect
|
||||
function Memory.VirtualProtect(addr, size, newProtect)
|
||||
local oldProtect = ffi.new('DWORD[1]')
|
||||
if not ffi.C.VirtualProtect(addr, size, newProtect, oldProtect) then
|
||||
error(string.format("failed to call VirtualProtect(%s, %s, %s)", addr, size, newProtect))
|
||||
end
|
||||
|
||||
return oldProtect
|
||||
end
|
||||
|
||||
return Memory
|
19
files/libraries/monitor-standby.lua
Normal file
@ -0,0 +1,19 @@
|
||||
-- Copyright (c) 2019-2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local ffi = require("ffi")
|
||||
|
||||
local MonitorStandby = {}
|
||||
|
||||
ffi.cdef([[
|
||||
int SetThreadExecutionState(int esFlags);
|
||||
]])
|
||||
|
||||
-- Reset computer and monitor standby timer.
|
||||
function MonitorStandby.ResetTimer()
|
||||
ffi.C.SetThreadExecutionState(3) -- ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
||||
end
|
||||
|
||||
return MonitorStandby
|
54
files/libraries/noita-api/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Noita API wrapper
|
||||
|
||||
This wraps the Noita API and exposes it in a more dev friendly way.
|
||||
Entities and components are returned as objects. All entity and component related functions are now methods of the respective objects.
|
||||
|
||||
The library also comes with EmmyLua annotations, so code completion, type information and other hints will work in any IDE or editor that supports this.
|
||||
(Only tested with VSCode for now)
|
||||
|
||||
## State
|
||||
|
||||
Working but incomplete.
|
||||
If something is missing, you need to add it!
|
||||
|
||||
It would be nice to have code generation to generate this library from the official files, but meh.
|
||||
But this would be too complex, as there are a lot of edge cases and stuff that has to be handled in a specific way.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Copy this library into your mod so you get the following file path: `mods/your-mod/files/libraries/noita-api/README.md`.
|
||||
2. Add the following at the beginning of your mod's `init.lua`:
|
||||
|
||||
```lua
|
||||
-- Emulate and override some functions and tables to make everything conform more to standard lua.
|
||||
-- This will make `require` work, even in sandboxes with restricted Noita API.
|
||||
local libPath = "mods/noita-mapcap/files/libraries/"
|
||||
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
||||
```
|
||||
|
||||
You need to adjust `libPath` to point into your mod's library directory.
|
||||
The trailing `/` is needed!
|
||||
|
||||
After that you can import and use the library like this:
|
||||
|
||||
```lua
|
||||
local EntityAPI = require("noita-api.entity")
|
||||
|
||||
local x, y, radius = 10, 10, 100
|
||||
|
||||
local entities = EntityAPI.GetInRadius(x, y, radius)
|
||||
for _, entity in ipairs(entities) do
|
||||
print(entity:GetName())
|
||||
|
||||
local components = entity:GetComponents("VelocityComponent")
|
||||
for _, component in ipairs(components) do
|
||||
entity:SetComponentsEnabled(component, false)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
To include the whole set of API commands, use:
|
||||
|
||||
```lua
|
||||
local NoitaAPI = require("noita-api")
|
||||
```
|
1422
files/libraries/noita-api/annotations.lua
Normal file
55
files/libraries/noita-api/camera.lua
Normal file
@ -0,0 +1,55 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
-------------
|
||||
-- Classes --
|
||||
-------------
|
||||
|
||||
local CameraAPI = {}
|
||||
|
||||
------------------------
|
||||
-- Noita API wrappers --
|
||||
------------------------
|
||||
|
||||
---
|
||||
---@param strength number
|
||||
---@param position Vec2|nil -- Defaults to camera position if not set.
|
||||
function CameraAPI.ScreenShake(strength, position)
|
||||
if position == nil then
|
||||
return GameScreenshake(strength)
|
||||
end
|
||||
return GameScreenshake(strength, position.x, position.y)
|
||||
end
|
||||
|
||||
---Returns the center position of the viewport in world/virtual coordinates.
|
||||
---@return Vec2
|
||||
function CameraAPI.GetPos()
|
||||
return Vec2(GameGetCameraPos())
|
||||
end
|
||||
|
||||
---Sets the center position of the viewport in world/virtual coordinates.
|
||||
---@param position Vec2
|
||||
function CameraAPI.SetPos(position)
|
||||
return GameSetCameraPos(position.x, position.y)
|
||||
end
|
||||
|
||||
---
|
||||
---@param isFree boolean
|
||||
function CameraAPI.SetCameraFree(isFree)
|
||||
return GameSetCameraFree(isFree)
|
||||
end
|
||||
|
||||
---Returns the camera boundary rectangle in world/virtual coordinates.
|
||||
---This may not be 100% pixel perfect with regards to what you see on the screen.
|
||||
---@return Vec2 topLeft
|
||||
---@return Vec2 bottomRight
|
||||
function CameraAPI.Bounds()
|
||||
local x, y, w, h = GameGetCameraBounds()
|
||||
return Vec2(x, y), Vec2(x + w, y + h)
|
||||
end
|
||||
|
||||
return CameraAPI
|
153
files/libraries/noita-api/compatibility.lua
Normal file
@ -0,0 +1,153 @@
|
||||
-- Copyright (c) 2019-2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Some code to make Noita's lua conform more to standard lua.
|
||||
|
||||
-- Stupid way to prevent this code from being called more than once per sandbox.
|
||||
-- Calling this lua file with dofile_once would still cause the setup function to be called multiple times.
|
||||
if _NoitaAPICompatibilityWrapperGuard_ then return function(dummy) end end
|
||||
_NoitaAPICompatibilityWrapperGuard_ = true
|
||||
|
||||
local oldPrint = print
|
||||
|
||||
-- Emulated print function that behaves more like the standard lua one.
|
||||
function print(...)
|
||||
local n = select("#", ...)
|
||||
|
||||
--for i, v in ipairs(arg) do
|
||||
local stringArgs = {}
|
||||
for i = 1, n do
|
||||
table.insert(stringArgs, tostring(select(i, ...)))
|
||||
end
|
||||
|
||||
oldPrint(unpack(stringArgs))
|
||||
end
|
||||
|
||||
-- Package doesn't exist when the Lua API is restricted.
|
||||
-- Therefore we create it here and apply some default values.
|
||||
package = package or {}
|
||||
package.path = package.path or "./?.lua" -- Allow paths relative to the working directory.
|
||||
package.preload = package.preload or {}
|
||||
package.loaded = package.loaded or {
|
||||
_G = _G,
|
||||
bit = bit,
|
||||
coroutine = coroutine,
|
||||
debug = debug,
|
||||
math = math,
|
||||
package = package,
|
||||
string = string,
|
||||
table = table,
|
||||
--io = io,
|
||||
--jit = jit,
|
||||
--os = os,
|
||||
}
|
||||
|
||||
local oldDofile = dofile
|
||||
|
||||
---Emulated dofile to execute a lua script from disk and circumvent any caching.
|
||||
---Noita for some reason caches script files (Or loads them into its virtual filesystem)(Or caches compiled bytecode), so reloading script files from disk does not work without this.
|
||||
---
|
||||
---This conforms more with standard lua.
|
||||
---@param path string
|
||||
---@return any ...
|
||||
function dofile(path)
|
||||
local func, err = loadfile(path)
|
||||
if not func then error(err) end
|
||||
|
||||
return func()
|
||||
end
|
||||
|
||||
local oldRequire = require
|
||||
|
||||
local recursionSet = {}
|
||||
|
||||
---Emulated require function in case the Lua API is restricted.
|
||||
---It's probably good enough for most use cases.
|
||||
---
|
||||
---We need to override the default require in any case, as only dofile and loadfile can access stuff in the virtual filesystem.
|
||||
---@param modName string
|
||||
---@return any ...
|
||||
function require(modName)
|
||||
-- Check if package was already loaded, return previous result.
|
||||
if package.loaded[modName] ~= nil then return package.loaded[modName] end
|
||||
|
||||
if recursionSet[modName] then
|
||||
recursionSet = {}
|
||||
error(string.format("Cyclic dependency with module %q", modName))
|
||||
end
|
||||
recursionSet[modName] = true
|
||||
|
||||
local notFoundStr = ""
|
||||
|
||||
-- Check if there is an entry in the preload table.
|
||||
local preloadFunc = package.preload[modName]
|
||||
if preloadFunc then
|
||||
local res = preloadFunc(modName)
|
||||
|
||||
if res == nil then res = true end
|
||||
package.loaded[modName] = res
|
||||
recursionSet[modName] = nil
|
||||
return res
|
||||
else
|
||||
notFoundStr = notFoundStr .. string.format("\tno field package.preload['%s']\n", modName)
|
||||
end
|
||||
|
||||
-- Load and execute scripts.
|
||||
-- Iterate over all package.path entries.
|
||||
for pathEntry in string.gmatch(package.path, "[^;]+") do
|
||||
local modPath = string.gsub(modName, "%.", "/") -- Replace "." with file path delimiter.
|
||||
local filePath = string.gsub(pathEntry, "?", modPath, 1) -- Insert modPath into "?" placeholder.
|
||||
local fixedPath = string.gsub(filePath, "^%.[\\/]", "") -- Need to remove "./" or ".\" at the beginning, as Noita allows only "data" and "mods".
|
||||
if fixedPath:sub(1, 4) == "data" or fixedPath:sub(1, 4) == "mods" then -- Ignore everything other than data and mod root path elements. It's not perfect, but this is just there to prevent console spam.
|
||||
local func, err = loadfile(fixedPath)
|
||||
if func then
|
||||
local state, res = pcall(func)
|
||||
if not state then
|
||||
recursionSet = {}
|
||||
error(res)
|
||||
end
|
||||
if res == nil then res = true end
|
||||
package.loaded[modName] = res
|
||||
recursionSet[modName] = nil
|
||||
return res
|
||||
elseif err and err:sub(1, 45) == "Error loading lua script - file doesn't exist" then -- I hate to do that.
|
||||
notFoundStr = notFoundStr .. string.format("\tno file '%s'\n", filePath)
|
||||
else
|
||||
recursionSet = {}
|
||||
error(err)
|
||||
end
|
||||
else
|
||||
notFoundStr = notFoundStr .. string.format("\tnot allowed '%s'\n", filePath)
|
||||
end
|
||||
end
|
||||
|
||||
-- Fall back to the original require, if it exists.
|
||||
if oldRequire then
|
||||
local ok, res = pcall(oldRequire, modName)
|
||||
if ok then
|
||||
recursionSet[modName] = nil
|
||||
return res
|
||||
else
|
||||
notFoundStr = notFoundStr .. string.format("\toriginal require:%s", res)
|
||||
end
|
||||
end
|
||||
|
||||
recursionSet = {}
|
||||
error(string.format("module %q not found:\n%s", modName, notFoundStr))
|
||||
end
|
||||
|
||||
---Set up some stuff so `require` works as expected.
|
||||
---@param libPath any -- Path to the libraries directory of this mod.
|
||||
local function setup(libPath)
|
||||
-- Add the library directory of the mod as base for any `require` lookups.
|
||||
package.path = package.path:gsub(";$", "") -- Get rid of any trailing semicolon.
|
||||
package.path = package.path .. ";./" .. libPath .. "?.lua"
|
||||
package.path = package.path .. ";./" .. libPath .. "?/init.lua"
|
||||
|
||||
-- Add the library directory of Noita itself.
|
||||
package.path = package.path .. ";./data/scripts/lib/?.lua" -- TODO: Get rid of Noita's lib path, create replacement libs for stuff in there
|
||||
end
|
||||
|
||||
return setup
|
201
files/libraries/noita-api/component.lua
Normal file
@ -0,0 +1,201 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local JSON = require("noita-api.json")
|
||||
|
||||
-------------
|
||||
-- Classes --
|
||||
-------------
|
||||
|
||||
local ComponentAPI = {}
|
||||
|
||||
---@class NoitaComponent
|
||||
---@field ID integer -- Noita component ID.
|
||||
local NoitaComponent = {}
|
||||
NoitaComponent.__index = NoitaComponent
|
||||
ComponentAPI.MetaTable = NoitaComponent
|
||||
|
||||
---Wraps the given component ID and returns a Noita component object.
|
||||
---@param id number|nil
|
||||
---@return NoitaComponent|nil
|
||||
function ComponentAPI.Wrap(id)
|
||||
if id == nil or type(id) ~= "number" then return nil end
|
||||
return setmetatable({ ID = id }, NoitaComponent)
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Noita API wrappers --
|
||||
------------------------
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
function NoitaComponent:AddTag(tag)
|
||||
return ComponentAddTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
function NoitaComponent:RemoveTag(tag)
|
||||
return ComponentRemoveTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
---@return boolean
|
||||
function NoitaComponent:HasTag(tag)
|
||||
return ComponentHasTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---Returns one or many values matching the type or subtypes of the requested field.
|
||||
---Reports error and returns nil if the field type is not supported or field was not found.
|
||||
---@param fieldName string
|
||||
---@return any|nil
|
||||
function NoitaComponent:GetValue(fieldName)
|
||||
return ComponentGetValue2(self.ID, fieldName) -- TODO: Rework Noita API to handle vectors, and return a vector instead of some shitty multi value result
|
||||
end
|
||||
|
||||
---Sets the value of a field. Value(s) should have a type matching the field type.
|
||||
---Reports error if the values weren't given in correct type, the field type is not supported, or the component does not exist.
|
||||
---@param fieldName string
|
||||
---@param ... any|nil -- Vectors use one argument per dimension.
|
||||
function NoitaComponent:SetValue(fieldName, ...)
|
||||
return ComponentSetValue2(self.ID, fieldName, ...) -- TODO: Rework Noita API to handle vectors, and use a vector instead of shitty multi value arguments
|
||||
end
|
||||
|
||||
---Returns one or many values matching the type or subtypes of the requested field in a component sub-object.
|
||||
---Reports error and returns nil if the field type is not supported or 'object_name' is not a metaobject.
|
||||
---
|
||||
---Reporting errors means that it spams the stdout with messages, instead of using the lua error handling. Thanks Nolla.
|
||||
---@param objectName string
|
||||
---@param fieldName string
|
||||
---@return any|nil
|
||||
function NoitaComponent:ObjectGetValue(objectName, fieldName)
|
||||
return ComponentObjectGetValue2(self.ID, objectName, fieldName) -- TODO: Rework Noita API to handle vectors, and return a vector instead of some shitty multi value result
|
||||
end
|
||||
|
||||
---Sets the value of a field in a component sub-object. Value(s) should have a type matching the field type.
|
||||
---Reports error if the values weren't given in correct type, the field type is not supported or 'object_name' is not a metaobject.
|
||||
---@param objectName string
|
||||
---@param fieldName string
|
||||
---@param ... any|nil -- Vectors use one argument per dimension.
|
||||
function NoitaComponent:ObjectSetValue(objectName, fieldName, ...)
|
||||
return ComponentObjectSetValue2(self.ID, objectName, fieldName, ...) -- TODO: Rework Noita API to handle vectors, and use a vector instead of shitty multi value arguments
|
||||
end
|
||||
|
||||
---
|
||||
---@param arrayMemberName string
|
||||
---@param typeStoredInVector "int"|"float"|"string"
|
||||
---@return number
|
||||
function NoitaComponent:GetVectorSize(arrayMemberName, typeStoredInVector)
|
||||
return ComponentGetVectorSize(self.ID, arrayMemberName, typeStoredInVector)
|
||||
end
|
||||
|
||||
---
|
||||
---@param arrayName string
|
||||
---@param typeStoredInVector "int"|"float"|"string"
|
||||
---@param index number
|
||||
---@return number|number|string|nil
|
||||
function NoitaComponent:GetVectorValue(arrayName, typeStoredInVector, index)
|
||||
return ComponentGetVectorValue(self.ID, arrayName, typeStoredInVector, index)
|
||||
end
|
||||
|
||||
---
|
||||
---@param arrayName string
|
||||
---@param typeStoredInVector "int"|"float"|"string"
|
||||
---@return number[]|number|string|nil
|
||||
function NoitaComponent:GetVector(arrayName, typeStoredInVector)
|
||||
return ComponentGetVector(self.ID, arrayName, typeStoredInVector)
|
||||
end
|
||||
|
||||
---Returns true if the given component exists and is enabled, else false.
|
||||
---@return boolean
|
||||
function NoitaComponent:GetIsEnabled()
|
||||
return ComponentGetIsEnabled(self.ID)
|
||||
end
|
||||
|
||||
---Returns a string-indexed table of string.
|
||||
---@return table<string, string>|nil
|
||||
function NoitaComponent:GetMembers()
|
||||
return ComponentGetMembers(self.ID)
|
||||
end
|
||||
|
||||
---Returns a string-indexed table of string or nil.
|
||||
---@param objectName string
|
||||
---@return table<string, string>|nil
|
||||
function NoitaComponent:ObjectGetMembers(objectName)
|
||||
return ComponentObjectGetMembers(self.ID, objectName)
|
||||
end
|
||||
|
||||
---
|
||||
---@return string string
|
||||
function NoitaComponent:GetTypeName()
|
||||
return ComponentGetTypeName(self.ID)
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
---
|
||||
---@return NoitaComponent|nil
|
||||
function ComponentAPI.GetUpdatedComponent()
|
||||
return ComponentAPI.Wrap(GetUpdatedComponentID())
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
-------------------------
|
||||
-- JSON Implementation --
|
||||
-------------------------
|
||||
|
||||
---Returns a new table with all arguments stored into keys `1`, `2`, etc. and with a field `"n"` with the total number of arguments.
|
||||
---@param ... any
|
||||
---@return table
|
||||
local function pack(...)
|
||||
local t = {...}
|
||||
t.n = select("#", ...)
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
-- Set of component keys that would return an "invalid type" error when called with ComponentGetValue2().
|
||||
-- This is more or less to get around console error spam that otherwise can't be prevented when iterating over component members.
|
||||
-- Only used inside the JSON marshaler, until there is a better solution.
|
||||
local componentValueKeysWithInvalidType = {}
|
||||
|
||||
---MarshalJSON implements the JSON marshaler interface.
|
||||
---@return string
|
||||
function NoitaComponent:MarshalJSON()
|
||||
-- Get list of members, but with correct type (instead of string values).
|
||||
local membersTable = self:GetMembers()
|
||||
local members = {}
|
||||
if membersTable then
|
||||
for k, v in pairs(membersTable) do
|
||||
if not componentValueKeysWithInvalidType[k] then
|
||||
local packedResult = pack(self:GetValue(k)) -- Try to get value with correct type. Assuming nil is an error, but this is not always the case... meh.
|
||||
if packedResult.n == 0 then
|
||||
members[k] = nil -- Write no result as nil. Basically do nothing.
|
||||
elseif packedResult.n == 1 then
|
||||
members[k] = packedResult[1] -- Write single value result as single value.
|
||||
else
|
||||
packedResult.n = nil -- Discard n field, otherwise this is not a pure array.
|
||||
members[k] = packedResult -- Write multi value result as array.
|
||||
end
|
||||
end
|
||||
if members[k] == nil then
|
||||
componentValueKeysWithInvalidType[k] = true
|
||||
--members[k] = v -- Fall back to string value of self:GetMembers().
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local resultObject = {
|
||||
typeName = self:GetTypeName(),
|
||||
members = members,
|
||||
--objectMembers = component:ObjectGetMembers
|
||||
}
|
||||
|
||||
return JSON.Marshal(resultObject)
|
||||
end
|
||||
|
||||
return ComponentAPI
|
73
files/libraries/noita-api/debug.lua
Normal file
@ -0,0 +1,73 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
-------------
|
||||
-- Classes --
|
||||
-------------
|
||||
|
||||
local DebugAPI = {}
|
||||
|
||||
------------------------
|
||||
-- Noita API wrappers --
|
||||
------------------------
|
||||
|
||||
---Returns the mouse cursor position in world coordinates.
|
||||
---@return Vec2
|
||||
function DebugAPI.GetMouseWorld()
|
||||
return Vec2(DEBUG_GetMouseWorld())
|
||||
end
|
||||
|
||||
---Draws a mark in the world at the given position.
|
||||
---@param pos Vec2 -- In world coordinates.
|
||||
---@param message string|nil -- Defaults to "".
|
||||
---@param r number|nil -- Color's red amount in the range [0, 1]. Defaults to 1.
|
||||
---@param g number|nil -- Color's green amount in the range [0, 1]. Defaults to 0.
|
||||
---@param b number|nil -- Color's blue amount in the range [0, 1]. Defaults to 0.
|
||||
function DebugAPI.Mark(pos, message, r, g, b)
|
||||
message, r, g, b = message or "", r or 1, g or 0, b or 0
|
||||
return DEBUG_MARK(pos.x, pos.y, message, r, g, b)
|
||||
end
|
||||
|
||||
---Returns true if this is a beta version of the game.
|
||||
---
|
||||
---Can return nil it seems.
|
||||
---@return boolean|nil
|
||||
function DebugAPI.IsBetaBuild()
|
||||
return GameIsBetaBuild()
|
||||
end
|
||||
|
||||
---Returns true if this is the dev version of the game (`noita_dev.exe`).
|
||||
---@return boolean
|
||||
function DebugAPI.IsDevBuild()
|
||||
return DebugGetIsDevBuild()
|
||||
end
|
||||
|
||||
---Enables the trailer mode and some other things:
|
||||
---
|
||||
--- - Disables in-game GUI.
|
||||
--- - Opens fog of war everywhere (Not the same as disabling it completely).
|
||||
--- - Enables `mTrailerMode`, whatever that does.
|
||||
---
|
||||
---No idea how to disable it, beside pressing F12 in dev build.
|
||||
function DebugAPI.EnableTrailerMode()
|
||||
return DebugEnableTrailerMode()
|
||||
end
|
||||
|
||||
---
|
||||
---@return boolean
|
||||
function DebugAPI.IsTrailerModeEnabled()
|
||||
return GameGetIsTrailerModeEnabled()
|
||||
end
|
||||
|
||||
---
|
||||
---@param pos Vec2 -- In world coordinates.
|
||||
---@return string
|
||||
function DebugAPI.BiomeMapGetFilename(pos)
|
||||
return DebugBiomeMapGetFilename(pos.x, pos.y)
|
||||
end
|
||||
|
||||
return DebugAPI
|
444
files/libraries/noita-api/entity.lua
Normal file
@ -0,0 +1,444 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local ComponentAPI = require("noita-api.component")
|
||||
local JSON = require("noita-api.json")
|
||||
|
||||
-------------
|
||||
-- Classes --
|
||||
-------------
|
||||
|
||||
local EntityAPI = {}
|
||||
|
||||
---@class NoitaEntity
|
||||
---@field ID integer -- Noita entity ID.
|
||||
local NoitaEntity = {}
|
||||
NoitaEntity.__index = NoitaEntity
|
||||
EntityAPI.MetaTable = NoitaEntity
|
||||
|
||||
---Wraps the given entity ID and returns a Noita entity object.
|
||||
---@param id number|nil
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.Wrap(id)
|
||||
if id == nil or type(id) ~= "number" then return nil end
|
||||
return setmetatable({ ID = id }, NoitaEntity)
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Noita API wrappers --
|
||||
------------------------
|
||||
|
||||
---
|
||||
---@param filename string
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- Y coordinate in world (virtual) pixels.
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.Load(filename, posX, posY) -- TODO: Change to use Vec2 object
|
||||
return EntityAPI.Wrap(EntityLoad(filename, posX, posY))
|
||||
end
|
||||
|
||||
---
|
||||
---@param filename string
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- Y coordinate in world (virtual) pixels.
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.LoadEndGameItem(filename, posX, posY) -- TODO: Change to use Vec2 object
|
||||
return EntityAPI.Wrap(EntityLoadEndGameItem(filename, posX, posY))
|
||||
end
|
||||
|
||||
---
|
||||
---@param filename string
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- Y coordinate in world (virtual) pixels.
|
||||
function EntityAPI.LoadCameraBound(filename, posX, posY) -- TODO: Change to use Vec2 object
|
||||
return EntityLoadCameraBound(filename, posX, posY)
|
||||
end
|
||||
|
||||
---Creates a new entity from the given XML file, and attaches it to entity.
|
||||
---This will not load tags and other stuff, it seems.
|
||||
---@param filename string
|
||||
---@param entity NoitaEntity
|
||||
function EntityAPI.LoadToEntity(filename, entity)
|
||||
return EntityLoadToEntity(filename, entity.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---Note: works only in dev builds.
|
||||
---@param filename string
|
||||
function NoitaEntity:Save(filename)
|
||||
return EntitySave(self.ID, filename)
|
||||
end
|
||||
|
||||
---
|
||||
---@param name string
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.CreateNew(name)
|
||||
return EntityAPI.Wrap(EntityCreateNew(name))
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:Kill()
|
||||
return EntityKill(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:IsAlive()
|
||||
return EntityGetIsAlive(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param componentTypeName string
|
||||
---@param tableOfComponentValues string[]
|
||||
---@return NoitaComponent|nil
|
||||
function NoitaEntity:AddComponent(componentTypeName, tableOfComponentValues)
|
||||
local componentID = EntityAddComponent(self.ID, componentTypeName, tableOfComponentValues)
|
||||
return ComponentAPI.Wrap(componentID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param component NoitaComponent
|
||||
function NoitaEntity:RemoveComponent(component)
|
||||
return EntityRemoveComponent(self.ID, component.ID)
|
||||
end
|
||||
|
||||
---Returns a table of with all components of this entity.
|
||||
---@return NoitaComponent[]
|
||||
function NoitaEntity:GetAllComponents()
|
||||
local componentIDs = EntityGetAllComponents(self.ID) or {}
|
||||
local result = {}
|
||||
for _, componentID in ipairs(componentIDs) do
|
||||
table.insert(result, ComponentAPI.Wrap(componentID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Returns a table of components filtered by the given parameters.
|
||||
---@param componentTypeName string
|
||||
---@param tag string|nil
|
||||
---@return NoitaComponent[]
|
||||
function NoitaEntity:GetComponents(componentTypeName, tag)
|
||||
local componentIDs
|
||||
if tag ~= nil then
|
||||
componentIDs = EntityGetComponent(self.ID, componentTypeName, tag) or {}
|
||||
else
|
||||
componentIDs = EntityGetComponent(self.ID, componentTypeName) or {}
|
||||
end
|
||||
local result = {}
|
||||
for _, componentID in ipairs(componentIDs) do
|
||||
table.insert(result, ComponentAPI.Wrap(componentID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Returns the first component of this entity that fits the given parameters.
|
||||
---@param componentTypeName string
|
||||
---@param tag string|nil
|
||||
---@return NoitaComponent|nil
|
||||
function NoitaEntity:GetFirstComponent(componentTypeName, tag)
|
||||
local componentID
|
||||
if tag ~= nil then
|
||||
componentID = EntityGetFirstComponent(self.ID, componentTypeName, tag)
|
||||
else
|
||||
componentID = EntityGetFirstComponent(self.ID, componentTypeName)
|
||||
end
|
||||
return ComponentAPI.Wrap(componentID)
|
||||
end
|
||||
|
||||
---Sets the transform of the entity.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param rotation number
|
||||
---@param scaleX number
|
||||
---@param scaleY number
|
||||
function NoitaEntity:SetTransform(x, y, rotation, scaleX, scaleY) -- TODO: Change to use Vec2 object
|
||||
return EntitySetTransform(self.ID, x, y, rotation, scaleX, scaleY)
|
||||
end
|
||||
|
||||
---Sets the transform and tries to immediately refresh components that calculate values based on an entity's transform.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param rotation number
|
||||
---@param scaleX number
|
||||
---@param scaleY number
|
||||
function NoitaEntity:SetAndApplyTransform(x, y, rotation, scaleX, scaleY) -- TODO: Change to use Vec2 object
|
||||
return EntityApplyTransform(self.ID, x, y, rotation, scaleX, scaleY)
|
||||
end
|
||||
|
||||
---Returns the transformation of the entity.
|
||||
---@return number x, number y, number rotation, number scaleX, number scaleY
|
||||
function NoitaEntity:GetTransform() -- TODO: Change to use Vec2 object
|
||||
return EntityGetTransform(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param child NoitaEntity
|
||||
function NoitaEntity:AddChild(child)
|
||||
return EntityAddChild(self.ID, child.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@return NoitaEntity[]
|
||||
function NoitaEntity:GetAllChildren()
|
||||
local entityIDs = EntityGetAllChildren(self.ID) or {}
|
||||
local result = {}
|
||||
for _, entityID in ipairs(entityIDs) do
|
||||
table.insert(result, EntityAPI.Wrap(entityID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---
|
||||
---@return NoitaEntity|nil
|
||||
function NoitaEntity:GetParent()
|
||||
return EntityAPI.Wrap(EntityGetParent(self.ID))
|
||||
end
|
||||
|
||||
---Returns the given entity if it has no parent, otherwise walks up the parent hierarchy to the topmost parent and returns it.
|
||||
---@return NoitaEntity|nil
|
||||
function NoitaEntity:GetRootEntity()
|
||||
return EntityAPI.Wrap(EntityGetRootEntity(self.ID))
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:RemoveFromParent()
|
||||
return EntityRemoveFromParent(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
---@param enabled boolean
|
||||
function NoitaEntity:SetComponentsWithTagEnabled(tag, enabled)
|
||||
return EntitySetComponentsWithTagEnabled(self.ID, tag, enabled)
|
||||
end
|
||||
|
||||
---
|
||||
---@param component NoitaComponent
|
||||
---@param enabled boolean
|
||||
function NoitaEntity:SetComponentsEnabled(component, enabled)
|
||||
return EntitySetComponentIsEnabled(self.ID, component.ID, enabled)
|
||||
end
|
||||
|
||||
---
|
||||
---@return string
|
||||
function NoitaEntity:GetName()
|
||||
return EntityGetName(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param name string
|
||||
function NoitaEntity:SetName(name)
|
||||
return EntitySetName(self.ID, name)
|
||||
end
|
||||
|
||||
---Returns an array of all the entity's tags.
|
||||
---@return string[]
|
||||
function NoitaEntity:GetTags()
|
||||
---@type string
|
||||
local tagsString = EntityGetTags(self.ID) or ""
|
||||
local result = {}
|
||||
for tag in tagsString:gmatch('([^,]+)') do
|
||||
table.insert(result, tag)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Returns all entities with 'tag'.
|
||||
---@param tag string
|
||||
---@return NoitaEntity[]
|
||||
function EntityAPI.GetWithTag(tag)
|
||||
local entityIDs = EntityGetWithTag(tag) or {}
|
||||
local result = {}
|
||||
for _, entityID in ipairs(entityIDs) do
|
||||
table.insert(result, EntityAPI.Wrap(entityID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Returns all entities in 'radius' distance from 'x','y'.
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- X coordinate in world (virtual) pixels.
|
||||
---@param radius number -- Radius in world (virtual) pixels.
|
||||
---@return NoitaEntity[]
|
||||
function EntityAPI.GetInRadius(posX, posY, radius) -- TODO: Change to use Vec2 object
|
||||
local entityIDs = EntityGetInRadius(posX, posY, radius) or {}
|
||||
local result = {}
|
||||
for _, entityID in ipairs(entityIDs) do
|
||||
table.insert(result, EntityAPI.Wrap(entityID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Returns all entities in 'radius' distance from 'x','y' that have the given tag.
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- X coordinate in world (virtual) pixels.
|
||||
---@param radius number -- Radius in world (virtual) pixels.
|
||||
---@param tag string
|
||||
---@return NoitaEntity[]
|
||||
function EntityAPI.GetInRadiusWithTag(posX, posY, radius, tag) -- TODO: Change to use Vec2 object
|
||||
local entityIDs = EntityGetInRadiusWithTag(posX, posY, radius, tag) or {}
|
||||
local result = {}
|
||||
for _, entityID in ipairs(entityIDs) do
|
||||
table.insert(result, EntityAPI.Wrap(entityID))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---
|
||||
---@param posX number -- X coordinate in world (virtual) pixels.
|
||||
---@param posY number -- X coordinate in world (virtual) pixels.
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.GetClosest(posX, posY) -- TODO: Change to use Vec2 object
|
||||
return EntityAPI.Wrap(EntityGetClosest(posX, posY))
|
||||
end
|
||||
|
||||
---
|
||||
---@param name string
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.GetWithName(name)
|
||||
return EntityAPI.Wrap(EntityGetWithName(name))
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
function NoitaEntity:AddTag(tag)
|
||||
return EntityAddTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
function NoitaEntity:RemoveTag(tag)
|
||||
return EntityRemoveTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---
|
||||
---@param tag string
|
||||
---@return boolean
|
||||
function NoitaEntity:HasTag(tag)
|
||||
return EntityHasTag(self.ID, tag)
|
||||
end
|
||||
|
||||
---
|
||||
---@return string -- example: 'data/entities/items/flute.xml'.
|
||||
function NoitaEntity:GetFilename()
|
||||
return EntityGetFilename(self.ID)
|
||||
end
|
||||
|
||||
---Creates a component of type 'component_type_name' and adds it to 'entity_id'.
|
||||
---'table_of_component_values' should be a string-indexed table, where keys are field names and values are field values of correct type.
|
||||
---The value setting works like ComponentObjectSetValue2(), with the exception that multi value types are not supported.
|
||||
---Additional supported values are _tags:comma_separated_string and _enabled:bool, which basically work like the those fields work in entity XML files.
|
||||
---Returns the created component, if creation succeeded, or nil.
|
||||
---@param componentTypeName string
|
||||
---@param tableOfComponentValues table<string, any>
|
||||
---@return NoitaComponent|nil
|
||||
function NoitaEntity:EntityAddComponent(componentTypeName, tableOfComponentValues)
|
||||
local componentID = EntityAddComponent2(self.ID, componentTypeName, tableOfComponentValues)
|
||||
return ComponentAPI.Wrap(componentID)
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
---
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.GetUpdatedEntity()
|
||||
return EntityAPI.Wrap(GetUpdatedEntityID())
|
||||
end
|
||||
|
||||
---
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.GetWorldStateEntity()
|
||||
return EntityAPI.Wrap(GameGetWorldStateEntity())
|
||||
end
|
||||
|
||||
---
|
||||
---@return NoitaEntity|nil
|
||||
function EntityAPI.GetPlayerStatsEntity()
|
||||
return EntityAPI.Wrap(GameGetPlayerStatsEntity())
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
---
|
||||
function NoitaEntity:RegenItemAction()
|
||||
return GameRegenItemAction(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:RegenItemActionsInContainer()
|
||||
return GameRegenItemActionsInContainer(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:RegenItemActionsInPlayer()
|
||||
return GameRegenItemActionsInPlayer(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param itemEntity NoitaEntity
|
||||
function NoitaEntity:KillInventoryItem(itemEntity)
|
||||
return GameKillInventoryItem(self.ID, itemEntity.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@param itemEntity NoitaEntity
|
||||
---@param doPickUpEffects boolean
|
||||
function NoitaEntity:PickUpInventoryItem(itemEntity, doPickUpEffects)
|
||||
if doPickUpEffects == nil then doPickUpEffects = true end
|
||||
return GamePickUpInventoryItem(self.ID, itemEntity.ID, doPickUpEffects)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:DropAllItems()
|
||||
return GameDropAllItems(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:DropPlayerInventoryItems()
|
||||
return GameDropPlayerInventoryItems(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
function NoitaEntity:DestroyInventoryItems()
|
||||
return GameDestroyInventoryItems(self.ID)
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
---
|
||||
---@return boolean
|
||||
function NoitaEntity:IsPlayer()
|
||||
return IsPlayer(self.ID)
|
||||
end
|
||||
|
||||
---
|
||||
---@return boolean
|
||||
function NoitaEntity:IsInvisible()
|
||||
return IsInvisible(self.ID)
|
||||
end
|
||||
|
||||
-- TODO: Add missing Noita API methods and functions.
|
||||
|
||||
-------------------------
|
||||
-- JSON Implementation --
|
||||
-------------------------
|
||||
|
||||
---MarshalJSON implements the JSON marshaler interface.
|
||||
---@return string
|
||||
function NoitaEntity:MarshalJSON()
|
||||
local result = {
|
||||
name = self:GetName(),
|
||||
filename = self:GetFilename(),
|
||||
tags = self:GetTags(),
|
||||
children = self:GetAllChildren(),
|
||||
components = self:GetAllComponents(),
|
||||
transform = {},
|
||||
}
|
||||
|
||||
result.transform.x, result.transform.y, result.transform.rotation, result.transform.scaleX, result.transform.scaleY = self:GetTransform()
|
||||
|
||||
return JSON.Marshal(result)
|
||||
end
|
||||
|
||||
return EntityAPI
|
14
files/libraries/noita-api/init.lua
Normal file
@ -0,0 +1,14 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
return {
|
||||
Camera = require("noita-api.camera"),
|
||||
Component = require("noita-api.component"),
|
||||
Debug = require("noita-api.debug"),
|
||||
Entity = require("noita-api.entity"),
|
||||
JSON = require("noita-api.json"),
|
||||
Utils = require("noita-api.utils"),
|
||||
Vec2 = require("noita-api.vec2"),
|
||||
}
|
178
files/libraries/noita-api/json.lua
Normal file
@ -0,0 +1,178 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Simple library to marshal JSON values. Mainly for Noita.
|
||||
|
||||
local lib = {}
|
||||
|
||||
---Maps single characters to escaped strings.
|
||||
---
|
||||
---Copyright (c) 2020 rxi
|
||||
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
||||
local escapeCharacters = {
|
||||
["\\"] = "\\",
|
||||
["\""] = "\"",
|
||||
["\b"] = "b",
|
||||
["\f"] = "f",
|
||||
["\n"] = "n",
|
||||
["\r"] = "r",
|
||||
["\t"] = "t",
|
||||
}
|
||||
|
||||
---escapeRune returns the escaped string for a given rune.
|
||||
---
|
||||
---Copyright (c) 2020 rxi
|
||||
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
||||
---@param rune string
|
||||
---@return string
|
||||
local function escapeCharacter(rune)
|
||||
return "\\" .. (escapeCharacters[rune] or string.format("u%04x", rune:byte()))
|
||||
end
|
||||
|
||||
---escapeString returns the escaped version of the given string.
|
||||
---
|
||||
---Copyright (c) 2020 rxi
|
||||
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
||||
---@param str string
|
||||
---@return string
|
||||
local function escapeString(str)
|
||||
local result, count = str:gsub('[%z\1-\31\\"]', escapeCharacter)
|
||||
return result
|
||||
end
|
||||
|
||||
---MarshalString returns the JSON representation of a string value.
|
||||
---@param val string
|
||||
---@return string
|
||||
function lib.MarshalString(val)
|
||||
return string.format("%q", escapeString(val))
|
||||
end
|
||||
|
||||
---MarshalNumber returns the JSON representation of a number value.
|
||||
---@param val number
|
||||
---@return string
|
||||
function lib.MarshalNumber(val)
|
||||
-- Edge cases, as there is no real solution to this.
|
||||
-- JSON can't store special IEEE754 values, this is dumb as hell.
|
||||
if val ~= val then
|
||||
return "null" -- Alternatively we could output the string "NaN" (With quotes).
|
||||
elseif val <= -math.huge then
|
||||
return "-1E+600" -- Just output a stupidly large number.
|
||||
elseif val >= math.huge then
|
||||
return "1E+600" -- Just output a stupidly large number.
|
||||
end
|
||||
|
||||
return tostring(val)
|
||||
end
|
||||
|
||||
---MarshalBoolean returns the JSON representation of a boolean value.
|
||||
---@param val boolean
|
||||
---@return string
|
||||
function lib.MarshalBoolean(val)
|
||||
return tostring(val)
|
||||
end
|
||||
|
||||
---MarshalObject returns the JSON representation of a table object.
|
||||
---
|
||||
---This only works with string keys. Number keys will be converted into strings.
|
||||
---@param val table<string,any>
|
||||
---@return string
|
||||
function lib.MarshalObject(val)
|
||||
local result = "{"
|
||||
|
||||
for k, v in pairs(val) do
|
||||
result = result .. lib.MarshalString(k) .. ": " .. lib.Marshal(v)
|
||||
-- Append character depending on whether this is the last element or not.
|
||||
if next(val, k) == nil then
|
||||
result = result .. "}"
|
||||
else
|
||||
result = result .. ", "
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---MarshalArray returns the JSON representation of an array object.
|
||||
---
|
||||
---@param val table<number,any>
|
||||
---@param customMarshalFunction function|nil -- Custom function for marshalling the array values.
|
||||
---@return string
|
||||
function lib.MarshalArray(val, customMarshalFunction)
|
||||
local result = "["
|
||||
|
||||
-- TODO: Check if the type of all array entries is the same.
|
||||
|
||||
local length = #val
|
||||
for i, v in ipairs(val) do
|
||||
if customMarshalFunction then
|
||||
result = result .. customMarshalFunction(v)
|
||||
else
|
||||
result = result .. lib.Marshal(v)
|
||||
end
|
||||
-- Append character depending on whether this is the last element or not.
|
||||
if i == length then
|
||||
result = result .. "]"
|
||||
else
|
||||
result = result .. ", "
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---Marshal marshals any value into JSON representation.
|
||||
---@param val any
|
||||
---@return string
|
||||
function lib.Marshal(val)
|
||||
local t = type(val)
|
||||
|
||||
if t == "nil" then
|
||||
return "null"
|
||||
elseif t == "number" then
|
||||
return lib.MarshalNumber(val)
|
||||
elseif t == "string" then
|
||||
return lib.MarshalString(val)
|
||||
elseif t == "boolean" then
|
||||
return lib.MarshalBoolean(val)
|
||||
elseif t == "table" then
|
||||
-- Check if object implements the JSON marshaler interface.
|
||||
if val.MarshalJSON ~= nil and type(val.MarshalJSON) == "function" then
|
||||
return val:MarshalJSON()
|
||||
end
|
||||
|
||||
-- If not, fall back to array or object handling.
|
||||
local commonKeyType, commonValueType
|
||||
for k, v in pairs(val) do
|
||||
local keyType, valueType = type(k), type(v)
|
||||
commonKeyType = commonKeyType or keyType
|
||||
if commonKeyType ~= keyType then
|
||||
-- Different types detected, abort.
|
||||
commonKeyType = "mixed"
|
||||
break
|
||||
end
|
||||
commonValueType = commonValueType or valueType
|
||||
if commonValueType ~= valueType then
|
||||
-- Different types detected, abort.
|
||||
commonValueType = "mixed"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Decide based on common types.
|
||||
if commonKeyType == "number" and commonValueType ~= "mixed" then
|
||||
return lib.MarshalArray(val) -- This will falsely detect sparse integer key maps as arrays. But meh.
|
||||
elseif commonKeyType == "string" then
|
||||
return lib.MarshalObject(val) -- This will not detect if there are number keys, which would work with MarshalObject.
|
||||
elseif commonKeyType == nil and commonValueType == nil then
|
||||
return "null" -- Fallback in case of empty table. There is no other way than using null, as we don't have type information without table elements.
|
||||
end
|
||||
|
||||
error(string.format("unsupported table type. CommonKeyType = %s. CommonValueType = %s. MetaTable = %s", commonKeyType or "nil", commonValueType or "nil", getmetatable(val) or "nil"))
|
||||
end
|
||||
|
||||
error(string.format("unsupported type %q", t))
|
||||
end
|
||||
|
||||
return lib
|
29
files/libraries/noita-api/live-reload.lua
Normal file
@ -0,0 +1,29 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Allows Noita mods to reload themselves every now and then.
|
||||
-- This helps dramatically with development, as we don't have to restart Noita for every change.
|
||||
|
||||
local LiveReload = {}
|
||||
|
||||
---Reloads the mod's init file in the given interval in frames.
|
||||
---For reloading to work correctly, the mod has to be structured in a special way.
|
||||
---Like the usage of require, namespaces, correct error handling, ...
|
||||
---
|
||||
---Just put this into your `OnWorldPreUpdate` or `OnWorldPostUpdate` callback:
|
||||
---
|
||||
--- LiveReload:Reload("mods/your-mod/", 60) -- The trailing path separator is needed!
|
||||
---@param modPath string
|
||||
---@param interval integer
|
||||
function LiveReload:Reload(modPath, interval)
|
||||
interval = interval or 60
|
||||
self.Counter = (self.Counter or 0) + 1
|
||||
if self.Counter < interval then return end
|
||||
self.Counter = nil
|
||||
|
||||
dofile(modPath .. "init.lua")
|
||||
end
|
||||
|
||||
return LiveReload
|
54
files/libraries/noita-api/utils.lua
Normal file
@ -0,0 +1,54 @@
|
||||
-- Copyright (c) 2019-2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- This contains just some utilities that may be useful to have.
|
||||
|
||||
local DebugAPI = require("noita-api.debug")
|
||||
|
||||
local Utils = {}
|
||||
|
||||
---Returns if the file at filePath exists.
|
||||
---This only works correctly when API access is not restricted.
|
||||
---@param filePath string
|
||||
---@return boolean
|
||||
function Utils.FileExists(filePath)
|
||||
local f = io.open(filePath, "r")
|
||||
if f ~= nil then
|
||||
io.close(f)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local specialDirectoryDev = {
|
||||
["save-shared"] = "save_shared/",
|
||||
["save-stats"] = "save_stats/", -- Assumes that the first save is the currently used one.
|
||||
["save"] = "save00/" -- Assumes that the first save is the currently used one.
|
||||
}
|
||||
|
||||
local specialDirectory = {
|
||||
["save-shared"] = "save_shared/",
|
||||
["save-stats"] = "save00/stats/", -- Assumes that the first save is the currently used one.
|
||||
["save"] = "save00/" -- Assumes that the first save is the currently used one.
|
||||
}
|
||||
|
||||
---Returns the path to the special directory, or nil in case it couldn't be determined.
|
||||
---This only works correctly when API access is not restricted.
|
||||
---@param id "save-shared"|"save-stats"|"save"
|
||||
---@return string|nil
|
||||
function Utils.GetSpecialDirectory(id)
|
||||
if DebugAPI.IsDevBuild() then
|
||||
-- We are in the dev build.
|
||||
return "./" .. specialDirectoryDev[id]
|
||||
else
|
||||
-- We are in the normal Noita executable.
|
||||
-- Hacky way to get to LocalLow, there is just no other way to get this path. :/
|
||||
local pathPrefix = os.getenv('APPDATA'):gsub("[^\\/]+$", "") .. "LocalLow/Nolla_Games_Noita/"
|
||||
return pathPrefix .. specialDirectory[id]
|
||||
end
|
||||
end
|
||||
|
||||
return Utils
|
330
files/libraries/noita-api/vec2.lua
Normal file
@ -0,0 +1,330 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Just 2D vector stuff. Mainly for Noita.
|
||||
|
||||
-- State: Some stuff is untested.
|
||||
|
||||
---Metatable of the Vec2 object that is returned by this lib.
|
||||
---This will only contain a __call field pointing to the constructor.
|
||||
---@class Vec2Meta
|
||||
local libMetaTable = {}
|
||||
|
||||
---@class Vec2
|
||||
---@field [1] number
|
||||
---@field [2] number
|
||||
---@field x number
|
||||
---@field y number
|
||||
local Vec2 = setmetatable({}, libMetaTable)
|
||||
|
||||
-----------------
|
||||
-- Constructor --
|
||||
-----------------
|
||||
|
||||
---Creates a new vector.
|
||||
---
|
||||
--- Vec2() -- Returns a vector with zeroed coordinates.
|
||||
--- Vec2(v) -- Returns a copy of the given Vec2 object.
|
||||
--- Vec2("1.2, 3.4") -- Returns a vector with x = 1.2 and y = 3.4.
|
||||
--- Vec2(1.2, 3.4) -- Returns a vector with x = 1.2 and y = 3.4.
|
||||
---@param ... any
|
||||
---@return Vec2
|
||||
function libMetaTable:__call(...)
|
||||
local n = select("#", ...)
|
||||
if n == 0 then
|
||||
return setmetatable({ 0, 0 }, Vec2) -- Zero initialized vector.
|
||||
elseif n == 1 then
|
||||
local param = select(1, ...)
|
||||
if type(param) == "string" then
|
||||
local vector = {}
|
||||
for field in string.gmatch(param, "[^,%s]+") do
|
||||
table.insert(vector, tonumber(field))
|
||||
end
|
||||
assert(#vector == 2, string.format("parsed vector contains an invalid number of fields: %d, expected %d", #vector, 2))
|
||||
return setmetatable(vector, Vec2) -- Vector initialized with the given coordinates.
|
||||
elseif getmetatable(param) == Vec2 then
|
||||
return Vec2(param[1], param[2])
|
||||
end
|
||||
error(string.format("unsupported argument type %q", type(param)))
|
||||
elseif n == 2 then
|
||||
assert(type(select(1, ...)) == "number", string.format("first argument has type %q, expects %q", type(select(1, ...)), "number"))
|
||||
assert(type(select(2, ...)) == "number", string.format("first argument has type %q, expects %q", type(select(2, ...)), "number"))
|
||||
return setmetatable({ ... }, Vec2) -- Vector initialized with the given coordinates.
|
||||
end
|
||||
|
||||
error(string.format("called Vec2 constructor with %d argument(s)", n))
|
||||
end
|
||||
|
||||
-----------------
|
||||
-- Metamethods --
|
||||
-----------------
|
||||
|
||||
---Handle special fields, like x and y.
|
||||
---@param key any
|
||||
---@return any
|
||||
function Vec2:__index(key)
|
||||
if type(key) == "number" then return rawget(self, key) end
|
||||
if key == "x" then return rawget(self, 1) end
|
||||
if key == "y" then return rawget(self, 2) end
|
||||
return rawget(Vec2, key)
|
||||
end
|
||||
|
||||
---Handle special fields, like x and y.
|
||||
---@param key any
|
||||
function Vec2:__newindex(key, value)
|
||||
if type(key) == "number" then return rawset(self, key, value) end
|
||||
if key == "x" then rawset(self, 1, value) end
|
||||
if key == "y" then rawset(self, 2, value) end
|
||||
-- There should no way to manipulate any other keys of the object or its metatable.
|
||||
end
|
||||
|
||||
---Returns a string representation of this vector.
|
||||
---This supports a round-trip via Vec2(tostring(v)) without loss of information.
|
||||
---@return string
|
||||
function Vec2:__tostring()
|
||||
return string.format("%.16g, %.16g", self[1], self[2])
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- Mathematic metamethods --
|
||||
----------------------------
|
||||
|
||||
---Returns a new vector that is the sum v1 + v2.
|
||||
---
|
||||
---This will not mutate any vector.
|
||||
---@param v1 Vec2
|
||||
---@param v2 Vec2
|
||||
---@return Vec2
|
||||
function Vec2.__add(v1, v2)
|
||||
assert(getmetatable(v1) == Vec2 and getmetatable(v2) == Vec2, "wrong argument types. Expected two Vec2 objects")
|
||||
return Vec2(v1[1] + v2[1], v1[2] + v2[2])
|
||||
end
|
||||
|
||||
---Returns a new vector that is the difference v1 - v2.
|
||||
---
|
||||
---This will not mutate any vector.
|
||||
---@param v1 Vec2
|
||||
---@param v2 Vec2
|
||||
---@return Vec2
|
||||
function Vec2.__sub(v1, v2)
|
||||
assert(getmetatable(v1) == Vec2 and getmetatable(v2) == Vec2, "wrong argument types. Expected two Vec2 objects")
|
||||
return Vec2(v1[1] - v2[1], v1[2] - v2[2])
|
||||
end
|
||||
|
||||
---Returns a new vector that is the multiplication of a vector with a scalar.
|
||||
---
|
||||
---This will not mutate any value.
|
||||
---@param a number|Vec2
|
||||
---@param b number|Vec2
|
||||
---@return Vec2
|
||||
function Vec2.__mul(a, b)
|
||||
if type(a) == "number" and getmetatable(b) == Vec2 then
|
||||
return Vec2(b[1] * a, b[2] * a)
|
||||
elseif getmetatable(a) == Vec2 and type(b) == "number" then
|
||||
return Vec2(a[1] * b, a[2] * b)
|
||||
end
|
||||
|
||||
error(string.format("invalid combination of argument types for multiplication: %q and %q", type(a), type(b)))
|
||||
end
|
||||
|
||||
---Returns a new vector that is the division of a vector by a scalar.
|
||||
---
|
||||
---This will not mutate any value.
|
||||
---@param v Vec2
|
||||
---@param s number
|
||||
---@return Vec2
|
||||
function Vec2.__div(v, s)
|
||||
if getmetatable(v) == Vec2 and type(s) == "number" then
|
||||
return Vec2(v[1] / s, v[2] / s)
|
||||
end
|
||||
|
||||
error(string.format("invalid combination of argument types for division: %q and %q", type(v), type(s)))
|
||||
end
|
||||
|
||||
---Returns the negated vector.
|
||||
---
|
||||
---This will not mutate any value.
|
||||
---@return Vec2
|
||||
function Vec2:__unm()
|
||||
return Vec2(-self[1], -self[2])
|
||||
end
|
||||
|
||||
---Returns whether the two vectors are equal.
|
||||
---Will return false if one of the values is not a vector.
|
||||
---@param v1 Vec2
|
||||
---@param v2 Vec2
|
||||
---@return boolean
|
||||
function Vec2.__eq(v1, v2)
|
||||
if getmetatable(v1) ~= Vec2 or getmetatable(v2) ~= Vec2 then return false end
|
||||
return v1[1] == v2[1] and v1[2] == v2[2]
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Methods --
|
||||
-------------
|
||||
|
||||
---Adds v to the vector.
|
||||
---
|
||||
---This mutates self.
|
||||
---@param v Vec2
|
||||
function Vec2:Add(v)
|
||||
assert(getmetatable(v) == Vec2, string.format("wrong argument type %q, expected Vec2 object", type(v)))
|
||||
self[1], self[2] = self[1] + v[1], self[2] + v[2]
|
||||
end
|
||||
|
||||
---Subtracts v from the vector.
|
||||
---
|
||||
---This mutates self.
|
||||
---@param v Vec2
|
||||
function Vec2:Sub(v)
|
||||
assert(getmetatable(v) == Vec2, string.format("wrong argument type %q, expected Vec2 object", type(v)))
|
||||
self[1], self[2] = self[1] - v[1], self[2] - v[2]
|
||||
end
|
||||
|
||||
---Multiplies self with the given scalar.
|
||||
---
|
||||
---This mutates self.
|
||||
---@param s number
|
||||
function Vec2:Mul(s)
|
||||
assert(type(s) == "number", string.format("wrong argument type %q, expected number", type(s)))
|
||||
self[1], self[2] = self[1] * s, self[2] * s
|
||||
end
|
||||
|
||||
---Divides self by the given scalar.
|
||||
---
|
||||
---This mutates self.
|
||||
---@param s number
|
||||
function Vec2:Div(s)
|
||||
assert(type(s) == "number", string.format("wrong argument type %q, expected number", type(s)))
|
||||
self[1], self[2] = self[1] / s, self[2] / s
|
||||
end
|
||||
|
||||
---Returns a copy of self.
|
||||
---@return Vec2
|
||||
function Vec2:Copy()
|
||||
return Vec2(self)
|
||||
end
|
||||
|
||||
---Returns the vector fields as parameters.
|
||||
---@return number
|
||||
---@return number
|
||||
function Vec2:Unpack()
|
||||
return self[1], self[2]
|
||||
end
|
||||
|
||||
---Sets the vector fields to the given coordinates.
|
||||
---@param x number
|
||||
---@param y number
|
||||
function Vec2:Set(x, y)
|
||||
assert(type(x) == "number", string.format("wrong argument type %q, expected number", type(x)))
|
||||
assert(type(y) == "number", string.format("wrong argument type %q, expected number", type(y)))
|
||||
self[1], self[2] = x, y
|
||||
end
|
||||
|
||||
---Returns the squared length of the vector.
|
||||
---@return number
|
||||
function Vec2:LengthSqr()
|
||||
return self[1] ^ 2 + self[2] ^ 2
|
||||
end
|
||||
|
||||
---Returns the length of the vector.
|
||||
---@return number
|
||||
function Vec2:Length()
|
||||
return math.sqrt(self:LengthSqr())
|
||||
end
|
||||
|
||||
---Returns the squared distance of self to the given vector.
|
||||
---@param v Vec2
|
||||
---@return number
|
||||
function Vec2:DistanceSqr(v)
|
||||
return (v - self):LengthSqr()
|
||||
end
|
||||
|
||||
---Returns the distance of self to the given vector.
|
||||
---@param v Vec2
|
||||
---@return number
|
||||
function Vec2:Distance(v)
|
||||
return (v - self):Length()
|
||||
end
|
||||
|
||||
---Sets the length of the vector to 1.
|
||||
---
|
||||
---This mutates self.
|
||||
function Vec2:Normalize()
|
||||
local len = self:Length()
|
||||
self:Div(len)
|
||||
end
|
||||
|
||||
---Returns a copy of the vector with its length set to 1.
|
||||
---@return Vec2
|
||||
function Vec2:Normalized()
|
||||
local len = self:Length()
|
||||
return self / len
|
||||
end
|
||||
|
||||
---Compares this vector to the given one.
|
||||
---@param v Vec2
|
||||
---@param tolerance number -- Tolerance per field.
|
||||
---@return boolean
|
||||
function Vec2:EqualTo(v, tolerance)
|
||||
if math.abs(v[1] - self[1]) > tolerance then return false end
|
||||
if math.abs(v[2] - self[2]) > tolerance then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
---Round returns v rounded by the given method.
|
||||
---@param x number
|
||||
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
|
||||
---@return integer
|
||||
local function round(x, method)
|
||||
method = method or "nearest"
|
||||
|
||||
if method == "nearest" then
|
||||
return math.floor(x + 0.5)
|
||||
elseif method == "floor" then
|
||||
return math.floor(x)
|
||||
elseif method == "ceil" then
|
||||
return math.ceil(x)
|
||||
elseif method == "to-zero" then
|
||||
if x >= 0 then
|
||||
return math.floor(x)
|
||||
else
|
||||
return math.ceil(x)
|
||||
end
|
||||
elseif method == "away-zero" then
|
||||
if x >= 0 then
|
||||
return math.ceil(x)
|
||||
else
|
||||
return math.floor(x)
|
||||
end
|
||||
end
|
||||
|
||||
error(string.format("invalid rounding method %q", method))
|
||||
end
|
||||
|
||||
---Round rounds all vector fields individually by the given rounding method.
|
||||
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
|
||||
function Vec2:Round(method)
|
||||
self[1], self[2] = round(self[1], method), round(self[2], method)
|
||||
end
|
||||
|
||||
---Round rounds all vector fields individually by the given rounding method.
|
||||
---@param method "nearest"|"floor"|"ceil"|"to-zero"|"away-zero"|nil -- Defaults to "nearest".
|
||||
---@return Vec2
|
||||
function Vec2:Rounded(method)
|
||||
return Vec2(round(self[1], method), round(self[2], method))
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- JSON Implementation --
|
||||
-------------------------
|
||||
|
||||
---MarshalJSON implements the JSON marshaler interface.
|
||||
---@return string
|
||||
function Vec2:MarshalJSON()
|
||||
return string.format("[%.16g, %.16g]", self[1], self[2]) -- Encode as JSON array. -- TODO: Handle NaN, +Inf, -Inf, ... correctly
|
||||
end
|
||||
|
||||
return Vec2
|
118
files/libraries/process-runner.lua
Normal file
@ -0,0 +1,118 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- A simple library to run/control processes. Specifically made for the Noita map capture addon.
|
||||
-- This allows only one process to be run at a time in a given context.
|
||||
|
||||
-- No idea if this library has much use outside of this mod.
|
||||
|
||||
if not async then
|
||||
require("coroutines") -- Loads Noita's coroutines library from `data/scripts/lib/coroutines.lua`.
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Classes --
|
||||
-------------
|
||||
|
||||
local ProcessRunner = {}
|
||||
|
||||
---@class ProcessRunnerCtx
|
||||
---@field running boolean|nil
|
||||
---@field stopping boolean|nil
|
||||
---@field state table|nil
|
||||
local Context = {}
|
||||
Context.__index = Context
|
||||
|
||||
-----------------
|
||||
-- Constructor --
|
||||
-----------------
|
||||
|
||||
---Returns a new process runner context.
|
||||
---@return ProcessRunnerCtx
|
||||
function ProcessRunner.New()
|
||||
return setmetatable({}, Context)
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Methods --
|
||||
-------------
|
||||
|
||||
---Returns whether some process is running.
|
||||
---@return boolean
|
||||
function Context:IsRunning()
|
||||
return self.running or false
|
||||
end
|
||||
|
||||
---Returns whether the process needs to stop as soon as possible.
|
||||
---@return boolean
|
||||
function Context:IsStopping()
|
||||
return self.stopping or false
|
||||
end
|
||||
|
||||
---Returns a table with information about the current state of the process.
|
||||
---Will return nil if there is no process.
|
||||
---@return table|nil -- Custom to the process.
|
||||
function Context:GetState()
|
||||
return self.state
|
||||
end
|
||||
|
||||
---Tells the currently running process to stop.
|
||||
function Context:Stop()
|
||||
self.stopping = true
|
||||
end
|
||||
|
||||
---Starts a process with the three given callback functions.
|
||||
---This will just call the tree callbacks in order.
|
||||
---Everything is called from inside a coroutine, so you can use yield.
|
||||
---
|
||||
---There can only be ever one process at a time.
|
||||
---If there is already a process running, this will just do nothing.
|
||||
---@param initFunc fun(ctx:ProcessRunnerCtx)|nil -- Called first.
|
||||
---@param doFunc fun(ctx:ProcessRunnerCtx)|nil -- Called after `initFunc` has been run.
|
||||
---@param endFunc fun(ctx:ProcessRunnerCtx)|nil -- Called after `doFunc` has been run.
|
||||
---@param errFunc fun(err:string, scope:"init"|"do"|"end") -- Called on any error.
|
||||
---@return boolean -- True if process was started successfully.
|
||||
function Context:Run(initFunc, doFunc, endFunc, errFunc)
|
||||
if self.running then return false end
|
||||
self.running, self.stopping, self.state = true, false, {}
|
||||
|
||||
async(function()
|
||||
-- Init function.
|
||||
if initFunc then
|
||||
local ok, err = pcall(initFunc, self)
|
||||
if not ok then
|
||||
-- Error happened, abort.
|
||||
if endFunc then pcall(endFunc, self) end
|
||||
errFunc(err, "init")
|
||||
self.running, self.stopping = false, false
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do function.
|
||||
if doFunc then
|
||||
local ok, err = pcall(doFunc, self)
|
||||
if not ok then
|
||||
-- Error happened, abort.
|
||||
errFunc(err, "do")
|
||||
end
|
||||
end
|
||||
|
||||
-- End function.
|
||||
if endFunc then
|
||||
local ok, err = pcall(endFunc, self)
|
||||
if not ok then
|
||||
-- Error happened, abort.
|
||||
errFunc(err, "end")
|
||||
end
|
||||
end
|
||||
|
||||
self.running, self.stopping, self.state = false, false, nil
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return ProcessRunner
|
63
files/libraries/screen-capture.lua
Normal file
@ -0,0 +1,63 @@
|
||||
-- Copyright (c) 2019-2023 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
local ffi = require("ffi")
|
||||
|
||||
local ScreenCap = {}
|
||||
|
||||
local status, res = pcall(ffi.load, "mods/noita-mapcap/bin/capture-b/capture")
|
||||
if not status then
|
||||
print(string.format("Error loading capture lib: %s", res))
|
||||
return
|
||||
end
|
||||
|
||||
ffi.cdef([[
|
||||
typedef long LONG;
|
||||
typedef struct {
|
||||
LONG left;
|
||||
LONG top;
|
||||
LONG right;
|
||||
LONG bottom;
|
||||
} RECT;
|
||||
|
||||
typedef struct {
|
||||
LONG x;
|
||||
LONG y;
|
||||
LONG width;
|
||||
LONG height;
|
||||
} GLViewportDims;
|
||||
|
||||
bool GetRect(RECT* rect);
|
||||
bool Capture(RECT* rect, int x, int y, int sx, int sy);
|
||||
]])
|
||||
|
||||
---Takes a screenshot of the client area of this process' active window.
|
||||
---@param topLeft Vec2 -- Screenshot rectangle's top left coordinate relative to the window's client area in screen pixels.
|
||||
---@param bottomRight Vec2 -- Screenshot rectangle's bottom right coordinate relative to the window's client area in screen pixels. The pixel is not included in the screenshot area.
|
||||
---@param topLeftOutput Vec2 -- The corresponding scaled world coordinates of the screenshot rectangles' top left corner.
|
||||
---@param finalDimensions Vec2|nil -- The final dimensions that the screenshot will be resized to. If set to zero, no resize will happen.
|
||||
---@return boolean
|
||||
function ScreenCap.Capture(topLeft, bottomRight, topLeftOutput, finalDimensions)
|
||||
finalDimensions = finalDimensions or Vec2(0, 0)
|
||||
|
||||
local rect = ffi.new("RECT", { math.floor(topLeft.x + 0.5), math.floor(topLeft.y + 0.5), math.floor(bottomRight.x + 0.5), math.floor(bottomRight.y + 0.5) })
|
||||
return res.Capture(rect, math.floor(topLeftOutput.x + 0.5), math.floor(topLeftOutput.y + 0.5), math.floor(finalDimensions.x + 0.5), math.floor(finalDimensions.y + 0.5))
|
||||
end
|
||||
|
||||
---Returns the client rectangle of the "Main" window of this process in screen coordinates.
|
||||
---@return Vec2|nil topLeft
|
||||
---@return Vec2|nil bottomRight
|
||||
function ScreenCap.GetRect()
|
||||
local rect = ffi.new("RECT")
|
||||
if not res.GetRect(rect) then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return Vec2(rect.left, rect.top), Vec2(rect.right, rect.bottom)
|
||||
end
|
||||
|
||||
return ScreenCap
|
@ -1,8 +0,0 @@
|
||||
<Entity>
|
||||
<LuaComponent script_source_file="mods/noita-mapcap/files/init.lua"
|
||||
enable_coroutines="1"
|
||||
execute_on_added="1"
|
||||
execute_every_n_frame="-1"
|
||||
execute_times="1">
|
||||
</LuaComponent>
|
||||
</Entity>
|
4
files/magic-numbers/1024.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<MagicNumbers
|
||||
VIRTUAL_RESOLUTION_X="1024"
|
||||
VIRTUAL_RESOLUTION_Y="1024"
|
||||
></MagicNumbers>
|
4
files/magic-numbers/512.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<MagicNumbers
|
||||
VIRTUAL_RESOLUTION_X="512"
|
||||
VIRTUAL_RESOLUTION_Y="512"
|
||||
></MagicNumbers>
|
4
files/magic-numbers/64.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<MagicNumbers
|
||||
VIRTUAL_RESOLUTION_X="64"
|
||||
VIRTUAL_RESOLUTION_Y="64"
|
||||
></MagicNumbers>
|
3
files/magic-numbers/fast-cam.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<MagicNumbers
|
||||
DEBUG_FREE_CAMERA_SPEED="10"
|
||||
></MagicNumbers>
|
@ -1,9 +1,5 @@
|
||||
<MagicNumbers VIRTUAL_RESOLUTION_X="1280"
|
||||
VIRTUAL_RESOLUTION_Y="720"
|
||||
VIRTUAL_RESOLUTION_OFFSET_X="-2"
|
||||
VIRTUAL_RESOLUTION_OFFSET_Y="0"
|
||||
<MagicNumbers
|
||||
DRAW_PARALLAX_BACKGROUND="0"
|
||||
DEBUG_FREE_CAMERA_SPEED="10"
|
||||
DEBUG_NO_LOGO_SPLASHES="1"
|
||||
DEBUG_PAUSE_GRID_UPDATE="1"
|
||||
DEBUG_PAUSE_BOX2D="1"
|
||||
@ -16,5 +12,5 @@
|
||||
UI_QUICKBAR_OFFSET_X="2000"
|
||||
UI_QUICKBAR_OFFSET_Y="2000"
|
||||
UI_BARS_POS_X="2000"
|
||||
UI_BARS_POS_Y="2000">
|
||||
</MagicNumbers>
|
||||
UI_BARS_POS_Y="2000"
|
||||
></MagicNumbers>
|
4
files/magic-numbers/offset.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<MagicNumbers
|
||||
VIRTUAL_RESOLUTION_OFFSET_X="-2"
|
||||
VIRTUAL_RESOLUTION_OFFSET_Y="0"
|
||||
></MagicNumbers>
|
240
files/message.lua
Normal file
@ -0,0 +1,240 @@
|
||||
-- Copyright (c) 2019-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-----------------------
|
||||
-- Load global stuff --
|
||||
-----------------------
|
||||
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
local Coords = require("coordinates")
|
||||
local DebugAPI = require("noita-api.debug")
|
||||
|
||||
----------
|
||||
-- Code --
|
||||
----------
|
||||
|
||||
---Removes all messages with the AutoClose flag.
|
||||
---Use this before you recreate all auto closing messages.
|
||||
function Message:CloseAutoClose()
|
||||
self.List = self.List or {}
|
||||
|
||||
for k, message in pairs(self.List) do
|
||||
if message.AutoClose then
|
||||
self.List[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Add a general runtime error message to the message list.
|
||||
---This will always overwrite the last runtime error with the same id.
|
||||
---@param id string
|
||||
---@param ... string
|
||||
function Message:ShowRuntimeError(id, ...)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["RuntimeError" .. id] = {
|
||||
Type = "error",
|
||||
Lines = { ... },
|
||||
}
|
||||
end
|
||||
|
||||
---Calls func and catches any exception.
|
||||
---If there is one, a runtime error message will be shown to the user.
|
||||
---@param id string
|
||||
---@param func function
|
||||
function Message:CatchException(id, func)
|
||||
local ok, err = xpcall(func, debug.traceback)
|
||||
if not ok then
|
||||
|
||||
print(string.format("An exception happened in %s: %s", id, err))
|
||||
self:ShowRuntimeError(id, string.format("An exception happened in %s", id), err)
|
||||
end
|
||||
end
|
||||
|
||||
---Request the user to let the addon automatically reset some Noita settings.
|
||||
function Message:ShowResetNoitaSettings()
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["ResetNoitaSettings"] = {
|
||||
Type = "info",
|
||||
Lines = {
|
||||
"You requested to reset some game settings like:",
|
||||
"- Custom resolutions",
|
||||
"- Screen-shake intensity",
|
||||
" ",
|
||||
"Press the following button to reset the settings and close Noita automatically:",
|
||||
},
|
||||
Actions = {
|
||||
{ Name = "Reset and close (May corrupt current save!)", Hint = nil, HintDesc = nil, Callback = function() Modification:Reset() end },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
---Request the user to let the addon automatically set Noita settings based on the given callback.
|
||||
---@param callback function
|
||||
---@param desc string -- What's wrong.
|
||||
function Message:ShowSetNoitaSettings(callback, desc)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["SetNoitaSettings"] = {
|
||||
Type = "warning",
|
||||
Lines = {
|
||||
"It seems that not all requested settings are applied to Noita:",
|
||||
desc or "",
|
||||
" ",
|
||||
"Press the button at the bottom to set up and close Noita automatically.",
|
||||
" ",
|
||||
"You can always reset any custom settings by right clicking the `start capture`",
|
||||
"button at the top left.",
|
||||
},
|
||||
Actions = {
|
||||
{ Name = "Setup and close (May corrupt current save!)", Hint = nil, HintDesc = nil, Callback = callback },
|
||||
},
|
||||
AutoClose = true, -- This message will automatically close.
|
||||
}
|
||||
end
|
||||
|
||||
---Request the user to restart Noita.
|
||||
---@param desc string -- What's wrong.
|
||||
function Message:ShowRequestRestart(desc)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["RequestRestart"] = {
|
||||
Type = "warning",
|
||||
Lines = {
|
||||
"It seems that not all requested settings are applied to Noita:",
|
||||
desc or "",
|
||||
" ",
|
||||
"To resolve this issue, restart the game.",
|
||||
},
|
||||
AutoClose = true, -- This message will automatically close.
|
||||
}
|
||||
end
|
||||
|
||||
---Request the user to let the addon automatically set Noita settings based on the given callback.
|
||||
---@param callback function
|
||||
---@param desc string -- What's wrong.
|
||||
function Message:ShowWrongResolution(callback, desc)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["WrongResolution"] = {
|
||||
Type = "warning",
|
||||
Lines = {
|
||||
"The resolution changed:",
|
||||
desc or "",
|
||||
" ",
|
||||
"Press the button at the bottom to set up and close Noita automatically.",
|
||||
" ",
|
||||
"You can always reset any custom settings by right clicking the `start capture`",
|
||||
"button at the top left.",
|
||||
},
|
||||
Actions = {
|
||||
{ Name = "Setup and close (May corrupt current save!)", Hint = nil, HintDesc = nil, Callback = callback },
|
||||
},
|
||||
AutoClose = true, -- This message will automatically close.
|
||||
}
|
||||
end
|
||||
|
||||
---Tell the user that there are files in the output directory.
|
||||
function Message:ShowOutputNonEmpty()
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["OutputNonEmpty"] = {
|
||||
Type = "hint",
|
||||
Lines = {
|
||||
"There are already files in the output directory.",
|
||||
"If you are continuing a capture session, ignore this message.",
|
||||
" ",
|
||||
"If you are about to capture a new map, make sure to delete all files in the output directory first."
|
||||
},
|
||||
Actions = {
|
||||
{ Name = "Open output directory", Hint = nil, HintDesc = nil, Callback = function() os.execute("start .\\mods\\noita-mapcap\\output\\") end },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
---Tell the user that some settings are not optimal.
|
||||
---@param ... string
|
||||
function Message:ShowGeneralSettingsProblem(...)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["GeneralSettingsProblem"] = {
|
||||
Type = "hint",
|
||||
Lines = { ... },
|
||||
AutoClose = true, -- This message will automatically close.
|
||||
}
|
||||
end
|
||||
|
||||
---Tell the user that there is something wrong with the mod installation.
|
||||
---@param ... string
|
||||
function Message:ShowGeneralInstallationProblem(...)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["GeneralInstallationProblem"] = {
|
||||
Type = "error",
|
||||
Lines = { ... },
|
||||
}
|
||||
end
|
||||
|
||||
---Tell the user that some modification couldn't be applied because it is unsupported.
|
||||
---@param realm "config"|"magicNumbers"|"processMemory"|"filePatches"
|
||||
---@param name string
|
||||
---@param value any
|
||||
function Message:ShowModificationUnsupported(realm, name, value)
|
||||
self.List = self.List or {}
|
||||
|
||||
self.List["ModificationFailed"] = self.List["ModificationFailed"] or {
|
||||
Type = "warning",
|
||||
}
|
||||
|
||||
-- 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
|
612
files/modification.lua
Normal file
@ -0,0 +1,612 @@
|
||||
-- Copyright (c) 2022-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Noita settings/configuration modifications.
|
||||
-- We try to keep persistent modifications to a minimum, but some things have to be changed in order for the mod to work correctly.
|
||||
|
||||
-- There are 4 ways Noita can be modified by code:
|
||||
-- - `config.xml`: These are persistent, and Noita needs to be force closed when changed from inside a mod.
|
||||
-- - `magic_numbers.xml`: Persistent per world, can only be applied at mod startup.
|
||||
-- - Process memory: Volatile, can be modified at runtime. Needs correct memory addresses to function.
|
||||
-- - File patching: Volatile, can only be applied at mod startup.
|
||||
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
local CameraAPI = require("noita-api.camera")
|
||||
local Coords = require("coordinates")
|
||||
local ffi = require("ffi")
|
||||
local Memory = require("memory")
|
||||
local NXML = require("luanxml.nxml")
|
||||
local Utils = require("noita-api.utils")
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
local DebugAPI = require("noita-api.debug")
|
||||
|
||||
----------
|
||||
-- Code --
|
||||
----------
|
||||
|
||||
---Reads the current config from `config.xml` and returns it as table.
|
||||
---@return table<string, string> config
|
||||
function Modification.GetConfig()
|
||||
local configFilename = Utils.GetSpecialDirectory("save-shared") .. "config.xml"
|
||||
|
||||
-- Read and modify config.
|
||||
local f, err = io.open(configFilename, "r")
|
||||
if not f then error(string.format("failed to read config file: %s", err)) end
|
||||
local xml = NXML.parse(f:read("*a"))
|
||||
|
||||
f:close()
|
||||
|
||||
return xml.attr
|
||||
end
|
||||
|
||||
---Will update Noita's `config.xml` with the values in the given table.
|
||||
---
|
||||
---This will force close Noita!
|
||||
---@param config table<string, string> -- List of `config.xml` attributes that should be changed.
|
||||
function Modification.SetConfig(config)
|
||||
local configFilename = Utils.GetSpecialDirectory("save-shared") .. "config.xml"
|
||||
|
||||
-- Read and modify config.
|
||||
local f, err = io.open(configFilename, "r")
|
||||
if not f then error(string.format("failed to read config file: %s", err)) end
|
||||
local xml = NXML.parse(f:read("*a"))
|
||||
|
||||
for k, v in pairs(config) do
|
||||
xml.attr[k] = v
|
||||
end
|
||||
|
||||
f:close()
|
||||
|
||||
-- Write modified config back.
|
||||
local f, err = io.open(configFilename, "w")
|
||||
if not f then error(string.format("failed to create config file: %s", err)) end
|
||||
f:write(tostring(xml))
|
||||
f:close()
|
||||
|
||||
-- We need to force close Noita, so it doesn't have any chance to overwrite the file.
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
---Will update Noita's `magic_numbers.xml` with the values in the given table.
|
||||
---
|
||||
---Should be called on mod initialization only.
|
||||
---@param magic table<string, string> -- List of `magic_numbers.xml` attributes that should be changed.
|
||||
function Modification.SetMagicNumbers(magic)
|
||||
local xml = NXML.new_element("MagicNumbers", magic)
|
||||
|
||||
-- Write magic number file.
|
||||
local f, err = io.open("mods/noita-mapcap/files/magic-numbers/generated.xml", "w")
|
||||
if not f then error(string.format("failed to create config file: %s", err)) end
|
||||
f:write(tostring(xml))
|
||||
f:close()
|
||||
|
||||
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/generated.xml")
|
||||
end
|
||||
|
||||
---Changes some options directly by manipulating process memory.
|
||||
---
|
||||
---Related issue: https://github.com/Dadido3/noita-mapcap/issues/14.
|
||||
---@param memory table
|
||||
function Modification.SetMemoryOptions(memory)
|
||||
-- Lookup table with the following hierarchy:
|
||||
-- DevBuild -> OS -> BuildDate -> Option -> ModFunc.
|
||||
local lookup = {
|
||||
[true] = {
|
||||
Windows = {
|
||||
{_Offset = 0x00F77B0C, _BuildString = "Build Apr 23 2021 18:36:55", -- GOG dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010E3B6C)[0] = value end, -- Can be found by using Cheat Engine while toggling options in the F7 menu.
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010E3B6D)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010E3B6E)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010E3B6F)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010E3B70)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010E3B71)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010E3B72)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x010E3B73)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00F80384, _BuildString = "Build Apr 23 2021 18:40:40", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010EDEBC)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010EDEBD)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010EDEBE)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010EDEBF)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010EDEC0)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010EDEC1)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010EDEC2)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x010EDEC3)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00F8A7B4, _BuildString = "Build Mar 11 2023 14:05:19", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010F80EC)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010F80ED)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010F80EE)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F80EF)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010F80F0)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F80F1)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F80F2)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x010F80F3)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00F8A8A4, _BuildString = "Build Jun 19 2023 14:14:52", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010F810C)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010F810D)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010F810E)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F810F)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010F8110)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F8111)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F8112)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x010F8113)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00F82464, _BuildString = "Build Jul 26 2023 23:06:16", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010E9A5C)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010E9A5D)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010E9A5E)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010E9A5F)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010E9A60)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010E9A61)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010E9A62)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x010E9A63)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00FA654C, _BuildString = "Build Dec 19 2023 18:34:31", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x011154BC)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x011154BD)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x011154BE)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x011154BF)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x011154C0)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x011154C1)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x011154C2)[0] = value end,
|
||||
mFreezeAI = function(value) ffi.cast("char*", 0x011154C3)[0] = value end,
|
||||
},
|
||||
{_Offset = 0x00F8A9DC, _BuildString = "Build Dec 21 2023 00:07:29", -- Steam dev build.
|
||||
mPostFxDisabled = function(value) ffi.cast("char*", 0x010F814C)[0] = value end,
|
||||
mGuiDisabled = function(value) ffi.cast("char*", 0x010F814D)[0] = value end,
|
||||
mGuiHalfSize = function(value) ffi.cast("char*", 0x010F814E)[0] = value end,
|
||||
mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F814F)[0] = value end,
|
||||
mTrailerMode = function(value) ffi.cast("char*", 0x010F8150)[0] = value end,
|
||||
mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F8151)[0] = value end,
|
||||
mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F8152)[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] = {
|
||||
Windows = {
|
||||
{_Offset = 0x00E1C550, _BuildString = "Build Apr 23 2021 18:44:24", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x0063D8AD) -- Can be found by searching for the pattern C6 80 20 01 00 00 >01< 8B CF E8 FB 1D. The pointer has to point to the highlighted byte.
|
||||
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 = 0x00E22E18, _BuildString = "Build Mar 11 2023 14:09:24", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x006429ED)
|
||||
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 = 0x00E22E18, _BuildString = "Build Jun 19 2023 14:18:46", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x006429ED)
|
||||
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 = 0x00E146D4, _BuildString = "Build Jul 26 2023 23:10:16", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x0064390D)
|
||||
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 = 0x00E333F4, _BuildString = "Build Dec 19 2023 18:38:23", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x00624C5D)
|
||||
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 = 0x00E23EC4, _BuildString = "Build Dec 21 2023 00:11:06", -- Steam build.
|
||||
enableModDetection = function(value)
|
||||
local ptr = ffi.cast("char*", 0x0064246D)
|
||||
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 = 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.
|
||||
|
||||
local level1 = lookup[DebugAPI.IsDevBuild()]
|
||||
level1 = level1 or {}
|
||||
|
||||
local level2 = level1[ffi.os]
|
||||
level2 = level2 or {}
|
||||
|
||||
local level3 = {}
|
||||
for _, v in ipairs(level2) do
|
||||
if ffi.string(ffi.cast("char*", v._Offset)) == v._BuildString then
|
||||
level3 = v
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in pairs(memory) do
|
||||
local modFunc = level3[k]
|
||||
if modFunc ~= nil then
|
||||
modFunc(v)
|
||||
else
|
||||
Message:ShowModificationUnsupported("processMemory", k, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Applies patches to game files based on in the given table.
|
||||
---
|
||||
---Should be called on mod initialization only.
|
||||
---@param patches table
|
||||
function Modification.PatchFiles(patches)
|
||||
-- Change constants in post_final.frag.
|
||||
if patches.PostFinalConst then
|
||||
local postFinal = ModTextFileGetContent("data/shaders/post_final.frag")
|
||||
for k, v in pairs(patches.PostFinalConst) do
|
||||
postFinal = postFinal:gsub(string.format(" %s%%s+=[^;]+;", k), string.format(" %s = %s;", k, tostring(v)), 1)
|
||||
end
|
||||
ModTextFileSetContent("data/shaders/post_final.frag", postFinal)
|
||||
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
|
||||
|
||||
---Returns tables with user requested game configuration changes.
|
||||
---@return table config -- List of `config.xml` attributes that should be changed.
|
||||
---@return table magic -- List of `magic_number.xml` attributes that should be changed.
|
||||
---@return table memory -- List of options in RAM of this process that should be changed.
|
||||
---@return table patches -- List of patches that should be applied to game files.
|
||||
function Modification.RequiredChanges()
|
||||
local config, magic, memory, patches = {}, {}, {}, {}
|
||||
|
||||
-- Does the user request a custom resolution?
|
||||
local customResolution = (ModSettingGet("noita-mapcap.custom-resolution-live") and ModSettingGet("noita-mapcap.capture-mode") == "live")
|
||||
or (ModSettingGet("noita-mapcap.custom-resolution-other") and ModSettingGet("noita-mapcap.capture-mode") ~= "live")
|
||||
|
||||
if customResolution then
|
||||
config["window_w"] = tostring(Vec2(ModSettingGet("noita-mapcap.window-resolution")).x)
|
||||
config["window_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.window-resolution")).y)
|
||||
config["internal_size_w"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).x)
|
||||
config["internal_size_h"] = tostring(Vec2(ModSettingGet("noita-mapcap.internal-resolution")).y)
|
||||
config["backbuffer_width"] = config["window_w"]
|
||||
config["backbuffer_height"] = config["window_h"]
|
||||
config["fullscreen"] = "0"
|
||||
magic["VIRTUAL_RESOLUTION_X"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).x)
|
||||
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.
|
||||
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"
|
||||
config["internal_size_h"] = "720"
|
||||
magic["VIRTUAL_RESOLUTION_X"] = "427"
|
||||
magic["VIRTUAL_RESOLUTION_Y"] = "242"
|
||||
magic["GRID_RENDER_BORDER"] = "2"
|
||||
magic["VIRTUAL_RESOLUTION_OFFSET_X"] = "-1"
|
||||
magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "-1"
|
||||
end
|
||||
|
||||
-- Also disable screen shake.
|
||||
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"
|
||||
|
||||
-- These magic numbers seem only to work in the dev build.
|
||||
magic["DEBUG_PAUSE_GRID_UPDATE"] = 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"
|
||||
|
||||
-- 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
|
||||
patches.PostFinalConst = {
|
||||
ENABLE_REFRACTION = false,
|
||||
ENABLE_LIGHTING = false,
|
||||
ENABLE_FOG_OF_WAR = false,
|
||||
ENABLE_GLOW = false,
|
||||
ENABLE_GAMMA_CORRECTION = false,
|
||||
ENABLE_PATH_DEBUG = false,
|
||||
FOG_FOREGROUND = "vec4(0.0,0.0,0.0,1.0)",
|
||||
FOG_BACKGROUND = "vec3(0.0,0.0,0.0)",
|
||||
FOG_FOREGROUND_NIGHT = "vec4(0.0,0.0,0.0,1.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
|
||||
|
||||
if ModSettingGet("noita-mapcap.disable-shaders-gui-ai") and DebugAPI.IsDevBuild() then
|
||||
memory["mPostFxDisabled"] = 1
|
||||
memory["mGuiDisabled"] = 1
|
||||
memory["mFreezeAI"] = 1
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if ModSettingGet("noita-mapcap.disable-mod-detection") and not DebugAPI.IsDevBuild() then
|
||||
memory["enableModDetection"] = 0
|
||||
else
|
||||
-- Don't actively (re)enable mod detection.
|
||||
--memory["enableModDetection"] = 1
|
||||
end
|
||||
|
||||
-- Disables or hides most of the UI.
|
||||
-- The game is still somewhat playable this way.
|
||||
if ModSettingGet("noita-mapcap.disable-ui") then
|
||||
magic["INVENTORY_GUI_ALWAYS_VISIBLE"] = "0"
|
||||
magic["UI_BARS2_OFFSET_X"] = "100"
|
||||
else
|
||||
-- Reset to default.
|
||||
magic["INVENTORY_GUI_ALWAYS_VISIBLE"] = "1"
|
||||
magic["UI_BARS2_OFFSET_X"] = "-40"
|
||||
end
|
||||
|
||||
return config, magic, memory, patches
|
||||
end
|
||||
|
||||
---Sets the camera free if required by the mod settings.
|
||||
---@param force boolean|nil -- If true, the camera will be set free regardless.
|
||||
function Modification.SetCameraFree(force)
|
||||
if force ~= nil then CameraAPI.SetCameraFree(force) return end
|
||||
|
||||
local captureMode = ModSettingGet("noita-mapcap.capture-mode")
|
||||
local spiralOrigin = ModSettingGet("noita-mapcap.capture-mode-spiral-origin")
|
||||
|
||||
-- Allow free roaming when in spiral mode with origin being the current position.
|
||||
if captureMode == "spiral" and spiralOrigin == "current" then
|
||||
CameraAPI.SetCameraFree(true)
|
||||
return
|
||||
end
|
||||
|
||||
CameraAPI.SetCameraFree(false)
|
||||
end
|
||||
|
||||
---Will change the game settings according to `Modification.RequiredChanges()`.
|
||||
---
|
||||
---This will force close Noita!
|
||||
function Modification.AutoSet()
|
||||
local config, magic = Modification.RequiredChanges()
|
||||
Modification.SetConfig(config)
|
||||
end
|
||||
|
||||
---Will reset all persistent settings that may have been changed by this mod.
|
||||
---
|
||||
---This will force close Noita!
|
||||
function Modification.Reset()
|
||||
local config = {
|
||||
window_w = "1280",
|
||||
window_h = "720",
|
||||
internal_size_w = "1280",
|
||||
internal_size_h = "720",
|
||||
backbuffer_width = "1280",
|
||||
backbuffer_height = "720",
|
||||
screenshake_intensity = "0.7",
|
||||
}
|
||||
|
||||
Modification.SetConfig(config)
|
||||
end
|
32
files/overrides/perks/perk.lua
Normal file
@ -0,0 +1,32 @@
|
||||
-- Copyright (c) 2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
-- Emulate and override some functions and tables to make everything conform more to standard lua.
|
||||
-- This will make `require` work, even in sandboxes with restricted Noita API.
|
||||
local libPath = "mods/noita-mapcap/files/libraries/"
|
||||
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
||||
|
||||
local EntityAPI = require("noita-api.entity")
|
||||
|
||||
local oldPerkSpawn = perk_spawn
|
||||
|
||||
---Spawns a perk.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param perkID integer
|
||||
---@param dontRemoveOtherPerks boolean
|
||||
---@return number|nil
|
||||
function perk_spawn(x, y, perkID, dontRemoveOtherPerks)
|
||||
local entity = EntityAPI.Wrap(oldPerkSpawn(x, y, perkID, dontRemoveOtherPerks))
|
||||
if entity == nil then return end
|
||||
|
||||
-- Remove the SpriteOffsetAnimatorComponent components from the entity.
|
||||
local components = entity:GetComponents("SpriteOffsetAnimatorComponent")
|
||||
for _, component in ipairs(components) do
|
||||
entity:RemoveComponent(component)
|
||||
end
|
||||
|
||||
return entity.ID
|
||||
end
|
BIN
files/ui-gfx/dismiss-8x8.png
Normal file
After Width: | Height: | Size: 171 B |
BIN
files/ui-gfx/hint-16x16.png
Normal file
After Width: | Height: | Size: 206 B |
BIN
files/ui-gfx/open-output-16x16.png
Normal file
After Width: | Height: | Size: 253 B |
BIN
files/ui-gfx/progress-a.png
Normal file
After Width: | Height: | Size: 187 B |
BIN
files/ui-gfx/progress-b.png
Normal file
After Width: | Height: | Size: 143 B |
BIN
files/ui-gfx/record-16x16.png
Normal file
After Width: | Height: | Size: 231 B |
BIN
files/ui-gfx/reset-16x16.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
files/ui-gfx/stitch-16x16.png
Normal file
After Width: | Height: | Size: 246 B |
BIN
files/ui-gfx/stop-16x16.png
Normal file
After Width: | Height: | Size: 215 B |
BIN
files/ui-gfx/warning-16x16.png
Normal file
After Width: | Height: | Size: 217 B |
341
files/ui.lua
@ -1,158 +1,215 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
-- Copyright (c) 2019-2022 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
UiCaptureDelay = 0 -- Waiting time in frames
|
||||
UiProgress = nil
|
||||
UiCaptureProblem = nil
|
||||
-----------------------
|
||||
-- Load global stuff --
|
||||
-----------------------
|
||||
|
||||
function DrawUI()
|
||||
if modGUI ~= nil then
|
||||
GuiStartFrame(modGUI)
|
||||
-- TODO: Wrap Noita utilities and wrap them into a table: https://stackoverflow.com/questions/9540732/loadfile-without-polluting-global-environment
|
||||
require("utilities") -- Loads Noita's utilities from `data/scripts/lib/utilities.lua`.
|
||||
|
||||
GuiLayoutBeginVertical(modGUI, 50, 20)
|
||||
if not UiProgress then
|
||||
-- Show informations
|
||||
local problem
|
||||
local rect = GetRect()
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
if not rect then
|
||||
GuiTextCentered(modGUI, 0, 0, '!!! WARNING !!! You are not using "Windowed" mode.')
|
||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
||||
GuiTextCentered(modGUI, 0, 0, '- Change the window mode in the game options to "Windowed"')
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
problem = true
|
||||
end
|
||||
----------
|
||||
-- Code --
|
||||
----------
|
||||
|
||||
if rect then
|
||||
local screenWidth, screenHeight = rect.right - rect.left, rect.bottom - rect.top
|
||||
local virtualWidth, virtualHeight =
|
||||
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
|
||||
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
|
||||
local ratioX, ratioY = screenWidth / virtualWidth, screenHeight / virtualHeight
|
||||
--GuiTextCentered(modGUI, 0, 0, string.format("SCREEN_RESOLUTION_*: %d, %d", screenWidth, screenHeight))
|
||||
--GuiTextCentered(modGUI, 0, 0, string.format("VIRTUAL_RESOLUTION_*: %d, %d", virtualWidth, virtualHeight))
|
||||
if math.abs(ratioX - CAPTURE_PIXEL_SIZE) > 0.0001 or math.abs(ratioY - CAPTURE_PIXEL_SIZE) > 0.0001 then
|
||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Screen and virtual resolution differ.")
|
||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
||||
GuiTextCentered(
|
||||
modGUI,
|
||||
0,
|
||||
0,
|
||||
string.format(
|
||||
"- Change the resolution in the game options to %dx%d",
|
||||
virtualWidth * CAPTURE_PIXEL_SIZE,
|
||||
virtualHeight * CAPTURE_PIXEL_SIZE
|
||||
)
|
||||
)
|
||||
GuiTextCentered(
|
||||
modGUI,
|
||||
0,
|
||||
0,
|
||||
string.format(
|
||||
"- Change the virtual resolution in the mod to %dx%d",
|
||||
screenWidth / CAPTURE_PIXEL_SIZE,
|
||||
screenHeight / CAPTURE_PIXEL_SIZE
|
||||
)
|
||||
)
|
||||
if math.abs(ratioX - ratioY) < 0.0001 then
|
||||
GuiTextCentered(modGUI, 0, 0, string.format("- Change the CAPTURE_PIXEL_SIZE in the mod to %f", ratioX))
|
||||
---Splits the given string to fit inside maxLength.
|
||||
---@param gui userdata
|
||||
---@param text string
|
||||
---@param maxLength number -- In UI pixels.
|
||||
---@return string[]
|
||||
local function splitString(gui, text, maxLength)
|
||||
local splitted = {}
|
||||
|
||||
local first, rest = text, ""
|
||||
|
||||
while first:len() > 0 do
|
||||
local width, height = GuiGetTextDimensions(gui, first, 1, 2)
|
||||
if width <= maxLength then
|
||||
table.insert(splitted, first)
|
||||
first, rest = rest, ""
|
||||
else
|
||||
first, rest = first:sub(1, -2), first:sub(-1, -1) .. rest
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return splitted
|
||||
end
|
||||
|
||||
---Returns unique IDs for the widgets.
|
||||
---`_ResetID` has to be called every time before the UI is rebuilt.
|
||||
---@return integer
|
||||
function UI:_GenID()
|
||||
self.CurrentID = (self.CurrentID or 0) + 1
|
||||
return self.CurrentID
|
||||
end
|
||||
|
||||
function UI:_ResetID()
|
||||
self.CurrentID = nil
|
||||
end
|
||||
|
||||
---Stops the UI from drawing for the next few frames.
|
||||
---@param frames integer
|
||||
function UI:SuspendDrawing(frames)
|
||||
self.suspendFrames = math.max(self.suspendFrames or 0, frames)
|
||||
end
|
||||
|
||||
function UI:_DrawToolbar()
|
||||
local gui = self.gui
|
||||
GuiZSet(gui, 0)
|
||||
|
||||
GuiLayoutBeginHorizontal(gui, 2, 2, true, 2, 2)
|
||||
|
||||
local captureMode = tostring(ModSettingGet("noita-mapcap.capture-mode"))
|
||||
|
||||
if Capture.MapCapturingCtx:IsRunning() then
|
||||
local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/stop-16x16.png")
|
||||
GuiTooltip(gui, "Stop capture", "Stop the capturing process.\n \nRight click: Reset any modifications that this mod has done to Noita.")
|
||||
if clicked then Capture:StopCapturing() end
|
||||
if clickedRight then Message:ShowResetNoitaSettings() end
|
||||
else
|
||||
local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/record-16x16.png")
|
||||
GuiTooltip(gui, string.format("Start %s capture", captureMode), "Go into mod settings to configure the capturing process.\n \nRight click: Reset any modifications that this mod has done to Noita.")
|
||||
if clicked then Capture:StartCapturing() end
|
||||
if clickedRight then Message:ShowResetNoitaSettings() end
|
||||
end
|
||||
|
||||
local clicked = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/open-output-16x16.png")
|
||||
GuiTooltip(gui, "Open output directory", "Reveals the output directory in your file browser.")
|
||||
if clicked then os.execute("start .\\mods\\noita-mapcap\\output\\") end
|
||||
|
||||
local clicked = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/stitch-16x16.png")
|
||||
GuiTooltip(gui, "Open stitch directory", "Reveals the directory of the stitching tool in your file browser.")
|
||||
if clicked then os.execute("start .\\mods\\noita-mapcap\\bin\\stitch\\") end
|
||||
|
||||
GuiLayoutEnd(gui)
|
||||
end
|
||||
|
||||
function UI:_DrawMessages(messages)
|
||||
local gui = self.gui
|
||||
|
||||
-- Abort if there is no messages list.
|
||||
if not messages then return end
|
||||
|
||||
local screenWidth, screenHeight = GuiGetScreenDimensions(gui)
|
||||
|
||||
GuiZSet(gui, 0)
|
||||
|
||||
-- Unfortunately you can't stack multiple layout containers with the same direction.
|
||||
-- So keep track of the y position manually.
|
||||
local posY = 60
|
||||
for key, message in pairs(messages) do
|
||||
GuiZSet(gui, -10)
|
||||
GuiBeginAutoBox(gui)
|
||||
|
||||
GuiLayoutBeginHorizontal(gui, 27, posY, true, 5, 0) posY = posY + 20
|
||||
|
||||
if message.Type == "warning" or message.Type == "error" then
|
||||
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/warning-16x16.png", 1, 1, 0, 0, 0, "")
|
||||
elseif message.Type == "hint" or message.Type == "info" then
|
||||
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "")
|
||||
else
|
||||
GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "")
|
||||
end
|
||||
|
||||
GuiLayoutBeginVertical(gui, 0, 0, false, 0, 0)
|
||||
if type(message.Lines) == "table" then
|
||||
for _, line in ipairs(message.Lines) do
|
||||
for splitLine in tostring(line):gmatch("[^\n]+") do
|
||||
for _, splitLine in ipairs(splitString(gui, splitLine, screenWidth - 80)) do
|
||||
GuiText(gui, 0, 0, splitLine) posY = posY + 11
|
||||
end
|
||||
GuiTextCentered(modGUI, 0, 0, '- Make sure that the console is not selected')
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
problem = true
|
||||
end
|
||||
end
|
||||
|
||||
if not fileExists("mods/noita-mapcap/bin/capture-b/capture.dll") then
|
||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Can't find library for screenshots.")
|
||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
||||
GuiTextCentered(modGUI, 0, 0, "- Redownload a release of this mod from GitHub, don't download the sourcecode")
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
problem = true
|
||||
end
|
||||
|
||||
if not fileExists("mods/noita-mapcap/bin/stitch/stitch.exe") then
|
||||
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Can't find software for stitching.")
|
||||
GuiTextCentered(modGUI, 0, 0, "You can still take screenshots, but you won't be able to stitch those screenshots.")
|
||||
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
|
||||
GuiTextCentered(modGUI, 0, 0, "- Redownload a release of this mod from GitHub, don't download the sourcecode")
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
problem = true
|
||||
end
|
||||
|
||||
if not problem then
|
||||
GuiTextCentered(modGUI, 0, 0, "No problems found.")
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
end
|
||||
|
||||
GuiTextCentered(modGUI, 0, 0, "You can freely look around and search a place to start capturing.")
|
||||
GuiTextCentered(modGUI, 0, 0, "When started the mod will take pictures automatically.")
|
||||
GuiTextCentered(modGUI, 0, 0, "Use ESC to pause, and close the game to stop the process.")
|
||||
GuiTextCentered(
|
||||
modGUI,
|
||||
0,
|
||||
0,
|
||||
'You can resume capturing just by restarting noita and pressing "Start capturing map" again,'
|
||||
)
|
||||
GuiTextCentered(modGUI, 0, 0, "the mod will skip already captured files.")
|
||||
GuiTextCentered(
|
||||
modGUI,
|
||||
0,
|
||||
0,
|
||||
'If you want to start a new map, you have to delete all images from the "output" folder!'
|
||||
)
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
if GuiButton(modGUI, 0, 0, ">> Start capturing map around view <<", 1) then
|
||||
UiProgress = {}
|
||||
startCapturingSpiral()
|
||||
end
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
if GuiButton(modGUI, 0, 0, ">> Start capturing base layout <<", 1) then
|
||||
UiProgress = {}
|
||||
startCapturingHilbert(CAPTURE_AREA_BASE_LAYOUT)
|
||||
end
|
||||
if GuiButton(modGUI, 0, 0, ">> Start capturing main world <<", 1) then
|
||||
UiProgress = {}
|
||||
startCapturingHilbert(CAPTURE_AREA_MAIN_WORLD)
|
||||
end
|
||||
if GuiButton(modGUI, 0, 0, ">> Start capturing extended map <<", 1) then
|
||||
UiProgress = {}
|
||||
startCapturingHilbert(CAPTURE_AREA_EXTENDED)
|
||||
end
|
||||
GuiTextCentered(modGUI, 0, 0, " ")
|
||||
elseif not UiProgress.Done then
|
||||
-- Show progress
|
||||
local x, y = GameGetCameraPos()
|
||||
GuiTextCentered(modGUI, 0, 0, string.format("Coordinates: %d, %d", x, y))
|
||||
GuiTextCentered(modGUI, 0, 0, string.format("Waiting %d frames...", UiCaptureDelay))
|
||||
if UiProgress.Progress then
|
||||
GuiTextCentered(
|
||||
modGUI,
|
||||
0,
|
||||
0,
|
||||
progressBarString(
|
||||
UiProgress,
|
||||
{BarLength = 100, CharFull = "l", CharEmpty = ".", Format = "|%s| [%d / %d] [%1.2f%%]"}
|
||||
)
|
||||
)
|
||||
end
|
||||
if UiCaptureProblem then
|
||||
GuiTextCentered(modGUI, 0, 0, string.format("A problem occurred while capturing: %s", UiCaptureProblem))
|
||||
end
|
||||
else
|
||||
GuiTextCentered(modGUI, 0, 0, "Done!")
|
||||
end
|
||||
GuiLayoutEnd(modGUI)
|
||||
if type(message.Actions) == "table" then
|
||||
posY = posY + 11
|
||||
for _, action in ipairs(message.Actions) do
|
||||
local clicked = GuiButton(gui, self:_GenID(), 0, 11, ">" .. action.Name .. " <") posY = posY + 11
|
||||
if action.Hint or action.HintDesc then
|
||||
GuiTooltip(gui, action.Hint or "", action.HintDesc or "")
|
||||
end
|
||||
if clicked then
|
||||
local ok, err = pcall(action.Callback)
|
||||
if not ok then
|
||||
Message:ShowRuntimeError("MessageAction", "Message action error:", err)
|
||||
end
|
||||
messages[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
GuiLayoutEnd(gui)
|
||||
|
||||
if not message.AutoClose then
|
||||
local clicked = GuiImageButton(gui, self:_GenID(), 5, 0, "", "mods/noita-mapcap/files/ui-gfx/dismiss-8x8.png")
|
||||
--GuiTooltip(gui, "Dismiss message", "")
|
||||
if clicked then messages[key] = nil end
|
||||
end
|
||||
|
||||
GuiLayoutEnd(gui)
|
||||
|
||||
GuiZSet(gui, -9)
|
||||
GuiEndAutoBoxNinePiece(gui, 5, 0, 0, false, 0, "data/ui_gfx/decorations/9piece0_gray.png", "data/ui_gfx/decorations/9piece0_gray.png")
|
||||
end
|
||||
end
|
||||
|
||||
async_loop(
|
||||
function()
|
||||
-- When capturing is active, DrawUI is called from a different coroutine
|
||||
-- This ensures that the text is drawn *after* a screenshot has been grabbed
|
||||
if not UiProgress or UiProgress.Done then DrawUI() end
|
||||
wait(0)
|
||||
function UI:_DrawProgress()
|
||||
local gui = self.gui
|
||||
|
||||
-- Check if there is progress to show.
|
||||
local state = Capture.MapCapturingCtx:GetState()
|
||||
if not state then return end
|
||||
|
||||
local factor
|
||||
if state.Current and state.Max > 0 then
|
||||
factor = state.Current / state.Max
|
||||
end
|
||||
)
|
||||
|
||||
local width, height = GuiGetScreenDimensions(gui)
|
||||
local widthHalf, heightHalf = math.floor(width/2), math.floor(height/2)
|
||||
GuiZSet(gui, -20)
|
||||
|
||||
local barWidth = width - 60
|
||||
local y = heightHalf
|
||||
if factor then
|
||||
GuiImageNinePiece(gui, self:_GenID(), 30, y, barWidth, 9, 1, "mods/noita-mapcap/files/ui-gfx/progress-a.png", "mods/noita-mapcap/files/ui-gfx/progress-a.png")
|
||||
GuiImageNinePiece(gui, self:_GenID(), 30, y, math.floor(barWidth * factor + 0.5), 9, 1, "mods/noita-mapcap/files/ui-gfx/progress-b.png", "mods/noita-mapcap/files/ui-gfx/progress-b.png")
|
||||
GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter)
|
||||
GuiText(gui, widthHalf, y, string.format("%d of %d (%.1f%%)", state.Current, state.Max, factor*100)) y = y + 11
|
||||
y = y + 15
|
||||
end
|
||||
|
||||
if state.WaitFrames then
|
||||
GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter)
|
||||
GuiText(gui, widthHalf, y, string.format("Waiting for %d frames.", state.WaitFrames)) y = y + 11
|
||||
end
|
||||
end
|
||||
|
||||
function UI:Draw()
|
||||
self.gui = self.gui or GuiCreate()
|
||||
local gui = self.gui
|
||||
|
||||
-- Skip drawing if we are asked to do so.
|
||||
-- TODO: Find a way to suspend UI drawing, but still being able to receive events
|
||||
if self.suspendFrames and self.suspendFrames > 0 then self.suspendFrames = self.suspendFrames - 1 return end
|
||||
self.suspendFrames = nil
|
||||
|
||||
-- Reset ID generator.
|
||||
self:_ResetID()
|
||||
|
||||
GuiStartFrame(gui)
|
||||
|
||||
GuiIdPushString(gui, "noita-mapcap")
|
||||
|
||||
self:_DrawToolbar()
|
||||
self:_DrawMessages(Message.List)
|
||||
self:_DrawProgress()
|
||||
|
||||
GuiIdPop(gui)
|
||||
end
|
||||
|
107
files/util.lua
@ -1,107 +0,0 @@
|
||||
-- Copyright (c) 2019-2020 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
function SplitStringByLength(string, length)
|
||||
local chunks = {}
|
||||
for i = 1, #string, length do
|
||||
table.insert(chunks, string:sub(i, i + length - 1))
|
||||
end
|
||||
return chunks
|
||||
end
|
||||
|
||||
-- Improved version of GamePrint, that behaves more like print.
|
||||
local oldGamePrint = GamePrint
|
||||
function GamePrint(...)
|
||||
local arg = {...}
|
||||
|
||||
local result = ""
|
||||
|
||||
for i, v in ipairs(arg) do
|
||||
result = result .. tostring(v) .. " "
|
||||
end
|
||||
|
||||
for line in result:gmatch("[^\r\n]+") do
|
||||
for i, v in ipairs(splitStringByLength(line, 100)) do
|
||||
oldGamePrint(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function getPlayer()
|
||||
local players = EntityGetWithTag("player_unit")
|
||||
if players == nil or #players < 1 then
|
||||
return nil
|
||||
end
|
||||
return players[1]
|
||||
end
|
||||
|
||||
function getPlayerPos()
|
||||
return EntityGetTransform(getPlayer())
|
||||
end
|
||||
|
||||
function teleportPlayer(x, y)
|
||||
EntitySetTransform(getPlayer(), x, y)
|
||||
end
|
||||
|
||||
function setPlayerHP(hp)
|
||||
local damagemodels = EntityGetComponent(getPlayer(), "DamageModelComponent")
|
||||
|
||||
if damagemodels ~= nil then
|
||||
for i, damagemodel in ipairs(damagemodels) do
|
||||
ComponentSetValue(damagemodel, "max_hp", hp)
|
||||
ComponentSetValue(damagemodel, "hp", hp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function addEffectToEntity(entity, gameEffect)
|
||||
local gameEffectComp = GetGameEffectLoadTo(entity, gameEffect, true)
|
||||
if gameEffectComp ~= nil then
|
||||
ComponentSetValue(gameEffectComp, "frames", "-1")
|
||||
end
|
||||
end
|
||||
|
||||
function addPerkToPlayer(perkID)
|
||||
local playerEntity = getPlayer()
|
||||
local x, y = getPlayerPos()
|
||||
local perkData = get_perk_with_id(perk_list, perkID)
|
||||
|
||||
-- Add effect
|
||||
addEffectToEntity(playerEntity, perkData.game_effect)
|
||||
|
||||
-- Add ui icon etc
|
||||
--[[local perkIcon = EntityCreateNew("")
|
||||
EntityAddComponent(
|
||||
perkIcon,
|
||||
"UIIconComponent",
|
||||
{
|
||||
name = perkData.ui_name,
|
||||
description = perkData.ui_description,
|
||||
icon_sprite_file = perkData.ui_icon
|
||||
}
|
||||
)
|
||||
EntityAddChild(playerEntity, perkIcon)]]
|
||||
|
||||
--local effect = EntityLoad("data/entities/misc/effect_protection_all.xml", x, y)
|
||||
--EntityAddChild(playerEntity, effect)
|
||||
end
|
||||
|
||||
function fileExists(fileName)
|
||||
local f = io.open(fileName, "r")
|
||||
if f ~= nil then
|
||||
io.close(f)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function progressBarString(progress, look)
|
||||
local factor = progress.Progress / progress.Max
|
||||
local count = math.ceil(look.BarLength * factor)
|
||||
local barString = string.rep(look.CharFull, count) .. string.rep(look.CharEmpty, look.BarLength - count)
|
||||
|
||||
return string.format(look.Format, barString, progress.Progress, progress.Max, factor * 100)
|
||||
end
|
45
go.mod
@ -1,26 +1,43 @@
|
||||
module github.com/Dadido3/noita-mapcap
|
||||
|
||||
go 1.18
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/cheggaaa/pb/v3 v3.1.0
|
||||
github.com/google/hilbert v0.0.0-20181122061418-320f2e35a565
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
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/coreos/go-semver v0.3.1
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
golang.org/x/image v0.0.0-20220617043117-41969df76e82
|
||||
github.com/tdewolff/canvas v0.0.0-20231218015800-2ad5075e9362
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/benoitkugler/textlayout v0.3.0 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.3 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/gen2brain/shm v0.1.0 // indirect
|
||||
github.com/go-fonts/latin-modern v0.3.2 // indirect
|
||||
github.com/go-fonts/liberation v0.3.2 // indirect
|
||||
github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea // indirect
|
||||
github.com/go-text/typesetting v0.0.0-20231219150831-cc0073efdbb4 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
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.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
|
||||
)
|
||||
|
133
go.sum
@ -1,8 +1,25 @@
|
||||
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
|
||||
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
|
||||
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
|
||||
github.com/1lann/promptui v0.8.1-0.20220708222609-81fad96dd5e1 h1:LejjvYg4tCW5HO7q/1nzPrprh47oUD9OUySQ29pDp5c=
|
||||
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/cheggaaa/pb/v3 v3.1.0 h1:3uouEsl32RL7gTiQsuaXD4Bzbfl5tGztXGUvXbs4O04=
|
||||
github.com/cheggaaa/pb/v3 v3.1.0/go.mod h1:YjrevcBqadFDaGQKRdmZxTY42pXEqda48Ea3lt0K/BE=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk=
|
||||
github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
|
||||
github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2hEUDeTlz90Ng=
|
||||
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/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=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
@ -12,47 +29,89 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
||||
github.com/google/hilbert v0.0.0-20181122061418-320f2e35a565 h1:KBAlCAY6eLC44FiEwbzEbHnpVlw15iVM4ZK8QpRIp4U=
|
||||
github.com/google/hilbert v0.0.0-20181122061418-320f2e35a565/go.mod h1:xn6EodFfRzV6j8NXQRPjngeHWlrpOrsZPKuuLRThU1k=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 h1:qq2nCpSrXrmvDGRxW0ruW9BVEV1CN2a9YDOExdt+U0o=
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/go-fonts/latin-modern v0.3.2 h1:M+Sq24Dp0ZRPf3TctPnG1MZxRblqyWC/cRUL9WmdaFc=
|
||||
github.com/go-fonts/latin-modern v0.3.2/go.mod h1:9odJt4NbRrbdj4UAMuLVd4zEukf6aAEKnDaQga0whqQ=
|
||||
github.com/go-fonts/liberation v0.3.2 h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ=
|
||||
github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI=
|
||||
github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc=
|
||||
github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/go-text/typesetting v0.0.0-20231219150831-cc0073efdbb4 h1:MKnPksPov832ct2c9a40QUB+2lgf2pBo7N92TxAAFA8=
|
||||
github.com/go-text/typesetting v0.0.0-20231219150831-cc0073efdbb4/go.mod h1:MrLApvxyzSW0MhQqLc484jkUWYX4wsEvEqDosB5Io80=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237 h1:YOp8St+CM/AQ9Vp4XYm4272E77MptJDHkwypQHIRl9Q=
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237/go.mod h1:e7qQlOY68wOz4b82D7n+DdaptZAi+SHW0+yKiWZzEYE=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
|
||||
golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/tdewolff/canvas v0.0.0-20231218015800-2ad5075e9362 h1:E9HkFtZcjoZQCaSyb2Finw4jhC0NWOJ2DCCoAMYrXLg=
|
||||
github.com/tdewolff/canvas v0.0.0-20231218015800-2ad5075e9362/go.mod h1:hGxWCl1a3KdYh6pxYy9sa9jLAlmKLMeuCSCjjy39iVE=
|
||||
github.com/tdewolff/minify/v2 v2.20.10 h1:iz9IkdRqD2pyneib/AvTas23RRG5TnuUFNcNVKmL/jU=
|
||||
github.com/tdewolff/minify/v2 v2.20.10/go.mod h1:xSJ9fXIfyuEMex88JT4jl8GvXnl/RzWNdqD96AqKlX0=
|
||||
github.com/tdewolff/parse/v2 v2.7.7 h1:V+50eFDH7Piw4IBwH8D8FtYeYbZp3T4SCtIvmBSIMyc=
|
||||
github.com/tdewolff/parse/v2 v2.7.7/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20231121141655-2d5236e10ae4 h1:CmTImZFElFD07EUPqgMEraDMnJX1E5oJKeibjg0SC2c=
|
||||
github.com/tdewolff/test v1.0.11-0.20231121141655-2d5236e10ae4/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
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.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.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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.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.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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
star-tex.org/x/tex v0.4.0 h1:AXUwgpnHLCxZUWW3qrmjv6ezNhH3PjUVBuLLejz2cgU=
|
||||
star-tex.org/x/tex v0.4.0/go.mod h1:w91ycsU/DkkCr7GWr60GPWqp3gn2U+6VX71T0o8k8qE=
|
||||
|
Before Width: | Height: | Size: 526 KiB |
Before Width: | Height: | Size: 292 KiB |
BIN
images/mod-settings-area.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
images/mod-settings-live.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
images/requester-autosetup.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
images/title.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
171
init.lua
@ -1,13 +1,168 @@
|
||||
dofile("mods/noita-mapcap/files/init.lua")
|
||||
-- Copyright (c) 2022-2024 David Vogel
|
||||
--
|
||||
-- This software is released under the MIT License.
|
||||
-- https://opensource.org/licenses/MIT
|
||||
|
||||
function OnPlayerSpawned(player_entity)
|
||||
--EntityLoad("mods/noita-mapcap/files/luacomponent.xml") -- ffi isn't accessible from inside lua components, scrap that idea
|
||||
modGUI = GuiCreate()
|
||||
GameSetCameraFree(true)
|
||||
-----------------------
|
||||
-- Load global stuff --
|
||||
-----------------------
|
||||
|
||||
-- Emulate and override some functions and tables to make everything conform more to standard lua.
|
||||
-- This will make `require` work, even in sandboxes with restricted Noita API.
|
||||
local libPath = "mods/noita-mapcap/files/libraries/"
|
||||
dofile(libPath .. "noita-api/compatibility.lua")(libPath)
|
||||
|
||||
-- TODO: Replace Noita's coroutine lib with something better
|
||||
if not async then
|
||||
require("coroutines") -- Loads Noita's coroutines library from `data/scripts/lib/coroutines.lua`.
|
||||
end
|
||||
|
||||
function OnWorldPostUpdate() -- this is called every time the game has finished updating the world
|
||||
wake_up_waiting_threads(1) -- Coroutines aren't run every frame in this sandbox, do it manually here.
|
||||
--------------------------
|
||||
-- Load library modules --
|
||||
--------------------------
|
||||
|
||||
local CameraAPI = require("noita-api.camera")
|
||||
local Coords = require("coordinates")
|
||||
local DebugAPI = require("noita-api.debug")
|
||||
local LiveReload = require("noita-api.live-reload")
|
||||
local Vec2 = require("noita-api.vec2")
|
||||
|
||||
-----------------------
|
||||
-- Global namespaces --
|
||||
-----------------------
|
||||
|
||||
Capture = Capture or {}
|
||||
Check = Check or {}
|
||||
Config = Config or {}
|
||||
Message = Message or {}
|
||||
Modification = Modification or {}
|
||||
UI = UI or {}
|
||||
|
||||
-------------------------------
|
||||
-- Load and run script files --
|
||||
-------------------------------
|
||||
|
||||
dofile("mods/noita-mapcap/files/capture.lua")
|
||||
dofile("mods/noita-mapcap/files/config.lua")
|
||||
dofile("mods/noita-mapcap/files/check.lua")
|
||||
dofile("mods/noita-mapcap/files/message.lua")
|
||||
dofile("mods/noita-mapcap/files/modification.lua")
|
||||
dofile("mods/noita-mapcap/files/ui.lua")
|
||||
|
||||
--------------------
|
||||
-- Hook callbacks --
|
||||
--------------------
|
||||
|
||||
---Called in order upon loading a new(?) game.
|
||||
function OnModPreInit()
|
||||
if ModSettingGet("noita-mapcap.seed") ~= "" then
|
||||
SetWorldSeed(tonumber(ModSettingGet("noita-mapcap.seed")) or 0)
|
||||
end
|
||||
|
||||
-- Read Noita's config to be used in checks later on.
|
||||
Check.StartupConfig = Modification.GetConfig()
|
||||
|
||||
-- Set magic numbers and other stuff based on mod settings.
|
||||
local config, magic, memory, patches = Modification.RequiredChanges()
|
||||
Modification.SetMagicNumbers(magic)
|
||||
Modification.SetMemoryOptions(memory)
|
||||
Modification.PatchFiles(patches)
|
||||
|
||||
-- Override virtual resolution and some other stuff.
|
||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/1024.xml")
|
||||
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/fast-cam.xml")
|
||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/no-ui.xml")
|
||||
--ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic-numbers/offset.xml")
|
||||
|
||||
-- Remove hover animation of newly created perks.
|
||||
ModLuaFileAppend("data/scripts/perks/perk.lua", "mods/noita-mapcap/files/overrides/perks/perk.lua")
|
||||
end
|
||||
|
||||
ModMagicNumbersFileAdd("mods/noita-mapcap/files/magic_numbers.xml") -- override some game constants
|
||||
---Called in order upon loading a new(?) game.
|
||||
function OnModInit()
|
||||
end
|
||||
|
||||
---Called in order upon loading a new(?) game.
|
||||
function OnModPostInit()
|
||||
end
|
||||
|
||||
---Called when player entity has been created.
|
||||
---Ensures chunks around the player have been loaded & created.
|
||||
---@param playerEntityID integer
|
||||
function OnPlayerSpawned(playerEntityID)
|
||||
-- Set camera free based on mod settings.
|
||||
-- We need to do this here, otherwise it will bug up or delete the player entity.
|
||||
Modification.SetCameraFree()
|
||||
end
|
||||
|
||||
---Called when the player dies.
|
||||
---@param playerEntityID integer
|
||||
function OnPlayerDied(playerEntityID)
|
||||
end
|
||||
|
||||
---Called once the game world is initialized.
|
||||
---Doesn't ensure any chunks around the player.
|
||||
function OnWorldInitialized()
|
||||
end
|
||||
|
||||
---Called *every* time the game is about to start updating the world.
|
||||
function OnWorldPreUpdate()
|
||||
Message:CatchException("OnWorldPreUpdate", function()
|
||||
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
|
||||
wake_up_waiting_threads(1)
|
||||
|
||||
end)
|
||||
end
|
||||
|
||||
---Called *every* time the game has finished updating the world.
|
||||
function OnWorldPostUpdate()
|
||||
Message:CatchException("OnWorldPostUpdate", function()
|
||||
-- Reload mod every 60 frames.
|
||||
-- This allows live updates to the mod while Noita is running.
|
||||
-- !!! DISABLE THE FOLLOWING LINE BEFORE COMMITTING !!!
|
||||
--LiveReload:Reload("mods/noita-mapcap/", 60)
|
||||
|
||||
-- Run checks every 60 frames.
|
||||
Check:Regular(60)
|
||||
|
||||
-- Draw UI after coroutines have been resumed.
|
||||
UI:Draw()
|
||||
|
||||
end)
|
||||
end
|
||||
|
||||
---Called when the biome config is loaded.
|
||||
function OnBiomeConfigLoaded()
|
||||
end
|
||||
|
||||
---The last point where the Mod API is available.
|
||||
---After this materials.xml will be loaded.
|
||||
function OnMagicNumbersAndWorldSeedInitialized()
|
||||
-- Get resolutions for correct coordinate transformations.
|
||||
-- This needs to be done once all magic numbers are set.
|
||||
Coords:ReadResolutions()
|
||||
|
||||
Check:Startup()
|
||||
end
|
||||
|
||||
---Called when the game is paused or unpaused.
|
||||
---@param isPaused boolean
|
||||
---@param isInventoryPause boolean
|
||||
function OnPausedChanged(isPaused, isInventoryPause)
|
||||
Message:CatchException("OnPausedChanged", function()
|
||||
-- Set some stuff based on mod settings.
|
||||
-- Normally this would be in `OnModSettingsChanged`, but that doesn't seem to be called.
|
||||
local config, magic, memory, patches = Modification.RequiredChanges()
|
||||
Modification.SetMemoryOptions(memory)
|
||||
|
||||
end)
|
||||
end
|
||||
|
||||
---Will be called when the game is unpaused, if player changed any mod settings while the game was paused.
|
||||
function OnModSettingsChanged()
|
||||
end
|
||||
|
||||
---Will be called when the game is paused, either by the pause menu or some inventory menus.
|
||||
---Please be careful with this, as not everything will behave well when called while the game is paused.
|
||||
function OnPausePreUpdate()
|
||||
end
|
||||
|
79
scripts/dist/compress.go
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2022 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// addPathToZip adds the given file or directory at srcPath to the zipWriter.
|
||||
//
|
||||
// The ignorePaths list is compared to the archive path (archive base path + relative path).
|
||||
func addPathToZip(zipWriter *zip.Writer, srcPath, archiveBasePath string, ignorePaths []string) error {
|
||||
return filepath.WalkDir(srcPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(srcPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
archivePath := filepath.Join(archiveBasePath, relPath)
|
||||
|
||||
// Skip if path is in ignore list.
|
||||
// This applies to directories or files.
|
||||
if slices.Contains(ignorePaths, archivePath) {
|
||||
log.Printf("Skipped %q", archivePath)
|
||||
if d.IsDir() {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ignore directories.
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileToZip, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileToZip.Close()
|
||||
|
||||
info, err := fileToZip.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = filepath.ToSlash(archivePath)
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(writer, fileToZip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|