diff --git a/README.md b/README.md index 5ae460a..1cc9fa8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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). @@ -20,10 +20,14 @@ A resulting image with close to 3 gigapixels can be [seen here](https://easyzoom 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 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. 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. 9. Start `.../Noita/mods/noita-mapcap/bin/stitch/stitch.exe`. - 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` - 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. ## License diff --git a/bin/capture-b/Capture.pb b/bin/capture-b/Capture.pb index 0ec5fb2..56c1013 100644 --- a/bin/capture-b/Capture.pb +++ b/bin/capture-b/Capture.pb @@ -8,104 +8,153 @@ UsePNGImageEncoder() Declare Worker(*Dummy) Structure QueueElement - img.i - x.i - y.i + img.i + x.i + y.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 + 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) - Global Semaphore = CreateSemaphore() - Global Mutex = CreateMutex() - Global NewList Queue.QueueElement() + Global Semaphore = CreateSemaphore() + Global Mutex = CreateMutex() + Global NewList Queue.QueueElement() - ExamineDesktops() - CreateDirectory("mods/noita-mapcap/output/") + CreateDirectory("mods/noita-mapcap/output/") - For i = 1 To 4 - CreateThread(@Worker(), #Null) - Next + For i = 1 To 4 + CreateThread(@Worker(), #Null) + Next EndProcedure Procedure Worker(*Dummy) - Protected img, x, y + Protected img, x, y - Repeat - WaitSemaphore(Semaphore) + Repeat + WaitSemaphore(Semaphore) - LockMutex(Mutex) - FirstElement(Queue()) - img = Queue()\img - x = Queue()\x - y = Queue()\y - DeleteElement(Queue()) - UnlockMutex(Mutex) + LockMutex(Mutex) + FirstElement(Queue()) + img = Queue()\img + x = Queue()\x + y = Queue()\y + DeleteElement(Queue()) + UnlockMutex(Mutex) - SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG) - FreeImage(img) - ForEver + SaveImage(img, "mods/noita-mapcap/output/" + x + "," + y + ".png", #PB_ImagePlugin_PNG) + ;SaveImage(img, "" + x + "," + y + ".png", #PB_ImagePlugin_PNG) ; Test + + FreeImage(img) + ForEver EndProcedure ProcedureDLL Capture(px.i, py.i) - ; Get dimensions of main screen + Protected rect.RECT - x = DesktopX(0) - y = DesktopY(0) - w = DesktopWidth(0) - h = DesktopHeight(0) + If Not GetRect(@rect) + ProcedureReturn #False + EndIf - imageID = CreateImage(#PB_Any, w, h) - If Not imageID - ProcedureReturn - EndIf + imageID = CreateImage(#PB_Any, rect\right-rect\left, rect\bottom-rect\top) + If Not imageID + ProcedureReturn #False + EndIf - ; Get DC of whole screen - screenDC = GetDC_(#Null) - If Not screenDC - FreeImage(imageID) - ProcedureReturn - EndIf + ; Get DC of whole screen + screenDC = GetDC_(#Null) + If Not screenDC + FreeImage(imageID) + ProcedureReturn #False + EndIf - hDC = StartDrawing(ImageOutput(imageID)) - If Not hDC - FreeImage(imageID) - ReleaseDC_(#Null, screenDC) - ProcedureReturn - 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. - FreeImage(imageID) - ReleaseDC_(#Null, screenDC) - StopDrawing() - ProcedureReturn - EndIf - StopDrawing() + hDC = StartDrawing(ImageOutput(imageID)) + If Not hDC + FreeImage(imageID) + ReleaseDC_(#Null, screenDC) + ProcedureReturn #False + EndIf + 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) + ReleaseDC_(#Null, screenDC) + StopDrawing() + ProcedureReturn #False + EndIf + StopDrawing() - ReleaseDC_(#Null, screenDC) + ReleaseDC_(#Null, screenDC) - LockMutex(Mutex) - ; Check if the queue has too many elements, if so, wait. (Simulate go's channels) - While ListSize(Queue()) > 0 - UnlockMutex(Mutex) - Delay(10) - LockMutex(Mutex) - Wend - LastElement(Queue()) - AddElement(Queue()) - Queue()\img = imageID - Queue()\x = px - Queue()\y = py - UnlockMutex(Mutex) + LockMutex(Mutex) + ; Check if the queue has too many elements, if so, wait. (Simulate go's channels) + While ListSize(Queue()) > 0 + UnlockMutex(Mutex) + Delay(10) + LockMutex(Mutex) + Wend + LastElement(Queue()) + AddElement(Queue()) + Queue()\img = imageID + Queue()\x = px + Queue()\y = py + UnlockMutex(Mutex) - SignalSemaphore(Semaphore) + SignalSemaphore(Semaphore) + ProcedureReturn #True 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) ; ExecutableFormat = Shared dll -; CursorPosition = 25 -; FirstLine = 3 -; Folding = - +; CursorPosition = 140 +; FirstLine = 94 +; Folding = -- ; EnableThread ; EnableXP ; Executable = capture.dll -; DisableDebugger ; Compiler = PureBasic 5.71 LTS (Windows - x86) \ No newline at end of file diff --git a/bin/capture-b/capture.dll b/bin/capture-b/capture.dll index f9a20ca..02cad43 100644 Binary files a/bin/capture-b/capture.dll and b/bin/capture-b/capture.dll differ diff --git a/bin/stitch/README.md b/bin/stitch/README.md index 44ae7d5..ef415a9 100644 --- a/bin/stitch/README.md +++ b/bin/stitch/README.md @@ -29,7 +29,7 @@ example list of files: - Run the program and follow the interactive prompt. - Run the program with parameters: - `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") - `output string` The path and filename of the resulting stitched image. (default "output.png") diff --git a/bin/stitch/stitch.go b/bin/stitch/stitch.go index aa27498..9231c49 100644 --- a/bin/stitch/stitch.go +++ b/bin/stitch/stitch.go @@ -19,7 +19,7 @@ import ( 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", 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 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.") diff --git a/files/capture.lua b/files/capture.lua index 05255fa..cc44df1 100644 --- a/files/capture.lua +++ b/files/capture.lua @@ -3,10 +3,10 @@ -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT -local CAPTURE_PIXEL_SIZE = 2 -- in FullHD an ingame pixel is expected to be 2 real pixels -local CAPTURE_GRID_SIZE = 256 -- in ingame pixels. There will be 6 to 12 images overlapping -local CAPTURE_DELAY = 15 -- in frames -local CAPTURE_FORCE_HP = 4 -- * 25HP +CAPTURE_PIXEL_SIZE = 1 -- Screen to virtual pixel ratio +CAPTURE_GRID_SIZE = 420 -- in ingame pixels. There will always be 3 to 6 images overlapping +CAPTURE_DELAY = 15 -- in frames +CAPTURE_FORCE_HP = 4 -- * 25HP local function preparePlayer() local playerEntity = getPlayer() diff --git a/files/external.lua b/files/external.lua index eb673fa..8487784 100644 --- a/files/external.lua +++ b/files/external.lua @@ -10,9 +10,28 @@ if not status then print("Error loading capture lib: " .. cap) end 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) - 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 diff --git a/files/magic_numbers.xml b/files/magic_numbers.xml index b141d94..4ba9c84 100644 --- a/files/magic_numbers.xml +++ b/files/magic_numbers.xml @@ -1,12 +1,12 @@ - + DEBUG_DISABLE_POSTFX_DITHERING="1" + DEBUG_NO_PAUSE_ON_WINDOW_FOCUS_LOST="1" + DEBUG_LUA="1"> diff --git a/files/ui.lua b/files/ui.lua index 94648fa..5efb53f 100644 --- a/files/ui.lua +++ b/files/ui.lua @@ -13,9 +13,81 @@ async_loop( GuiLayoutBeginVertical(modGUI, 50, 50) 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, "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( modGUI, 0, @@ -29,10 +101,12 @@ async_loop( 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 <<", 1) then startCapturing() UiReduce = true end + GuiTextCentered(modGUI, 0, 0, " ") end if not UiHide then local x, y = GameGetCameraPos()