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
This commit is contained in:
David Vogel 2019-10-20 16:28:17 +02:00
parent be48a75c25
commit 99ad91e67c
21 changed files with 565 additions and 24 deletions

34
.gitignore vendored
View File

@ -1,7 +1,28 @@
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
# Created by https://www.gitignore.io/api/visualstudiocode,windows,lua # Created by https://www.gitignore.io/api/lua,visualstudiocode,windows,go
# Edit at https://www.gitignore.io/?templates=visualstudiocode,windows,lua # 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 ### ### Lua ###
# Compiled Lua sources # Compiled Lua sources
@ -32,14 +53,9 @@ luac.out
*.exp *.exp
# Shared objects (inc. Windows DLLs) # Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.* *.so.*
*.dylib
# Executables # Executables
*.exe
*.out
*.app *.app
*.i*86 *.i*86
*.x86_64 *.x86_64
@ -83,7 +99,9 @@ $RECYCLE.BIN/
# Windows shortcuts # Windows shortcuts
*.lnk *.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) # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
/libs/
output/

View File

@ -1,5 +1,7 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Vogel" "Vogel",
"kbinani",
"noita"
] ]
} }

95
bin/capture-b/Capture.pb Normal file
View File

@ -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)

7
bin/capture-b/README.md Normal file
View File

@ -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.

7
bin/capture/README.md Normal file
View File

@ -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.

9
bin/capture/build.bat Normal file
View File

@ -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

View File

@ -4,3 +4,83 @@
// https://opensource.org/licenses/MIT // https://opensource.org/licenses/MIT
package main 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,
}
}

78
bin/capture/capture.h Normal file
View File

@ -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 <stddef.h> /* 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

View File

@ -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)
}
}

View File

@ -0,0 +1,17 @@
<Entity >
<FogOfWarRadiusComponent _enabled="0"
radius="200">
</FogOfWarRadiusComponent>
<SpriteComponent _enabled="0"
alpha="0.8"
image_file="data/particles/torch_fog_of_war_hole.xml"
smooth_filtering="1"
fog_of_war_hole="1"
has_special_scale="1"
special_scale_x="4"
special_scale_y="4">
</SpriteComponent>
</Entity>

View File

@ -3,12 +3,19 @@
-- 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
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/lib/utilities.lua")
dofile("data/scripts/perks/perk_list.lua") dofile("data/scripts/perks/perk_list.lua")
local CAPTURE_GRID_SIZE = 64 -- in ingame pixels if not async then -- Check if lib is already loaded
local CAPTURE_DELAY = 30 -- in frames 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 CAPTURE_FORCE_HP = 40 -- * 25HP
local function getPlayer() local function getPlayer()
@ -87,20 +94,14 @@ local function resetPlayer()
setPlayerHP(CAPTURE_FORCE_HP) setPlayerHP(CAPTURE_FORCE_HP)
end end
local function doCapture() local function startCapturing()
local ox, oy = getPlayerPos() 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 local x, y = ox, oy
preparePlayer() preparePlayer()
-- Coroutine to force player to x, y coordinate GameSetCameraFree(true)
async_loop(
function()
teleportPlayer(x, y)
resetPlayer()
wait(0)
end
)
-- Coroutine to calculate next coordinate, and trigger screenshots -- Coroutine to calculate next coordinate, and trigger screenshots
local i = 1 local i = 1
@ -108,23 +109,31 @@ local function doCapture()
function() function()
-- +x -- +x
for i = 1, i, 1 do for i = 1, i, 1 do
TriggerCapture(x, y)
x, y = x + CAPTURE_GRID_SIZE, y x, y = x + CAPTURE_GRID_SIZE, y
GameSetCameraPos(x, y)
wait(CAPTURE_DELAY) wait(CAPTURE_DELAY)
end end
-- +y -- +y
for i = 1, i, 1 do for i = 1, i, 1 do
TriggerCapture(x, y)
x, y = x, y + CAPTURE_GRID_SIZE x, y = x, y + CAPTURE_GRID_SIZE
GameSetCameraPos(x, y)
wait(CAPTURE_DELAY) wait(CAPTURE_DELAY)
end end
i = i + 1 i = i + 1
-- -x -- -x
for i = 1, i, 1 do for i = 1, i, 1 do
TriggerCapture(x, y)
x, y = x - CAPTURE_GRID_SIZE, y x, y = x - CAPTURE_GRID_SIZE, y
GameSetCameraPos(x, y)
wait(CAPTURE_DELAY) wait(CAPTURE_DELAY)
end end
-- -y -- -y
for i = 1, i, 1 do for i = 1, i, 1 do
TriggerCapture(x, y)
x, y = x, y - CAPTURE_GRID_SIZE x, y = x, y - CAPTURE_GRID_SIZE
GameSetCameraPos(x, y)
wait(CAPTURE_DELAY) wait(CAPTURE_DELAY)
end end
i = i + 1 i = i + 1
@ -143,12 +152,19 @@ async_loop(
GuiLayoutBeginVertical(gui, 50, 20) GuiLayoutBeginVertical(gui, 50, 20)
if GuiButton(gui, 0, 0, "Start capturing map", 1) then if GuiButton(gui, 0, 0, "Start capturing map", 1) then
doCapture() startCapturing()
GuiDestroy(gui) GuiDestroy(gui)
gui = nil gui = nil
end end
GuiTextCentered(gui, 0, 0, "Don't do anything while the capturing process is running!") 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.") 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) GuiLayoutEnd(gui)
end end

67
files/external.lua Normal file
View File

@ -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]]

View File

@ -1,5 +1,5 @@
<MagicNumbers VIRTUAL_RESOLUTION_X="427" <MagicNumbers VIRTUAL_RESOLUTION_X="960"
VIRTUAL_RESOLUTION_Y="242" VIRTUAL_RESOLUTION_Y="540"
CAMERA_NO_MOVE_BUFFER_NEAR_VIEWPORT_EDGE="0.0" CAMERA_NO_MOVE_BUFFER_NEAR_VIEWPORT_EDGE="0.0"
CAMERA_MOUSE_INTERPOLATION_SPEED="0.0" CAMERA_MOUSE_INTERPOLATION_SPEED="0.0"
CAMERA_POSITION_INTERPOLATION_SPEED="50.0" CAMERA_POSITION_INTERPOLATION_SPEED="50.0"

44
files/util.lua Normal file
View File

@ -0,0 +1,44 @@
-- Copyright (c) 2019 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.
function IngamePrint(...)
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
GamePrint(v)
end
end
end
-- Globally overwrite print function and write output into a logfile
local logFile = io.open("lualog.txt", "w")
function print(...)
local arg = {...}
local result = ""
for i, v in ipairs(arg) do
result = result .. tostring(v) .. "\t"
end
result = result .. "\n"
logFile:write(result)
logFile:flush()
end

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module github.com/Dadido3/noita-mapcap
go 1.13
require (
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 // indirect
github.com/gen2brain/shm v0.0.0-20180314170312-6c18ff7f8b90 // indirect
github.com/kbinani/screenshot v0.0.0-20190719135742-f06580e30cdc
github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8 // indirect
)

10
go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/gen2brain/shm v0.0.0-20180314170312-6c18ff7f8b90 h1:QagTG5rauLt6pVVEhnVSrlIX4ifhVIZOwmw6x6D8TUw=
github.com/gen2brain/shm v0.0.0-20180314170312-6c18ff7f8b90/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
github.com/kbinani/screenshot v0.0.0-20190719135742-f06580e30cdc h1:kGFotla6Dyr6a2ILeExAHlttPgJtnoP/GIw2uVN/4h4=
github.com/kbinani/screenshot v0.0.0-20190719135742-f06580e30cdc/go.mod h1:f8GY5V3lRzakvEyr49P7hHRYoHtPr8zvj/7JodCoRzw=
github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8 h1:RVMGIuuNgrpGB7I79f6xfhGCkpN47IaEGh8VTM0p7Xc=
github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=