From 99ad91e67c9165a377ac0a75e4eabae3094734a8 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 20 Oct 2019 16:28:17 +0200 Subject: [PATCH] Several changes - Don't change effects, as it's not needed anymore - Add capture library to be loaded by lua (go and PureBasic) - Modify pixel size, to nicely fit into FullHD - Move camera instead of player - Add capturing routine - Round position to fit into grid, and also render the world pixel perfect - Add util/helper functions --- .gitignore | 34 +++++-- .vscode/settings.json | 4 +- bin/capture-b/Capture.pb | 95 +++++++++++++++++++ bin/capture-b/README.md | 7 ++ bin/capture/README.md | 7 ++ bin/capture/build.bat | 9 ++ bin/capture/capture.go | 80 ++++++++++++++++ bin/capture/capture.h | 78 +++++++++++++++ bin/capture/server.go.disabled | 81 ++++++++++++++++ ...orph.xml => effect_polymorph.xml.disabled} | 0 ...l => effect_polymorph_random.xml.disabled} | 0 ....xml => effect_teleportation.xml.disabled} | 0 ...> effect_teleportation_enemy.xml.disabled} | 0 ...s.xml => effect_teleportitis.xml.disabled} | 0 .../misc/fogofwar_radius.xml.disabled | 17 ++++ files/capture.lua | 42 +++++--- files/external.lua | 67 +++++++++++++ files/magic_numbers.xml | 4 +- files/util.lua | 44 +++++++++ go.mod | 10 ++ go.sum | 10 ++ 21 files changed, 565 insertions(+), 24 deletions(-) create mode 100644 bin/capture-b/Capture.pb create mode 100644 bin/capture-b/README.md create mode 100644 bin/capture/README.md create mode 100644 bin/capture/build.bat create mode 100644 bin/capture/capture.h create mode 100644 bin/capture/server.go.disabled rename data/entities/misc/{effect_polymorph.xml => effect_polymorph.xml.disabled} (100%) rename data/entities/misc/{effect_polymorph_random.xml => effect_polymorph_random.xml.disabled} (100%) rename data/entities/misc/{effect_teleportation.xml => effect_teleportation.xml.disabled} (100%) rename data/entities/misc/{effect_teleportation_enemy.xml => effect_teleportation_enemy.xml.disabled} (100%) rename data/entities/misc/{effect_teleportitis.xml => effect_teleportitis.xml.disabled} (100%) create mode 100644 data/entities/misc/fogofwar_radius.xml.disabled create mode 100644 files/external.lua create mode 100644 files/util.lua create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore index 6c88fa0..f8e5b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,28 @@ # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig -# Created by https://www.gitignore.io/api/visualstudiocode,windows,lua -# Edit at https://www.gitignore.io/?templates=visualstudiocode,windows,lua +# Created by https://www.gitignore.io/api/lua,visualstudiocode,windows,go +# Edit at https://www.gitignore.io/?templates=lua,visualstudiocode,windows,go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ ### Lua ### # Compiled Lua sources @@ -32,14 +53,9 @@ luac.out *.exp # Shared objects (inc. Windows DLLs) -*.dll -*.so *.so.* -*.dylib # Executables -*.exe -*.out *.app *.i*86 *.x86_64 @@ -83,7 +99,9 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.gitignore.io/api/visualstudiocode,windows,lua +# End of https://www.gitignore.io/api/lua,visualstudiocode,windows,go # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +/libs/ +output/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6dcaddf..07a98d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "Vogel" + "Vogel", + "kbinani", + "noita" ] } \ No newline at end of file diff --git a/bin/capture-b/Capture.pb b/bin/capture-b/Capture.pb new file mode 100644 index 0000000..d82b7dc --- /dev/null +++ b/bin/capture-b/Capture.pb @@ -0,0 +1,95 @@ +UsePNGImageEncoder() + +Declare Worker(*Dummy) + +Structure QueueElement + img.i + x.i + y.i +EndStructure + +ProcedureDLL AttachProcess(Instance) + Global Semaphore = CreateSemaphore() + Global Mutex = CreateMutex() + Global NewList Queue.QueueElement() + + For i = 1 To 4 + CreateThread(@Worker(), #Null) + Next +EndProcedure + +Procedure Worker(*Dummy) + Protected img, x, y + + Repeat + WaitSemaphore(Semaphore) + + 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 +EndProcedure + +ProcedureDLL Capture(px.i, py.i) + ; Get dimensions of main screen + + ExamineDesktops() + + x = DesktopX(0) + y = DesktopY(0) + w = DesktopWidth(0) + h = DesktopHeight(0) + + imageID = CreateImage(#PB_Any, w, h) + If Not imageID + ProcedureReturn + EndIf + + ; Get DC of whole screen + screenDC = GetDC_(#Null) + + hDC = StartDrawing(ImageOutput(imageID)) + If Not hDC + ReleaseDC_(#Null, screenDC) + FreeImage(imageID) + ProcedureReturn + EndIf + BitBlt_(hDC, 0, 0, w, h, screenDC, x, y, #SRCCOPY) + StopDrawing() + + 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) + + SignalSemaphore(Semaphore) + +EndProcedure + +; IDE Options = PureBasic 5.71 LTS (Windows - x64) +; ExecutableFormat = Shared dll +; CursorPosition = 15 +; 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/README.md b/bin/capture-b/README.md new file mode 100644 index 0000000..bb20648 --- /dev/null +++ b/bin/capture-b/README.md @@ -0,0 +1,7 @@ +# Capture + +A dynamically linked library, that supplies a single function to take a screenshot and store it as PNG file. + +## State + +Works, not as fast as the go version, but ok. diff --git a/bin/capture/README.md b/bin/capture/README.md new file mode 100644 index 0000000..2ff1ede --- /dev/null +++ b/bin/capture/README.md @@ -0,0 +1,7 @@ +# Capture + +A dynamically linked library, that supplies a single function to take a screenshot and store it as PNG file. + +## State + +Works, but it seems the go runtime interferes with other stuff inside noita. Therefore it's unusable, because it causes crashes. diff --git a/bin/capture/build.bat b/bin/capture/build.bat new file mode 100644 index 0000000..488b70f --- /dev/null +++ b/bin/capture/build.bat @@ -0,0 +1,9 @@ +rem Copyright (c) 2019 David Vogel +rem +rem This software is released under the MIT License. +rem https://opensource.org/licenses/MIT + +set GOARCH=386 +set CGO_ENABLED=1 + +go build -o capture.dll -buildmode=c-shared \ No newline at end of file diff --git a/bin/capture/capture.go b/bin/capture/capture.go index 64e7404..12ccf2f 100644 --- a/bin/capture/capture.go +++ b/bin/capture/capture.go @@ -4,3 +4,83 @@ // https://opensource.org/licenses/MIT package main + +import "C" + +import ( + "fmt" + "image" + "image/png" + "os" + + "github.com/kbinani/screenshot" +) + +type encodeElement struct { + x, y int + img *image.RGBA +} + +var encodeQueue = make(chan encodeElement) + +var bounds = screenshot.GetDisplayBounds(0) // Only care about the main screen + +func init() { + // Start encode workers + startWorkers() +} + +func main() { + /*flagX := flag.Int("x", 0, "x coordinate") + flagY := flag.Int("y", 0, "y coordinate") + + flag.Parse() + + Capture(*flagX, *flagY) + + //startServer()*/ + + /*for i := 0; i < 5000; i++ { + Capture(i, 0) + }*/ +} + +func startWorkers() { + for i := 0; i < 8; i++ { + go func() { + encoder := png.Encoder{CompressionLevel: png.BestSpeed} + + for elem := range encodeQueue { + fileName := fmt.Sprintf("mods/noita-mapcap/output/%d,%d.png", elem.x, elem.y) + //fileName := fmt.Sprintf("%d,%d.png", x, y) + file, err := os.Create(fileName) + if err != nil { + continue + } + + encoder.Encode(file, elem.img) + + file.Close() + } + }() + } +} + +//Capture creates a snapshot of the whole main screen, and stores it inside the mod's output folder. +//export Capture +func Capture(x, y int) { + + img, err := screenshot.CaptureRect(bounds) + if err != nil { + panic(err) + //return + } + + //img := image.NewRGBA(image.Rect(0, 0, 1920, 1080)) + + encodeQueue <- encodeElement{ + img: img, + x: x, + y: y, + } +} diff --git a/bin/capture/capture.h b/bin/capture/capture.h new file mode 100644 index 0000000..22e49d0 --- /dev/null +++ b/bin/capture/capture.h @@ -0,0 +1,78 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package github.com/Dadido3/noita-mapcap/bin/capture */ + + +#line 1 "cgo-builtin-export-prolog" + +#include /* for ptrdiff_t below */ + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt32 GoInt; +typedef GoUint32 GoUint; +typedef __SIZE_TYPE__ GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_32_bit_pointer_matching_GoInt[sizeof(void*)==32/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + + +//Capture creates a snapshot of the whole main screen, and stores it inside the mod's output folder. + +extern void Capture(GoInt p0, GoInt p1); + +#ifdef __cplusplus +} +#endif diff --git a/bin/capture/server.go.disabled b/bin/capture/server.go.disabled new file mode 100644 index 0000000..b425dc2 --- /dev/null +++ b/bin/capture/server.go.disabled @@ -0,0 +1,81 @@ +// Copyright (c) 2019 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package main + +import ( + "bufio" + "log" + "net" + "strconv" + "strings" +) + +func startServer() { + log.Printf("Launching server...\n") + + // listen on all interfaces + ln, err := net.Listen("tcp", ":46789") + if err != nil { + log.Fatalf("Couldn't open TCP server at port 46789: %v", err) + } + + // accept connections + for { + conn, err := ln.Accept() + if err != nil { + log.Fatalf("Couldn't accept TCP connection: %v", err) + } + + // Handle incoming data + go func(conn net.Conn) { + defer conn.Close() + var x, y int + + for { + message, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + log.Printf("Couldn't read from connection: %v", err) + return + } + + // Check for end condition + if message == "\n" { + break + } + + colonPos := strings.IndexByte(message, ':') // Expect UTF-8 + if colonPos < 0 { + log.Printf("Missing colon in message: %v", message) + return + } + key, value := message[:colonPos], message[colonPos+1:] + log.Printf("Bla %v blub %v", key, value) + switch key { // Expect UTF-8 + case "x": + if res, err := strconv.ParseInt(value, 10, 0); err == nil { + x = int(res) + } else { + log.Printf("Can't parse string %v to integer: %v", value, err) + return + } + + case "y": + if res, err := strconv.ParseInt(value, 10, 0); err == nil { + y = int(res) + } else { + log.Printf("Can't parse string %v to integer: %v", value, err) + return + } + } + } + + conn.Write([]byte("\n")) + + log.Printf("capture bla at %v, %v", x, y) + + }(conn) + } +} diff --git a/data/entities/misc/effect_polymorph.xml b/data/entities/misc/effect_polymorph.xml.disabled similarity index 100% rename from data/entities/misc/effect_polymorph.xml rename to data/entities/misc/effect_polymorph.xml.disabled diff --git a/data/entities/misc/effect_polymorph_random.xml b/data/entities/misc/effect_polymorph_random.xml.disabled similarity index 100% rename from data/entities/misc/effect_polymorph_random.xml rename to data/entities/misc/effect_polymorph_random.xml.disabled diff --git a/data/entities/misc/effect_teleportation.xml b/data/entities/misc/effect_teleportation.xml.disabled similarity index 100% rename from data/entities/misc/effect_teleportation.xml rename to data/entities/misc/effect_teleportation.xml.disabled diff --git a/data/entities/misc/effect_teleportation_enemy.xml b/data/entities/misc/effect_teleportation_enemy.xml.disabled similarity index 100% rename from data/entities/misc/effect_teleportation_enemy.xml rename to data/entities/misc/effect_teleportation_enemy.xml.disabled diff --git a/data/entities/misc/effect_teleportitis.xml b/data/entities/misc/effect_teleportitis.xml.disabled similarity index 100% rename from data/entities/misc/effect_teleportitis.xml rename to data/entities/misc/effect_teleportitis.xml.disabled diff --git a/data/entities/misc/fogofwar_radius.xml.disabled b/data/entities/misc/fogofwar_radius.xml.disabled new file mode 100644 index 0000000..eda677c --- /dev/null +++ b/data/entities/misc/fogofwar_radius.xml.disabled @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/files/capture.lua b/files/capture.lua index 6dafb4e..fe7b9cd 100644 --- a/files/capture.lua +++ b/files/capture.lua @@ -3,12 +3,19 @@ -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT -dofile("data/scripts/lib/coroutines.lua") +dofile("mods/noita-mapcap/files/util.lua") +dofile("mods/noita-mapcap/files/external.lua") +dofile("mods/noita-mapcap/files/ws.lua") + --dofile("data/scripts/lib/utilities.lua") dofile("data/scripts/perks/perk_list.lua") -local CAPTURE_GRID_SIZE = 64 -- in ingame pixels -local CAPTURE_DELAY = 30 -- in frames +if not async then -- Check if lib is already loaded + dofile("data/scripts/lib/coroutines.lua") +end + +local CAPTURE_GRID_SIZE = 128 -- in ingame pixels +local CAPTURE_DELAY = 15 -- in frames local CAPTURE_FORCE_HP = 40 -- * 25HP local function getPlayer() @@ -87,20 +94,14 @@ local function resetPlayer() setPlayerHP(CAPTURE_FORCE_HP) end -local function doCapture() +local function startCapturing() local ox, oy = getPlayerPos() + ox, oy = math.floor(ox / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE, math.floor(oy / CAPTURE_GRID_SIZE) * CAPTURE_GRID_SIZE local x, y = ox, oy preparePlayer() - -- Coroutine to force player to x, y coordinate - async_loop( - function() - teleportPlayer(x, y) - resetPlayer() - wait(0) - end - ) + GameSetCameraFree(true) -- Coroutine to calculate next coordinate, and trigger screenshots local i = 1 @@ -108,23 +109,31 @@ local function doCapture() function() -- +x for i = 1, i, 1 do + TriggerCapture(x, y) x, y = x + CAPTURE_GRID_SIZE, y + GameSetCameraPos(x, y) wait(CAPTURE_DELAY) end -- +y for i = 1, i, 1 do + TriggerCapture(x, y) x, y = x, y + CAPTURE_GRID_SIZE + GameSetCameraPos(x, y) wait(CAPTURE_DELAY) end i = i + 1 -- -x for i = 1, i, 1 do + TriggerCapture(x, y) x, y = x - CAPTURE_GRID_SIZE, y + GameSetCameraPos(x, y) wait(CAPTURE_DELAY) end -- -y for i = 1, i, 1 do + TriggerCapture(x, y) x, y = x, y - CAPTURE_GRID_SIZE + GameSetCameraPos(x, y) wait(CAPTURE_DELAY) end i = i + 1 @@ -143,12 +152,19 @@ async_loop( GuiLayoutBeginVertical(gui, 50, 20) if GuiButton(gui, 0, 0, "Start capturing map", 1) then - doCapture() + startCapturing() GuiDestroy(gui) gui = nil end GuiTextCentered(gui, 0, 0, "Don't do anything while the capturing process is running!") GuiTextCentered(gui, 0, 0, "Use ESC and close the game to stop the process.") + --[[if GuiButton(gui, 0, 0, "DEBUG globals", 1) then + local file = io.open("mods/noita-mapcap/output/globals.txt", "w") + for i, v in pairs(_G) do + file:write(i .. "\n") + end + file:close() + end]] GuiLayoutEnd(gui) end diff --git a/files/external.lua b/files/external.lua new file mode 100644 index 0000000..05123dd --- /dev/null +++ b/files/external.lua @@ -0,0 +1,67 @@ +-- Copyright (c) 2019 David Vogel +-- +-- This software is released under the MIT License. +-- https://opensource.org/licenses/MIT + +--local host, port = "127.0.0.1", "46789" +--local socket = require("socket") +--local tcp = assert(socket.tcp()) + +--package.path = package.path .. ";" .. "mods/noita-mapcap/libs/lua/" .. "?.lua" +--package.cpath = package.cpath .. ";" .. "mods/noita-mapcap/libs/" .. "?.dll" -- TODO: Make it OS aware + +local ffi = ffi or _G.ffi or require("ffi") + +-- Don't let lua garbage collect and unload the lib, it will cause crashes later on! +-- Blah, this causes random crashes anyways. Probably the go runtime that interferes with something else in noita. +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 [[ + void Capture(int x, int y); +]] + +function TriggerCapture(x, y) + --IngamePrint(os.execute(string.format("mods/noita-mapcap/bin/capture/capture.exe -x %i -y %i", x, y))) + --IngamePrint("a", os.execute("capture.exe")) + --os.execute("screenshots") + --IngamePrint("b", os.execute("../bin/capture/capture.exe")) + --local handle = io.popen("mods/noita-mapcap/bin/capture/capture.exe") + --local result = handle:read("*a") + --IngamePrint(result) + --handle:close() + --IngamePrint(os.execute("echo test")) + + --print("trace", "A") + + caplib.Capture(x, y) + + --print("trace", "B") +end + +--[[function TriggerCapture(x, y) + local status, lib = pcall(require, "socket") + if not status then + IngamePrint("Error loading socket lib: " .. lib) + end + + IngamePrint("DEBUG - Capture") + tcp:connect(host, port) + IngamePrint("DEBUG - Connected") + + tcp:send(string.format("x: %i\n", x)) + tcp:send(string.format("y: %i\n", y)) + tcp:send(string.format("\n", y)) + + IngamePrint("DEBUG - Sent") + + local result, error = tcp:receive("*l") + -- Ignore error or result for now, the function blocks until a newline character is received. + + IngamePrint("DEBUG - Received") + + tcp:close() + + IngamePrint("DEBUG - Closed") +end]] diff --git a/files/magic_numbers.xml b/files/magic_numbers.xml index 953226f..16b2487 100644 --- a/files/magic_numbers.xml +++ b/files/magic_numbers.xml @@ -1,5 +1,5 @@ -