diff --git a/.debug/lua-definitions/noitapatcher.lua b/.debug/lua-definitions/noitapatcher.lua index b968e3e5..0d8e466f 100644 --- a/.debug/lua-definitions/noitapatcher.lua +++ b/.debug/lua-definitions/noitapatcher.lua @@ -1,5 +1,6 @@ ---@meta 'noitapatcher' ----@class noitapatcher +---@module noitapatcher + local noitapatcher = {} ---Enable OnProjectileFired and OnProjectileFiredPost callbacks. @@ -105,7 +106,7 @@ function noitapatcher.SerializeEntity(entity_id) end ---@param serialized_data string The serialized data ---@param x number? Position to force the entity to if provided ---@param y number? Position to force the entity to if provided ----@return integer entity_id The entity_id passed into the function if deserialization was successful. +---@return integer? entity_id The entity_id passed into the function if deserialization was successful. function noitapatcher.DeserializeEntity(entity_id, serialized_data, x, y) end ---Set box2d parameters of a PhysicsBody(2)Component diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/load.lua b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/load.lua new file mode 100644 index 00000000..b41f6603 --- /dev/null +++ b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/load.lua @@ -0,0 +1,24 @@ + +-- You're supposed to `dofile_once("path/to/load.lua")` this file. + + +local orig_do_mod_appends = do_mod_appends + +do_mod_appends = function(filename, ...) + do_mod_appends = orig_do_mod_appends + do_mod_appends(filename, ...) + + local noitapatcher_path = string.match(filename, "(.*)/load.lua") + if not noitapatcher_path then + print("Couldn't detect NoitaPatcher path") + end + + __nsew_path = noitapatcher_path .. "/noitapatcher/nsew/" + + package.cpath = package.cpath .. ";./" .. noitapatcher_path .. "/?.dll" + package.path = package.path .. ";./" .. noitapatcher_path .. "/?.lua" + + -- Lua's loader should now be setup properly: + -- local np = require("noitapatcher") + -- local nsew = require("noitapatcher.nsew") +end diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/noitapatcher.dll b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/noitapatcher.dll index 65cb67b0..38390dae 100644 Binary files a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/noitapatcher.dll and b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/noitapatcher.dll differ diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/native_dll.lua b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/native_dll.lua new file mode 100644 index 00000000..5d1c76e8 --- /dev/null +++ b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/native_dll.lua @@ -0,0 +1,11 @@ +--- Native library. Primarily for internal use. +---@module 'noitapatcher.nsew.native_dll' + +local ffi = require("ffi") + +native_dll = {} + +--- The NSEW support dll loaded in with `ffi.load`. +native_dll.lib = ffi.load(__nsew_path .. "nsew_native.dll") + +return native_dll diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/nsew_native.dll b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/nsew_native.dll new file mode 100644 index 00000000..bb8c8ea0 Binary files /dev/null and b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/nsew_native.dll differ diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/rect.lua b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/rect.lua new file mode 100644 index 00000000..8c163d39 --- /dev/null +++ b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/rect.lua @@ -0,0 +1,134 @@ +--- Rectangle utilities. +---@module 'noitapatcher.nsew.rect' + +local rect = {} + +local ffi = require("ffi") +local native_dll = require("noitapatcher.nsew.native_dll") + +ffi.cdef([[ + +struct nsew_rectangle { + int32_t left; + int32_t top; + int32_t right; + int32_t bottom; +}; + + +struct nsew_rectangle_optimiser; + +struct nsew_rectangle_optimiser* rectangle_optimiser_new(); +void rectangle_optimiser_delete(struct nsew_rectangle_optimiser* rectangle_optimiser); +void rectangle_optimiser_reset(struct nsew_rectangle_optimiser* rectangle_optimiser); +void rectangle_optimiser_submit(struct nsew_rectangle_optimiser* rectangle_optimiser, struct nsew_rectangle* rectangle); +void rectangle_optimiser_scan(struct nsew_rectangle_optimiser* rectangle_optimiser); +int32_t rectangle_optimiser_size(const struct nsew_rectangle_optimiser* rectangle_optimiser); +const struct nsew_rectangle* rectangle_optimiser_get(const struct nsew_rectangle_optimiser* rectangle_optimiser, int32_t index); + + +struct lua_nsew_rectangle_optimiser { + struct nsew_rectangle_optimiser* impl; +}; + +]]) + +local Rectangle_mt = { + __index = { + area = function(r) + return (r.right - r.left) * (r.bottom - r.top) + end, + height = function(r) + return r.bottom - r.top + end, + width = function(r) + return r.right - r.left + end, + }, +} +rect.Rectangle = ffi.metatype("struct nsew_rectangle", Rectangle_mt) + +--- Given an iterator that returns rectangles, return an iterator where the +--- rectangle extents never exceed `size`. +-- @param it iterator returning squares +-- @tparam int size maximum width and height +-- @return rectangle iterator where the extents never exceed `size` +function rect.parts(it, size) + local region + local posx + local posy + return function() + if region == nil then + region = it() + if region == nil then + return nil + end + posx = region.left + posy = region.top + end + + local endx = math.min(posx + size, region.right) + local endy = math.min(posy + size, region.bottom) + + local ret = rect.Rectangle(posx, posy, endx, endy) + + -- Setup for next iteration: place to the right, wraparound, or + -- we're done with this region. + if endx ~= region.right then + posx = endx + elseif endy ~= region.bottom then + posx = region.left + posy = endy + else + region = nil + end + + return ret + end +end + +local Optimiser_mt = { + __gc = function(opt) + native_dll.lib.rectangle_optimiser_delete(opt.impl) + end, + + __index = { + submit = function(opt, rectangle) + native_dll.lib.rectangle_optimiser_submit(opt.impl, rectangle) + end, + scan = function(opt) + native_dll.lib.rectangle_optimiser_scan(opt.impl) + end, + reset = function(opt) + native_dll.lib.rectangle_optimiser_reset(opt.impl) + end, + size = function(opt) + return native_dll.lib.rectangle_optimiser_size() + end, + get = function(opt, index) + return native_dll.lib.rectangle_optimiser_get(index) + end, + iterate = function(opt) + local size = native_dll.lib.rectangle_optimiser_size(opt.impl) + local index = 0 + return function() + if index >= size then + return nil + end + + ret = native_dll.lib.rectangle_optimiser_get(opt.impl, index) + index = index + 1 + return ret + end + end, + } +} +rect.Optimiser = ffi.metatype("struct lua_nsew_rectangle_optimiser", Optimiser_mt) + +--- Create a new rectangle Optimiser +-- @treturn Optimiser empty optimiser +function rect.Optimiser_new() + return rect.Optimiser(native_dll.lib.rectangle_optimiser_new()) +end + +return rect diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world.lua b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world.lua new file mode 100644 index 00000000..ba91180b --- /dev/null +++ b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world.lua @@ -0,0 +1,239 @@ +--- World read / write functionality. +---@module 'noitapatcher.nsew.world' +local world = {} + +local ffi = require("ffi") +local world_ffi = require("noitapatcher.nsew.world_ffi") + +local C = ffi.C + +ffi.cdef([[ + +enum ENCODE_CONST { + PIXEL_RUN_MAX = 4096, + + LIQUID_FLAG_STATIC = 1, +}; + +struct __attribute__ ((__packed__)) EncodedAreaHeader { + int32_t x; + int32_t y; + uint8_t width; + uint8_t height; + + uint16_t pixel_run_count; +}; + +struct __attribute__ ((__packed__)) PixelRun { + uint16_t length; + int16_t material; + uint8_t flags; +}; + +struct __attribute__ ((__packed__)) EncodedArea { + struct EncodedAreaHeader header; + struct PixelRun pixel_runs[PIXEL_RUN_MAX]; +}; + +]]) + +world.EncodedAreaHeader = ffi.typeof("struct EncodedAreaHeader") +world.PixelRun = ffi.typeof("struct PixelRun") +world.EncodedArea = ffi.typeof("struct EncodedArea") + +local pliquid_cell = ffi.typeof("struct CLiquidCell*") + +--- Total bytes taken up by the encoded area +-- @tparam EncodedArea encoded_area +-- @treturn int total number of bytes that encodes the area +-- @usage +-- local data = ffi.string(area, world.encoded_size(area)) +-- peer:send(data) +function world.encoded_size(encoded_area) + return (ffi.sizeof(world.EncodedAreaHeader) + encoded_area.header.pixel_run_count * ffi.sizeof(world.PixelRun)) +end + +--- Encode the given rectangle of the world +-- The rectangle defined by {`start_x`, `start_y`, `end_x`, `end_y`} must not +-- exceed 256 in width or height. +-- @param chunk_map +-- @tparam int start_x coordinate +-- @tparam int start_y coordinate +-- @tparam int end_x coordinate +-- @tparam int end_y coordinate +-- @tparam EncodedArea encoded_area memory to use, if nil this function allocates its own memory +-- @return returns an EncodedArea or nil if the area could not be encoded +-- @see decode +function world.encode_area(chunk_map, start_x, start_y, end_x, end_y, encoded_area) + start_x = ffi.cast('int32_t', start_x) + start_y = ffi.cast('int32_t', start_y) + end_x = ffi.cast('int32_t', end_x) + end_y = ffi.cast('int32_t', end_y) + + encoded_area = encoded_area or world.EncodedArea() + + local width = end_x - start_x + local height = end_y - start_y + + if width <= 0 or height <= 0 then + print("Invalid world part, negative dimension") + return nil + end + + if width > 256 or height > 256 then + print("Invalid world part, dimension greater than 256") + return nil + end + + encoded_area.header.x = start_x + encoded_area.header.y = start_y + encoded_area.header.width = width - 1 + encoded_area.header.height = height - 1 + + local run_count = 1 + + local current_run = encoded_area.pixel_runs[0] + local run_length = 0 + local current_material = 0 + local current_flags = 0 + + local y = start_y + while y < end_y do + local x = start_x + while x < end_x do + local material_number = 0 + local flags = 0 + + local ppixel = world_ffi.get_cell(chunk_map, x, y) + local pixel = ppixel[0] + if pixel ~= nil then + local cell_type = pixel.vtable.get_cell_type(pixel) + + if cell_type ~= C.CELL_TYPE_SOLID then + local material_ptr = pixel.vtable.get_material(pixel) + material_number = world_ffi.get_material_id(material_ptr) + end + + if cell_type == C.CELL_TYPE_LIQUID then + local liquid_cell = ffi.cast(pliquid_cell, pixel) + if liquid_cell.is_static then + flags = bit.bor(flags, C.LIQUID_FLAG_STATIC) + end + end + end + + if x == start_x and y == start_y then + -- Initial run + current_material = material_number + current_flags = flags + elseif current_material ~= material_number or current_flags ~= flags then + -- Next run + current_run.length = run_length - 1 + current_run.material = current_material + current_run.flags = current_flags + + if run_count == C.PIXEL_RUN_MAX then + print("Area too complicated to encode") + return nil + end + + current_run = encoded_area.pixel_runs[run_count] + run_count = run_count + 1 + + run_length = 0 + current_material = material_number + current_flags = flags + end + + run_length = run_length + 1 + + x = x + 1 + end + y = y + 1 + end + + current_run.length = run_length - 1 + current_run.material = current_material + current_run.flags = current_flags + + encoded_area.header.pixel_run_count = run_count + + return encoded_area +end + +local PixelRun_const_ptr = ffi.typeof("struct PixelRun const*") + +--- Load an encoded area back into the world. +-- @param grid_world +-- @tparam EncodedAreaHeader header header of the encoded area +-- @param received pointer or ffi array of PixelRun from the encoded area +-- @see encode_area +function world.decode(grid_world, header, pixel_runs) + local chunk_map = grid_world.vtable.get_chunk_map(grid_world) + + local top_left_x = header.x + local top_left_y = header.y + local width = header.width + 1 + local height = header.height + 1 + local bottom_right_x = top_left_x + width + local bottom_right_y = top_left_y + height + + local current_run_ix = 0 + local current_run = pixel_runs[current_run_ix] + local new_material = current_run.material + local flags = current_run.flags + local left = current_run.length + 1 + + local y = top_left_y + while y < bottom_right_y do + local x = top_left_x + while x < bottom_right_x do + if world_ffi.chunk_loaded(chunk_map, x, y) then + local ppixel = world_ffi.get_cell(chunk_map, x, y) + local current_material = 0 + + if ppixel[0] ~= nil then + local pixel = ppixel[0] + current_material = world_ffi.get_material_id(pixel.vtable.get_material(pixel)) + + if new_material ~= current_material then + world_ffi.remove_cell(grid_world, pixel, x, y, false) + end + end + + if current_material ~= new_material and new_material ~= 0 then + local pixel = world_ffi.construct_cell(grid_world, x, y, world_ffi.get_material_ptr(new_material), nil) + local cell_type = pixel.vtable.get_cell_type(pixel) + + if cell_type == C.CELL_TYPE_LIQUID then + local liquid_cell = ffi.cast(pliquid_cell, pixel) + liquid_cell.is_static = bit.band(flags, C.CELL_TYPE_LIQUID) == C.LIQUID_FLAG_STATIC + end + + ppixel[0] = pixel + end + end + + left = left - 1 + if left <= 0 then + current_run_ix = current_run_ix + 1 + if current_run_ix >= header.pixel_run_count then + -- No more runs, done + assert(x == bottom_right_x - 1) + assert(y == bottom_right_y - 1) + return + end + + current_run = pixel_runs[current_run_ix] + new_material = current_run.material + flags = current_run.flags + left = current_run.length + 1 + end + + x = x + 1 + end + y = y + 1 + end +end + +return world diff --git a/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world_ffi.lua b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world_ffi.lua new file mode 100644 index 00000000..5a00a883 --- /dev/null +++ b/mods/noita-mp/lua_modules/lib/lua/5.1/noitapatcher/nsew/world_ffi.lua @@ -0,0 +1,276 @@ +--- Noita world functionality exposed. +---@module 'noitapatcher.nsew.world_ffi' + +local world_ffi = {} + +local ffi = require("ffi") + +local np = require("noitapatcher") +local world_info = np.GetWorldInfo() + +if not world_info then + error("Couldn't get world info from NoitaPatcher.") +end + +local gg_ptr = world_info.game_global + +ffi.cdef([[ + +typedef void* __thiscall placeholder_memfn(void*); + +struct Position { + int x; + int y; +}; + +struct Colour { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +}; + +struct AABB { + struct Position top_left; + struct Position bottom_right; +}; + +enum CellType { + CELL_TYPE_NONE = 0, + CELL_TYPE_LIQUID = 1, + CELL_TYPE_GAS = 2, + CELL_TYPE_SOLID = 3, + CELL_TYPE_FIRE = 4, +}; + +struct Cell_vtable { + void (__thiscall *destroy)(struct Cell*, char dealloc); + enum CellType (__thiscall *get_cell_type)(struct Cell*); + void* field2_0x8; + void* field3_0xc; + void* field4_0x10; + struct Colour (__thiscall *get_colour)(struct Cell*); + void* field6_0x18; + void (__thiscall *set_colour)(struct Cell*, struct Colour); + void* field8_0x20; + void* field9_0x24; + void* field10_0x28; + void* field11_0x2c; + void* (__thiscall *get_material)(void *); + void* field13_0x34; + void* field14_0x38; + void* field15_0x3c; + void* field16_0x40; + void* field17_0x44; + void* field18_0x48; + void* field19_0x4c; + struct Position * (__thiscall *get_position)(void *, struct Position *); + void* field21_0x54; + void* field22_0x58; + void* field23_0x5c; + void* field24_0x60; + void* field25_0x64; + void* field26_0x68; + void* field27_0x6c; + void* field28_0x70; + bool (__thiscall *is_burning)(struct Cell*); + void* field30_0x78; + void* field31_0x7c; + void* field32_0x80; + void (__thiscall *stop_burning)(struct Cell*); + void* field34_0x88; + void* field35_0x8c; + void* field36_0x90; + void* field37_0x94; + void* field38_0x98; + void (__thiscall *remove)(struct Cell*); + void* field40_0xa0; +}; + +// In the Noita code this would be the ICellBurnable class +struct Cell { + struct Cell_vtable* vtable; + + int hp; + char unknown1[8]; + bool is_burning; + char unknown2[3]; + uintptr_t material_ptr; +}; + +struct CLiquidCell { + struct Cell cell; + int x; + int y; + char unknown1; + char unknown2; + bool is_static; + char unknown3; + int unknown4[3]; + struct Colour colour; + unsigned not_colour; +}; + +typedef struct Cell (*cell_array)[0x40000]; + +struct ChunkMap { + int unknown[2]; + cell_array* (*cells)[0x40000]; + int unknown2[8]; +}; + +struct GridWorld_vtable { + placeholder_memfn* unknown[3]; + struct ChunkMap* (__thiscall *get_chunk_map)(struct GridWorld* this); + placeholder_memfn* unknown2[30]; +}; + +struct GridWorld { + struct GridWorld_vtable* vtable; + int unknown[318]; + int world_update_count; + struct ChunkMap chunk_map; + int unknown2[41]; + struct GridWorldThreadImpl* mThreadImpl; +}; + +struct GridWorldThreaded_vtable; + +struct GridWorldThreaded { + struct GridWorldThreaded_vtable* vtable; + int unknown[287]; + struct AABB update_region; +}; + +struct vec_pGridWorldThreaded { + struct GridWorldThreaded** begin; + struct GridWorldThreaded** end_; + struct GridWorldThreaded** capacity_end; +}; + +struct WorldUpdateParams { + struct AABB update_region; + int unknown; + struct GridWorldThreaded* grid_world_threaded; +}; + +struct vec_WorldUpdateParams { + struct WorldUpdateParams* begin; + struct WorldUpdateParams* end_; + struct WorldUpdateParams* capacity_end; +}; + +struct GridWorldThreadImpl { + int chunk_update_count; + struct vec_pGridWorldThreaded updated_grid_worlds; + + int world_update_params_count; + struct vec_WorldUpdateParams world_update_params; + + int grid_with_area_count; + struct vec_pGridWorldThreaded with_area_grid_worlds; + + int another_count; + int another_vec[3]; + + int some_kind_of_ptr; + int some_kind_of_counter; + + int last_vec[3]; +}; + +typedef struct Cell** __thiscall get_cell_f(struct ChunkMap*, int x, int y); +typedef bool __thiscall chunk_loaded_f(struct ChunkMap*, int x, int y); + +typedef void __thiscall remove_cell_f(struct GridWorld*, void* cell, int x, int y, bool); +typedef struct Cell* __thiscall construct_cell_f(struct GridWorld*, int x, int y, void* material_ptr, void* memory); + +]]) + +--- Access a pixel in the world. +-- @function get_cell +-- @param chunk_map chunk map +-- @tparam int x coordinate +-- @tparam int y coordinate +-- @return Pointer to a pointer to a cell. You can write a cell created from @{construct_cell} to this pointer to add a cell into the world. If there's already a cell at this position, make sure to call @{remove_cell} first. +world_ffi.get_cell = ffi.cast("get_cell_f*", world_info.get_cell) + +--- Remove a cell from the world. +-- @function remove_cell +-- @param grid_world +-- @param cell pointer to the cell you want to remove +-- @tparam int x coordinate +-- @tparam int y coordinate +-- @tparam bool noidea no idea +world_ffi.remove_cell = ffi.cast("remove_cell_f*", world_info.remove_cell) + +--- Create a new cell. +-- @function construct_cell +-- @param grid_world +-- @tparam int x coordinate +-- @tparam int y coordinate +-- @param material_ptr pointer to material +-- @param pointer to memory to use. nullptr will make this function allocate its own memory +world_ffi.construct_cell = ffi.cast("construct_cell_f*", world_info.construct_cell) + +--- Check if a chunk is loaded. +-- @function chunk_loaded +-- @param chunk_map +-- @tparam int x world coordinate +-- @tparam int y world coordinate +-- @usage +-- if world_ffi.chunk_loaded(chunk_map, x, y) then +-- local cell = world_ffi.get_cell(chunk_map, x, y) +-- -- ... +world_ffi.chunk_loaded = ffi.cast("chunk_loaded_f*", world_info.chunk_loaded) + +world_ffi.Position = ffi.typeof("struct Position") +world_ffi.Colour = ffi.typeof("struct Colour") +world_ffi.AABB = ffi.typeof("struct AABB") +world_ffi.CellType = ffi.typeof("enum CellType") +world_ffi.Cell = ffi.typeof("struct Cell") +world_ffi.CLiquidCell = ffi.typeof("struct CLiquidCell") +world_ffi.ChunkMap = ffi.typeof("struct ChunkMap") +world_ffi.GridWorld = ffi.typeof("struct GridWorld") +world_ffi.GridWorldThreaded = ffi.typeof("struct GridWorldThreaded") +world_ffi.WorldUpdateParams = ffi.typeof("struct WorldUpdateParams") +world_ffi.GridWorldThreadImpl = ffi.typeof("struct GridWorldThreadImpl") + +--- Get the grid world. +-- @return pointer to the grid world +function world_ffi.get_grid_world() + local game_global = ffi.cast("void*", gg_ptr) + local world_data = ffi.cast("void**", ffi.cast("char*", game_global) + 0xc)[0] + local grid_world = ffi.cast("struct GridWorld**", ffi.cast("char*", world_data) + 0x44)[0] + return grid_world +end + +local material_props_size = 0x28c + +--- Turn a standard material id into a material pointer. +-- @param id material id that is used in the standard Noita functions +-- @return pointer to internal material data (aka cell data). +-- @usage local gold_ptr = world_ffi.get_material_ptr(CellFactory_GetType("gold")) +function world_ffi.get_material_ptr(id) + local game_global = ffi.cast("char*", gg_ptr) + local cell_factory = ffi.cast('char**', (game_global + 0x18))[0] + local begin = ffi.cast('char**', cell_factory + 0x18)[0] + local ptr = begin + material_props_size * id + return ptr +end + +--- Turn a material pointer into a standard material id. +-- @param ptr pointer to a material (aka cell data) +-- @treturn int material id that is accepted by standard Noita functions such as +-- `CellFactory_GetUIName` and `ConvertMaterialOnAreaInstantly`. +-- @usage local mat_id = world_ffi.get_material_id(cell.vtable.get_material(cell)) +-- @see get_material_ptr +function world_ffi.get_material_id(ptr) + local game_global = ffi.cast("char*", gg_ptr) + local cell_factory = ffi.cast('char**', (game_global + 0x18))[0] + local begin = ffi.cast('char**', cell_factory + 0x18)[0] + local offset = ffi.cast('char*', ptr) - begin + return offset / material_props_size +end + +return world_ffi