Several updates

- Change virtual resolution to 1280x720
- Change virtual to screen pixel ratio to 1:1
- Increase grid size to 420
- Make the mod capture only the window (and only the client area)
- Increase STREAMING_CHUNK_TARGET, so that chunks don't unload after some frames
- Add ingame warnings for wrong settings, and information how to fix these
- Update README.md
This commit is contained in:
David Vogel 2019-11-01 02:40:21 +01:00
parent 638f6223c3
commit c87a4d05d0
9 changed files with 235 additions and 92 deletions

View File

@ -2,7 +2,7 @@
Addon that captures the map and saves it as image. Addon that captures the map and saves it as image.
![](images/example1.png) ![missing image](images/example1.png)
A resulting image with close to 3 gigapixels can be [seen here](https://easyzoom.com/image/158284/album/0/4) (Warning: Spoilers). A resulting image with close to 3 gigapixels can be [seen here](https://easyzoom.com/image/158284/album/0/4) (Warning: Spoilers).
@ -20,10 +20,14 @@ A resulting image with close to 3 gigapixels can be [seen here](https://easyzoom
1. Have Noita installed. 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) 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`. 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 1920x1080 if possible, and use the `Windowed` mode. (Not `Fullscreen (Windowed)`!) If you have to use a different resolution, see advanced usage. 4. Set your resolution to 1280x720, and use the `Windowed` mode. (Not `Fullscreen (Windowed)`!) If you have to use a different resolution, see advanced usage.
5. Enable the mod and restart Noita. 5. Enable the mod and restart Noita.
6. In the game you should see a `>> Start capturing map <<` text on the screen, click it. 6. In the game you should see a `>> Start capturing map <<` text on the screen, click it.
7. The screen will jump around, and the game will take screenshots automatically. Don't interfere with it. Screenshots are saved in `.../Noita/mods/noita-mapcap/output/`. 7. The screen will jump around, and the game will take screenshots automatically.
- Screenshots are saved in `.../Noita/mods/noita-mapcap/output/`.
- Don't cover the game window.
- Don't move the game window outside of screen space.
- If you need to pause, use the ESC menu.
8. When you think you are done, close noita. 8. When you think you are done, close noita.
9. Start `.../Noita/mods/noita-mapcap/bin/stitch/stitch.exe`. 9. Start `.../Noita/mods/noita-mapcap/bin/stitch/stitch.exe`.
- Use the default values to create a complete stitch. - Use the default values to create a complete stitch.
@ -44,9 +48,6 @@ The following two formulae have to be true:
- `VIRTUAL_RESOLUTION_*` can be found inside `.../Noita/mods/noita-mapcap/files/magic_numbers.xml` - `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. - and `SCREEN_RESOLUTION_*` is the screen resolution you have set up in noita.
If you have a resolution of `1366 x 768`, then you should change the `VIRTUAL_RESOLUTION_*` to `683 x 384`.
Another solution would be to change the `CAPTURE_PIXEL_SIZE` to `1.423`, but then you would get blurry images.
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. 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.
## License ## License

View File

@ -8,104 +8,153 @@ UsePNGImageEncoder()
Declare Worker(*Dummy) Declare Worker(*Dummy)
Structure QueueElement Structure QueueElement
img.i img.i
x.i x.i
y.i y.i
EndStructure 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
EndIf
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
ProcedureDLL GetRect(*rect.RECT)
Protected hWnd.l = GetProcHwnd()
If Not hWnd
ProcedureReturn #False
EndIf
If Not *rect
ProcedureReturn #False
EndIf
GetClientRect_(hWnd, *rect)
; A RECT consists basically of two POINT structures
ClientToScreen_(hWnd, @*rect\left)
ClientToScreen_(hWnd, @*rect\Right)
ProcedureReturn #True
EndProcedure
ProcedureDLL AttachProcess(Instance) ProcedureDLL AttachProcess(Instance)
Global Semaphore = CreateSemaphore() Global Semaphore = CreateSemaphore()
Global Mutex = CreateMutex() Global Mutex = CreateMutex()
Global NewList Queue.QueueElement() Global NewList Queue.QueueElement()
ExamineDesktops() CreateDirectory("mods/noita-mapcap/output/")
CreateDirectory("mods/noita-mapcap/output/")
For i = 1 To 4 For i = 1 To 4
CreateThread(@Worker(), #Null) CreateThread(@Worker(), #Null)
Next Next
EndProcedure EndProcedure
Procedure Worker(*Dummy) Procedure Worker(*Dummy)
Protected img, x, y Protected img, x, y
Repeat Repeat
WaitSemaphore(Semaphore) WaitSemaphore(Semaphore)
LockMutex(Mutex) LockMutex(Mutex)
FirstElement(Queue()) FirstElement(Queue())
img = Queue()\img img = Queue()\img
x = Queue()\x x = Queue()\x
y = Queue()\y y = Queue()\y
DeleteElement(Queue()) DeleteElement(Queue())
UnlockMutex(Mutex) UnlockMutex(Mutex)
SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG) SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG)
FreeImage(img) ;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test
ForEver
FreeImage(img)
ForEver
EndProcedure EndProcedure
ProcedureDLL Capture(px.i, py.i) ProcedureDLL Capture(px.i, py.i)
; Get dimensions of main screen Protected rect.RECT
x = DesktopX(0) If Not GetRect(@rect)
y = DesktopY(0) ProcedureReturn #False
w = DesktopWidth(0) EndIf
h = DesktopHeight(0)
imageID = CreateImage(#PB_Any, w, h) imageID = CreateImage(#PB_Any, rect\right-rect\left, rect\bottom-rect\top)
If Not imageID If Not imageID
ProcedureReturn ProcedureReturn #False
EndIf EndIf
; Get DC of whole screen ; Get DC of whole screen
screenDC = GetDC_(#Null) screenDC = GetDC_(#Null)
If Not screenDC If Not screenDC
FreeImage(imageID) FreeImage(imageID)
ProcedureReturn ProcedureReturn #False
EndIf EndIf
hDC = StartDrawing(ImageOutput(imageID)) hDC = StartDrawing(ImageOutput(imageID))
If Not hDC If Not hDC
FreeImage(imageID) FreeImage(imageID)
ReleaseDC_(#Null, screenDC) ReleaseDC_(#Null, screenDC)
ProcedureReturn ProcedureReturn #False
EndIf EndIf
If Not BitBlt_(hDC, 0, 0, w, h, screenDC, x, y, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes. If Not BitBlt_(hDC, 0, 0, rect\right-rect\left, rect\bottom-rect\top, screenDC, rect\left, rect\top, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes.
FreeImage(imageID) FreeImage(imageID)
ReleaseDC_(#Null, screenDC) ReleaseDC_(#Null, screenDC)
StopDrawing() StopDrawing()
ProcedureReturn ProcedureReturn #False
EndIf EndIf
StopDrawing() StopDrawing()
ReleaseDC_(#Null, screenDC) ReleaseDC_(#Null, screenDC)
LockMutex(Mutex) LockMutex(Mutex)
; Check if the queue has too many elements, if so, wait. (Simulate go's channels) ; Check if the queue has too many elements, if so, wait. (Simulate go's channels)
While ListSize(Queue()) > 0 While ListSize(Queue()) > 0
UnlockMutex(Mutex) UnlockMutex(Mutex)
Delay(10) Delay(10)
LockMutex(Mutex) LockMutex(Mutex)
Wend Wend
LastElement(Queue()) LastElement(Queue())
AddElement(Queue()) AddElement(Queue())
Queue()\img = imageID Queue()\img = imageID
Queue()\x = px Queue()\x = px
Queue()\y = py Queue()\y = py
UnlockMutex(Mutex) UnlockMutex(Mutex)
SignalSemaphore(Semaphore) SignalSemaphore(Semaphore)
ProcedureReturn #True
EndProcedure EndProcedure
; #### Test
;AttachProcess(0)
;OpenWindow(0, 100, 200, 195, 260, "PureBasic Window", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget)
;Delay(1000)
;Capture(123, 123)
;Delay(1000)
; IDE Options = PureBasic 5.71 LTS (Windows - x64) ; IDE Options = PureBasic 5.71 LTS (Windows - x64)
; ExecutableFormat = Shared dll ; ExecutableFormat = Shared dll
; CursorPosition = 25 ; CursorPosition = 140
; FirstLine = 3 ; FirstLine = 94
; Folding = - ; Folding = --
; EnableThread ; EnableThread
; EnableXP ; EnableXP
; Executable = capture.dll ; Executable = capture.dll
; DisableDebugger
; Compiler = PureBasic 5.71 LTS (Windows - x86) ; Compiler = PureBasic 5.71 LTS (Windows - x86)

Binary file not shown.

View File

@ -29,7 +29,7 @@ example list of files:
- Run the program and follow the interactive prompt. - Run the program and follow the interactive prompt.
- Run the program with parameters: - Run the program with parameters:
- `divide int` - `divide int`
A downscaling factor. 2 will produce an image with half the side lengths. (default 2) A downscaling factor. 2 will produce an image with half the side lengths. (default 1)
- `input string`The source path of the image tiles to be stitched. (default "..\\..\\output") - `input string`The source path of the image tiles to be stitched. (default "..\\..\\output")
- `output string` - `output string`
The path and filename of the resulting stitched image. (default "output.png") The path and filename of the resulting stitched image. (default "output.png")

View File

@ -19,7 +19,7 @@ import (
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.") var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.") var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.")
var flagScaleDivider = flag.Int("divide", 2, "A downscaling factor. 2 will produce an image with half the side lengths.") var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.") var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coordinate is included in the output.")
var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This coordinate is included in the output.") var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This coordinate is included in the output.")
var flagXMax = flag.Int("xmax", 0, "Right bound of the output rectangle. This coordinate is not included in the output.") var flagXMax = flag.Int("xmax", 0, "Right bound of the output rectangle. This coordinate is not included in the output.")

View File

@ -3,10 +3,10 @@
-- This software is released under the MIT License. -- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT -- https://opensource.org/licenses/MIT
local CAPTURE_PIXEL_SIZE = 2 -- in FullHD an ingame pixel is expected to be 2 real pixels CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio
local CAPTURE_GRID_SIZE = 256 -- in ingame pixels. There will be 6 to 12 images overlapping CAPTURE_GRID_SIZE = 420 -- in ingame pixels. There will always be 3 to 6 images overlapping
local CAPTURE_DELAY = 15 -- in frames CAPTURE_DELAY = 15 -- in frames
local CAPTURE_FORCE_HP = 4 -- * 25HP CAPTURE_FORCE_HP = 4 -- * 25HP
local function preparePlayer() local function preparePlayer()
local playerEntity = getPlayer() local playerEntity = getPlayer()

View File

@ -10,9 +10,28 @@ if not status then
print("Error loading capture lib: " .. cap) print("Error loading capture lib: " .. cap)
end end
ffi.cdef [[ ffi.cdef [[
void Capture(int x, int y); typedef long LONG;
typedef struct {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
bool GetRect(RECT* rect);
bool Capture(int x, int y);
]] ]]
function TriggerCapture(x, y) function TriggerCapture(x, y)
caplib.Capture(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 end

View File

@ -1,12 +1,12 @@
<MagicNumbers VIRTUAL_RESOLUTION_X="960" <MagicNumbers VIRTUAL_RESOLUTION_X="1280"
VIRTUAL_RESOLUTION_Y="540" VIRTUAL_RESOLUTION_Y="720"
CAMERA_NO_MOVE_BUFFER_NEAR_VIEWPORT_EDGE="0.0" STREAMING_CHUNK_TARGET="24"
CAMERA_MOUSE_INTERPOLATION_SPEED="0.0"
CAMERA_POSITION_INTERPOLATION_SPEED="50.0"
DRAW_PARALLAX_BACKGROUND="0" DRAW_PARALLAX_BACKGROUND="0"
DEBUG_FREE_CAMERA_SPEED="10" DEBUG_FREE_CAMERA_SPEED="10"
DEBUG_NO_LOGO_SPLASHES="1" DEBUG_NO_LOGO_SPLASHES="1"
DEBUG_PAUSE_GRID_UPDATE="1" DEBUG_PAUSE_GRID_UPDATE="1"
DEBUG_PAUSE_BOX2D="1" DEBUG_PAUSE_BOX2D="1"
DEBUG_DISABLE_POSTFX_DITHERING="1"> DEBUG_DISABLE_POSTFX_DITHERING="1"
DEBUG_NO_PAUSE_ON_WINDOW_FOCUS_LOST="1"
DEBUG_LUA="1">
</MagicNumbers> </MagicNumbers>

View File

@ -13,9 +13,81 @@ async_loop(
GuiLayoutBeginVertical(modGUI, 50, 50) GuiLayoutBeginVertical(modGUI, 50, 50)
if not UiReduce then if not UiReduce then
local problem
local rect = GetRect()
if not rect then
GuiTextCentered(modGUI, 0, 0, '!!! WARNING !!! You are not using "Windowed" mode.')
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
GuiTextCentered(modGUI, 0, 0, '- Change the window mode in the game options to "Windowed"')
GuiTextCentered(modGUI, 0, 0, " ")
problem = true
end
if rect then
local screenWidth, screenHeight = rect.right - rect.left, rect.bottom - rect.top
local virtualWidth, virtualHeight =
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_X")),
tonumber(MagicNumbersGetValue("VIRTUAL_RESOLUTION_Y"))
local ratioX, ratioY = screenWidth / virtualWidth, screenHeight / virtualHeight
--GuiTextCentered(modGUI, 0, 0, string.format("SCREEN_RESOLUTION_*: %d, %d", screenWidth, screenHeight))
--GuiTextCentered(modGUI, 0, 0, string.format("VIRTUAL_RESOLUTION_*: %d, %d", virtualWidth, virtualHeight))
if math.abs(ratioX - CAPTURE_PIXEL_SIZE) > 0.0001 or math.abs(ratioY - CAPTURE_PIXEL_SIZE) > 0.0001 then
GuiTextCentered(modGUI, 0, 0, "!!! WARNING !!! Screen and virtual resolution differ.")
GuiTextCentered(modGUI, 0, 0, "To fix the problem, do one of these:")
GuiTextCentered(
modGUI,
0,
0,
string.format(
"- Change the resolution in the game options to %dx%d",
virtualWidth * CAPTURE_PIXEL_SIZE,
virtualHeight * CAPTURE_PIXEL_SIZE
)
)
GuiTextCentered(
modGUI,
0,
0,
string.format(
"- Change the virtual resolution in the mod to %dx%d",
screenWidth / CAPTURE_PIXEL_SIZE,
screenHeight / CAPTURE_PIXEL_SIZE
)
)
if math.abs(ratioX - ratioY) < 0.0001 then
GuiTextCentered(modGUI, 0, 0, string.format("- Change the CAPTURE_PIXEL_SIZE in the mod to %f", ratioX))
end
GuiTextCentered(modGUI, 0, 0, " ")
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, "You can freely look around and search a place to start capturing.")
GuiTextCentered(modGUI, 0, 0, "The mod will then take images in a spiral around your current view.") GuiTextCentered(modGUI, 0, 0, "The mod will then take images in a spiral around your current view.")
GuiTextCentered(modGUI, 0, 0, "Use ESC and close the game to stop the process.") GuiTextCentered(modGUI, 0, 0, "Use ESC to pause, and close the game to stop the process.")
GuiTextCentered( GuiTextCentered(
modGUI, modGUI,
0, 0,
@ -29,10 +101,12 @@ async_loop(
0, 0,
'If you want to start a new map, you have to delete all images from the "output" folder!' 'If you want to start a new map, you have to delete all images from the "output" folder!'
) )
GuiTextCentered(modGUI, 0, 0, " ")
if GuiButton(modGUI, 0, 0, ">> Start capturing map <<", 1) then if GuiButton(modGUI, 0, 0, ">> Start capturing map <<", 1) then
startCapturing() startCapturing()
UiReduce = true UiReduce = true
end end
GuiTextCentered(modGUI, 0, 0, " ")
end end
if not UiHide then if not UiHide then
local x, y = GameGetCameraPos() local x, y = GameGetCameraPos()