This commit is contained in:
IQuant 2024-04-29 22:14:22 +03:00
commit ce608c9bdf
19 changed files with 1662 additions and 0 deletions

24
NoitaPatcher/load.lua Normal file
View file

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

Binary file not shown.

View file

@ -0,0 +1,151 @@
---@meta 'noitapatcher'
---@module noitapatcher
local noitapatcher = {}
---Enable OnProjectileFired and OnProjectileFiredPost callbacks.
---@return nil
function noitapatcher.InstallShootProjectileFiredCallbacks() end
---Enables GetDamageDetails in newly created Lua states.
---@return nil
function noitapatcher.InstallDamageDetailsPatch() end
---Sets Noita's internal RNG state to the specified value.
---This RNG state is used for many things including setting a fired projectile's
---direction based on random spread.
---@param rng_value integer New RNG state value
function noitapatcher.SetProjectileSpreadRNG(rng_value) end
---Disable the red flash upon taking damage for all entities with a PlatformShooterPlayerComponent except for the one specified by entity_id.
---You can restore the original behaviour by passing in -1 for the entity_id.
---@param entity_id integer ID of the only entity for which to do the damage flash.
function noitapatcher.RegisterPlayerEntityId(entity_id) end
---Change the item that the entity is holding.
---@param entity_id integer id of the entity for which you want to change what they are holding.
---@param item_id integer id of the entity that should be held. For the best effect it should be an item in the inventory_quick child of the entity specified by entity_id.
---@param unknown boolean Not sure what this does. Let me know if you find out!
---@param make_noise boolean Whether or not switching to this item should make a noise.
function noitapatcher.SetActiveHeldEntity(entity_id, item_id, unknown, make_noise) end
---Changes the entity that the game considers to be the player.
---This determines what entity is followed by the camera and whose death ends the game.
---A bunch more stuff is probably tied to this.
---@param entity_id integer The entity to make the game think of as the player.
function noitapatcher.SetPlayerEntity(entity_id) end
---Enables or disables game simulate pausing when opening escape or wand menu.
---@param enabled boolean Whether to enable or disable pausing.
function noitapatcher.EnableGameSimulatePausing(enabled) end
---Disable InventoryGuiComponent updates without disabling the component.
---Disabling updates for this component makes clicking on an empty wand slot work
---after using EnableGameSimulatePausing(false) and entering the wand pickup menu.
---@param enabled boolean Whether to enable or disable Inventory GUI updates.
function noitapatcher.EnableInventoryGuiUpdate(enabled) end
---Enable/disable ItemPickUpperComponent updates for the entity registerd using RegisterPlayerEntityId
---Disabling updates for this component prevents double wand cards from appearing
---after using EnableGameSimulatePausing(false) and entering the wand pickup menu.
---@param enabled boolean Whether to enable or disable ItemPickUpper updates.
function noitapatcher.EnablePlayerItemPickUpper(enabled) end
---Send a 'use item' message causing the item to get activated by the entity's ability component.
---@param responsible_entity_id integer Entity that should be seen as responsible for the item's use.
---@param item_entity_id integer Wand or other item entity.
---@param ignore_reload boolean _
---@param charge boolean _
---@param started_using_this_frame boolean _
---@param pos_x number _
---@param pos_y number _
---@param target_x number _
---@param target_y number _
function noitapatcher.UseItem(responsible_entity_id, item_entity_id, ignore_reload, charge, started_using_this_frame, pos_x, pos_y, target_x, target_y) end
---Patch out logging for a certain string literal.
---@param logstr string The string to look for in the exe, it should end with a newline character in most cases.
---@return boolean patch_successful
function noitapatcher.SilenceLogs(logstr) end
---Like Noita's LoadPixelScene, but doesn't care if the scene has been loaded before.
---@param materials_filename string
---@param colors_filename string
---@param x number
---@param y number
---@param background_file string
---@param skip_biome_checks boolean Defaults to false
---@param skip_edge_textures boolean Defaults to false
---@param color_to_material_table table Defaults to {}
---@param background_z_index integer Defaults to 50
function noitapatcher.ForceLoadPixelScene(materials_filename, colors_filename, x, y, background_file, skip_biome_checks, skip_edge_textures, color_to_material_table, background_z_index) end
---Enable source location logging
---@param enable boolean enable or disable
function noitapatcher.EnableExtendedLogging(enable) end
---Enable the FilterLog callback
---@param enable boolean enable or disable
function noitapatcher.EnableLogFiltering(enable) end
---Disable system updates
---@param system_name string Name of the system to disable, for instance BlackHoleSystem
---@param change_to boolean enable (true) or disable (false)
---@return boolean change_succeeded
function noitapatcher.ComponentUpdatesSetEnabled(system_name, change_to) end
---Serialize an entity
---@param entity_id integer
---@nodiscard
---@return string serialized_data
function noitapatcher.SerializeEntity(entity_id) end
---Deserialize an entity. If x and y are provided then the entity's position is changed to that instead of using the position info in the serialized data.
---@param entity_id integer Entity to deserialize into, most of the time you want this to be an "empty" entity.
---@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.
function noitapatcher.DeserializeEntity(entity_id, serialized_data, x, y) end
---Set box2d parameters of a PhysicsBody(2)Component
---@param component_id integer The PhysicsBody(2)Component
---@param x number box2d x coordinate
---@param y number box2d y coordinate
---@param r number box2d rotation
---@param vx number box2d x velocity
---@param vy number box2d y velocity
---@param av number box2d angular velocity
function noitapatcher.PhysBodySetTransform(component_id, x, y, r, vx, vy, av) end
---Get the box2d parameters of a PhysicsBody(2)Component
---@param component_id integer The PhysicsBody(2)Component
---@nodiscard
---@return number box2d x coordinate
---@return number box2d y coordinate
---@return number box2d rotation
---@return number box2d x velocity
---@return number box2d y velocity
---@return number box2d angular velocity
function noitapatcher.PhysBodyGetTransform(component_id) end
---Mark the current game mode as a daily. Disables spell unlocks and if called during mod init makes all spells available for the run.
---@param deterministic boolean
function noitapatcher.SetGameModeDeterministic(deterministic)end
---Set the current pause state bitfield.
---0, 1, 4 and >=32 are safe values to use.
---0 means unpaused, 4 is the escape menu pause, the other safe values don't have any GUI.
---@param value integer new pause state value
---@return integer previous pause state value
function noitapatcher.SetPauseState(value)end
---Set the current pause state bitfield value.
---@return integer current pause state value
function noitapatcher.GetPauseState()end
---Enable or disable inventory cursor interactions.
---@param enable boolean
function SetInventoryCursorEnabled(enable)end
return noitapatcher

Binary file not shown.

View file

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

Binary file not shown.

View file

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

View file

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

View file

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

4
compatibility.xml Executable file
View file

@ -0,0 +1,4 @@
<Mod
version_built_with="1"
>
</Mod>

View file

@ -0,0 +1,9 @@
<Entity name="chunk_loader" tags="chunk_loader,spectator_no_clear">
<StreamingKeepAliveComponent/>
<HitboxComponent
aabb_min_x="-500"
aabb_min_y="-500"
aabb_max_x="500"
aabb_max_y="500"
></HitboxComponent>
</Entity>

326
files/entities/client.xml Normal file
View file

@ -0,0 +1,326 @@
<Entity name="client" tags="client,mortal,human,hittable,peasant,prey,polymorphable_NOT">
<HotspotComponent
_tags="hat"
sprite_hotspot_name="hat"
/>
<VelocityComponent
updates_velocity="0"
></VelocityComponent>
<ControlsComponent
_enabled="1"
enabled="0"
gamepad_fire_on_thumbstick_extend="0"
gamepad_fire_on_thumbstick_extend_threshold="0.7"
gamepad_indirect_aiming_enabled="0"
polymorph_hax="0"
polymorph_next_attack_frame="0" >
</ControlsComponent>
<!-- Protections -->
<Entity>
<InheritTransformComponent />
<GameEffectComponent
effect="PROTECTION_FREEZE"
frames="-1"
>
</GameEffectComponent >
<GameEffectComponent
effect="STUN_PROTECTION_FREEZE"
frames="-1"
>
</GameEffectComponent >
</Entity>
<Entity name="chunk_loader">
<Base file="mods/quant.ew/files/entities/chunk_loader.xml">
</Base>
</Entity>
<StatusEffectDataComponent/>
<Entity name="cursor">
<SpriteComponent
alpha="1"
image_file="mods/quant.ew/files/sprites/cursor.png"
next_rect_animation=""
offset_x="2.5"
offset_y="2.5"
emissive="1"
additive="1"
rect_animation=""
z_index="1"
></SpriteComponent>
</Entity>
<CharacterDataComponent
platforming_type="2"
check_collision_max_size_x="4"
check_collision_max_size_y="4"
climb_over_y="4"
collision_aabb_min_x="-2.0"
collision_aabb_max_x="2.0"
collision_aabb_min_y="-4.5"
collision_aabb_max_y="2.1"
eff_hg_offset_y="1.28572"
eff_hg_position_x="0"
eff_hg_position_y="5"
eff_hg_size_x="6.42857"
eff_hg_size_y="5.14286"
eff_hg_velocity_max_x="19.5787896514"
eff_hg_velocity_max_y="-11.5714"
eff_hg_velocity_min_x="-19.5714"
eff_hg_velocity_min_y="-40"
eff_hg_damage_min="10"
eff_hg_damage_max="95"
eff_hg_update_box2d="0"
eff_hg_b2force_multiplier="0.0015"
effect_hit_ground="1"
fly_time_max="3.0"
fly_recharge_spd="0.4"
fly_recharge_spd_ground="6"
flying_needs_recharge="1"
flying_in_air_wait_frames="38"
flying_recharge_removal_frames="8"
gravity="0"
buoyancy_check_offset_y="-7"
send_transform_update_message="1"
></CharacterDataComponent>
<CharacterPlatformingComponent
animation_to_play=""
jump_keydown_buffer="2"
jump_velocity_y="-95"
jump_velocity_x="56"
fly_model_player="0"
fly_smooth_y="0"
fly_speed_max_up="95"
fly_speed_max_down="85"
fly_speed_mult="20"
fly_speed_change_spd="0.25"
mouse_look="1"
keyboard_look="0"
mouse_look_buffer="1"
pixel_gravity="350"
run_velocity="154"
fly_velocity_x="52"
accel_x="0.15"
turning_buffer="0.5"
velocity_min_x="-57"
velocity_max_x="57"
velocity_min_y="-200"
velocity_max_y="350"
></CharacterPlatformingComponent>
<HitboxComponent
aabb_max_x="0"
aabb_max_y="-20"
aabb_min_x="0"
aabb_min_y="-20"
is_enemy="1"
is_item="0"
is_player="0"
></HitboxComponent>
<HitboxComponent
aabb_max_x="3"
aabb_max_y="4"
aabb_min_x="-3"
aabb_min_y="-12"
is_enemy="1"
is_item="0"
is_player="0"
></HitboxComponent>
<LiquidDisplacerComponent
radius="1"
></LiquidDisplacerComponent>
<KickComponent>
</KickComponent>
<Base file="data/entities/base_jetpack_nosound.xml">
<ParticleEmitterComponent
offset.x="-2"
offset.y="5"
lifetime_min="0.0"
></ParticleEmitterComponent>
</Base>
<GenomeDataComponent
herd_id="player"
food_chain_rank="20"
is_predator="1"
berserk_dont_attack_friends="1"
></GenomeDataComponent>
<HotspotComponent
_tags="hand"
sprite_hotspot_name="hand"
></HotspotComponent>
<DamageModelComponent
air_in_lungs="7"
air_in_lungs_max="7"
air_lack_of_damage="0.6"
air_needed="1"
falling_damage_damage_max="1.2"
falling_damage_damage_min="0.1"
falling_damage_height_max="250"
falling_damage_height_min="70"
falling_damages="0"
fire_damage_amount="0"
invincibility_frames="60"
fire_probability_of_ignition="0"
fire_how_much_fire_generates="0"
hp="4"
is_on_fire="0"
materials_damage="1"
materials_that_damage="acid,lava,blood_cold_vapour,blood_cold,poison,radioactive_gas,radioactive_gas_static,rock_static_radioactive,rock_static_poison,ice_radioactive_static,ice_radioactive_glass,ice_acid_static,ice_acid_glass,rock_static_cursed,magic_gas_hp_regeneration,gold_radioactive,gold_static_radioactive,rock_static_cursed_green,cursed_liquid,poo_gas"
materials_how_much_damage="0.005,0.003,0.0006,0.0009,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.005,-0.005,0.0002,0.0002,0.004,0.0005,0.00001"
ragdoll_filenames_file="data/ragdolls/player/filenames.txt"
ragdoll_offset_y="-5"
ragdollify_child_entity_sprites="1"
blood_spray_material="blood"
physics_objects_damage="0"
drop_items_on_death="0"
critical_damage_resistance="0"
><damage_multipliers
explosion="0.35" >
</damage_multipliers>
</DamageModelComponent>
<SpriteAnimatorComponent>
</SpriteAnimatorComponent>
<SpriteComponent
_tags="character,skin_root"
alpha="1"
image_file="mods/evaisa.arena/files/gfx/player.xml"
next_rect_animation=""
offset_x="6"
offset_y="14"
rect_animation="walk"
z_index="0.6"
></SpriteComponent>
<HotspotComponent
_tags="right_arm_root"
sprite_hotspot_name="right_arm_start"
transform_with_scale="1"
></HotspotComponent>
<Entity name="arm_r" tags="player_arm_r">
<SpriteComponent
_tags="with_item"
alpha="1"
image_file="data/enemies_gfx/player_arm.xml"
next_rect_animation=""
rect_animation="default"
z_index="0.59"
></SpriteComponent>
<InheritTransformComponent
parent_hotspot_tag="right_arm_root"
only_position="1"
></InheritTransformComponent>
<HotspotComponent
_tags="hand"
sprite_hotspot_name="hand"
transform_with_scale="1"
></HotspotComponent>
</Entity>
<HotspotComponent
_tags="cape_root"
sprite_hotspot_name="cape" >
</HotspotComponent>
<Entity name="cape">
<Base file="data/entities/verlet_chains/cape/cape.xml">
</Base>
</Entity>
<Inventory2Component
_enabled="1"
full_inventory_slots_x="0"
full_inventory_slots_y="0"
mSavedActiveItemIndex="0"
quick_inventory_slots="4" >
</Inventory2Component>
<Entity name="inventory_quick">
</Entity>
<StreamingKeepAliveComponent/>
<GunComponent>
</GunComponent>
<ItemPickUpperComponent
drop_items_on_death="0"
is_immune_to_kicks="1"
only_pick_this_entity="52395832806"
></ItemPickUpperComponent>
<PlatformShooterPlayerComponent
center_camera_on_this_entity="0"
aiming_reticle_distance_from_character="60"
camera_max_distance_from_character="50"
move_camera_with_aim="0"
eating_area_min.x="-6"
eating_area_max.x="6"
eating_area_min.y="-4"
eating_area_max.y="6"
eating_cells_per_frame="2"
></PlatformShooterPlayerComponent>
<StatusEffectDataComponent>
</StatusEffectDataComponent>
<IngestionComponent/>
<PlayerCollisionComponent
getting_crushed_threshold="6"
moving_up_before_getting_crushed_threshold="6"
></PlayerCollisionComponent >
<AudioComponent
file="data/audio/Desktop/player.bank"
event_root="player"
audio_physics_material="character_player"
set_latest_event_position="1"
></AudioComponent>
<AudioLoopComponent
_tags="sound_jetpack"
file="data/audio/Desktop/player.bank"
event_name="player/jetpack"
volume_autofade_speed="0.25"
></AudioLoopComponent>
<AudioLoopComponent
_tags="sound_pick_gold_sand"
file="data/audio/Desktop/player.bank"
event_name="player/pick_gold_sand"
volume_autofade_speed="0.05"
></AudioLoopComponent>
</Entity>

BIN
files/sprites/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

10
files/src/ctx.lua Normal file
View file

@ -0,0 +1,10 @@
local ctx = {
}
ctx.init = function()
ctx.host_id = 0
ctx.players = {}
end
return ctx

15
files/src/net.lua Normal file
View file

@ -0,0 +1,15 @@
local first = true
local net = {
get_events = function()
if first then
first = false
return {
{ kind = "connect", peer_id = 1}
}
end
return {}
end
}
return net

321
files/src/player_fns.lua Normal file
View file

@ -0,0 +1,321 @@
local ctx = dofile_once("mods/quant.ew/files/src/ctx.lua")
local ffi = require("ffi")
-- Controls struct
ffi.cdef([[
#pragma pack(push, 1)
typedef struct A {
float aim_x;
float aim_y;
float aimNormal_x;
float aimNormal_y;
float aimNonZero_x;
float aimNonZero_y;
float mouse_x;
float mouse_y;
float mouseRaw_x;
float mouseRaw_y;
float mouseRawPrev_x;
float mouseRawPrev_y;
float mouseDelta_x;
float mouseDelta_y;
bool kick:1;
bool fire:1;
bool fire2:1;
bool action:1;
bool throw:1;
bool interact:1;
bool left:1;
bool right:1;
bool up:1;
bool down:1;
bool jump:1;
bool fly:1;
bool leftClick:1;
bool rightClick:1;
} Controls;
#pragma pack(pop)
]])
local Controls = ffi.typeof("Controls")
local player_fns = {
deserialize_inputs = function(message, player_data)
if (player_data ~= nil and player_data.entity ~= nil and EntityGetIsAlive(player_data.entity)) then
--print(json.stringify(message))
local controls_data = player_data.controls
local controlsComp = EntityGetFirstComponentIncludingDisabled(player_data.entity, "ControlsComponent")
if(message.kick)then
ComponentSetValue2(controlsComp, "mButtonDownKick", true)
if (not controls_data.kick) then
ComponentSetValue2(controlsComp, "mButtonFrameKick", GameGetFrameNum() + 1)
end
controls_data.kick = true
else
ComponentSetValue2(controlsComp, "mButtonDownKick", false)
controls_data.kick = false
end
--EntityHelper.BlockFiring(data.players[tostring(user)].entity, true)
if(message.fire)then
ComponentSetValue2(controlsComp, "mButtonDownFire", true)
if (not controls_data.fire) then
ComponentSetValue2(controlsComp, "mButtonFrameFire", GameGetFrameNum()+1)
end
ComponentSetValue2(controlsComp, "mButtonLastFrameFire", GameGetFrameNum())
controls_data.fire = true
else
ComponentSetValue2(controlsComp, "mButtonDownFire", false)
controls_data.fire = false
end
if(message.fire2)then
ComponentSetValue2(controlsComp, "mButtonDownFire2", true)
if (not controls_data.fire2) then
ComponentSetValue2(controlsComp, "mButtonFrameFire2", GameGetFrameNum()+1)
end
controls_data.fire2 = true
else
ComponentSetValue2(controlsComp, "mButtonDownFire2", false)
controls_data.fire2 = false
end
if(message.action)then
ComponentSetValue2(controlsComp, "mButtonDownAction", true)
if (not controls_data.action) then
ComponentSetValue2(controlsComp, "mButtonFrameAction", GameGetFrameNum() + 1)
end
controls_data.action = true
else
ComponentSetValue2(controlsComp, "mButtonDownAction", false)
controls_data.action = false
end
if(message.throw)then
ComponentSetValue2(controlsComp, "mButtonDownThrow", true)
if (not controls_data.throw) then
ComponentSetValue2(controlsComp, "mButtonFrameThrow", GameGetFrameNum() + 1)
end
controls_data.throw = true
else
ComponentSetValue2(controlsComp, "mButtonDownThrow", false)
controls_data.throw = false
end
if(message.interact)then
ComponentSetValue2(controlsComp, "mButtonDownInteract", true)
if (not controls_data.interact) then
ComponentSetValue2(controlsComp, "mButtonFrameInteract", GameGetFrameNum() + 1)
end
controls_data.interact = true
else
ComponentSetValue2(controlsComp, "mButtonDownInteract", false)
controls_data.interact = false
end
if(message.left)then
ComponentSetValue2(controlsComp, "mButtonDownLeft", true)
if (not controls_data.left) then
ComponentSetValue2(controlsComp, "mButtonFrameLeft", GameGetFrameNum() + 1)
end
controls_data.left = true
else
ComponentSetValue2(controlsComp, "mButtonDownLeft", false)
controls_data.left = false
end
if(message.right)then
ComponentSetValue2(controlsComp, "mButtonDownRight", true)
if (not controls_data.right) then
ComponentSetValue2(controlsComp, "mButtonFrameRight", GameGetFrameNum() + 1)
end
controls_data.right = true
else
ComponentSetValue2(controlsComp, "mButtonDownRight", false)
controls_data.right = false
end
if(message.up)then
ComponentSetValue2(controlsComp, "mButtonDownUp", true)
if (not controls_data.up) then
ComponentSetValue2(controlsComp, "mButtonFrameUp", GameGetFrameNum() + 1)
end
controls_data.up = true
else
ComponentSetValue2(controlsComp, "mButtonDownUp", false)
controls_data.up = false
end
if(message.down)then
ComponentSetValue2(controlsComp, "mButtonDownDown", true)
if (not controls_data.down) then
ComponentSetValue2(controlsComp, "mButtonFrameDown", GameGetFrameNum() + 1)
end
controls_data.down = true
else
ComponentSetValue2(controlsComp, "mButtonDownDown", false)
controls_data.down = false
end
if(message.jump)then
ComponentSetValue2(controlsComp, "mButtonDownJump", true)
if (not controls_data.jump) then
ComponentSetValue2(controlsComp, "mButtonFrameJump", GameGetFrameNum() + 1)
end
controls_data.jump = true
else
ComponentSetValue2(controlsComp, "mButtonDownJump", false)
controls_data.jump = false
end
if(message.fly)then
ComponentSetValue2(controlsComp, "mButtonDownFly", true)
if (not controls_data.fly) then
ComponentSetValue2(controlsComp, "mButtonFrameFly", GameGetFrameNum() + 1)
end
controls_data.fly = true
else
ComponentSetValue2(controlsComp, "mButtonDownFly", false)
controls_data.fly = false
end
if(message.leftClick)then
ComponentSetValue2(controlsComp, "mButtonDownLeftClick", true)
if (not controls_data.leftClick) then
ComponentSetValue2(controlsComp, "mButtonFrameLeftClick", GameGetFrameNum() + 1)
end
controls_data.leftClick = true
else
ComponentSetValue2(controlsComp, "mButtonDownLeftClick", false)
controls_data.leftClick = false
end
if(message.rightClick)then
ComponentSetValue2(controlsComp, "mButtonDownRightClick", true)
if (not controls_data.rightClick) then
ComponentSetValue2(controlsComp, "mButtonFrameRightClick", GameGetFrameNum() + 1)
end
controls_data.rightClick = true
else
ComponentSetValue2(controlsComp, "mButtonDownRightClick", false)
controls_data.rightClick = false
end
--[[
local aim_x, aim_y = ComponentGetValue2(controls, "mAimingVector") -- float, float
local aimNormal_x, aimNormal_y = ComponentGetValue2(controls, "mAimingVectorNormalized") -- float, float
local aimNonZero_x, aimNonZero_y = ComponentGetValue2(controls, "mAimingVectorNonZeroLatest") -- float, float
local mouse_x, mouse_y = ComponentGetValue2(controls, "mMousePosition") -- float, float
local mouseRaw_x, mouseRaw_y = ComponentGetValue2(controls, "mMousePositionRaw") -- float, float
local mouseRawPrev_x, mouseRawPrev_y = ComponentGetValue2(controls, "mMousePositionRawPrev") -- float, float
local mouseDelta_x, mouseDelta_y = ComponentGetValue2(controls, "mMouseDelta") -- float, float
]]
ComponentSetValue2(controlsComp, "mAimingVector", message.aim_x, message.aim_y)
ComponentSetValue2(controlsComp, "mAimingVectorNormalized", message.aimNormal_x, message.aimNormal_y)
ComponentSetValue2(controlsComp, "mAimingVectorNonZeroLatest", message.aimNonZero_x, message.aimNonZero_y)
ComponentSetValue2(controlsComp, "mMousePosition", message.mouse_x, message.mouse_y)
ComponentSetValue2(controlsComp, "mMousePositionRaw", message.mouseRaw_x, message.mouseRaw_y)
ComponentSetValue2(controlsComp, "mMousePositionRawPrev", message.mouseRawPrev_x, message.mouseRawPrev_y)
ComponentSetValue2(controlsComp, "mMouseDelta", message.mouseDelta_x, message.mouseDelta_y)
local children = EntityGetAllChildren(player_data.entity) or {}
for i, child in ipairs(children) do
if (EntityGetName(child) == "cursor") then
--EntitySetTransform(child, message.mouse_x, message.mouse_y)
EntityApplyTransform(child, message.mouse_x, message.mouse_y)
end
end
end
end,
serialize_inputs = function(player_data)
local player = player_data.entity
if (player == nil) then
return
end
local controls = EntityGetFirstComponentIncludingDisabled(player, "ControlsComponent")
if (controls ~= nil) then
local kick = ComponentGetValue2(controls, "mButtonDownKick") -- boolean
local fire = ComponentGetValue2(controls, "mButtonDownFire") -- boolean
local fire2 = ComponentGetValue2(controls, "mButtonDownFire2") -- boolean
local action = ComponentGetValue2(controls, "mButtonDownAction") -- boolean
local throw = ComponentGetValue2(controls, "mButtonDownThrow") -- boolean
local interact = ComponentGetValue2(controls, "mButtonDownInteract") -- boolean
local left = ComponentGetValue2(controls, "mButtonDownLeft") -- boolean
local right = ComponentGetValue2(controls, "mButtonDownRight") -- boolean
local up = ComponentGetValue2(controls, "mButtonDownUp") -- boolean
local down = ComponentGetValue2(controls, "mButtonDownDown") -- boolean
local jump = ComponentGetValue2(controls, "mButtonDownJump") -- boolean
local fly = ComponentGetValue2(controls, "mButtonDownFly") -- boolean
local leftClick = ComponentGetValue2(controls, "mButtonDownLeftClick") -- boolean
local rightClick = ComponentGetValue2(controls, "mButtonDownRightClick") -- boolean
local aim_x, aim_y = ComponentGetValue2(controls, "mAimingVector") -- float, float
local aimNormal_x, aimNormal_y = ComponentGetValue2(controls, "mAimingVectorNormalized") -- float, float
local aimNonZero_x, aimNonZero_y = ComponentGetValue2(controls, "mAimingVectorNonZeroLatest") -- float, float
local mouse_x, mouse_y = ComponentGetValue2(controls, "mMousePosition") -- float, float
local mouseRaw_x, mouseRaw_y = ComponentGetValue2(controls, "mMousePositionRaw") -- float, float
local mouseRawPrev_x, mouseRawPrev_y = ComponentGetValue2(controls, "mMousePositionRawPrev") -- float, float
local mouseDelta_x, mouseDelta_y = ComponentGetValue2(controls, "mMouseDelta") -- float, float
local c = Controls{
kick = kick,
fire = fire,
fire2 = fire2,
action = action,
throw = throw,
interact = interact,
left = left,
right = right,
up = up,
down = down,
jump = jump,
fly = fly,
leftClick = leftClick,
rightClick = rightClick,
aim_x = aim_x,
aim_y = aim_y,
aimNormal_x = aimNormal_x,
aimNormal_y = aimNormal_y,
aimNonZero_x = aimNonZero_x,
aimNonZero_y = aimNonZero_y,
mouse_x = mouse_x,
mouse_y = mouse_y,
mouseRaw_x = mouseRaw_x,
mouseRaw_y = mouseRaw_y,
mouseRawPrev_x = mouseRawPrev_x,
mouseRawPrev_y = mouseRawPrev_y,
mouseDelta_x = mouseDelta_x,
mouseDelta_y = mouseDelta_y,
}
return c
end
end,
make_playerdata_for = function(entity_id)
GamePrint("Made playerdata for "..entity_id)
return {
entity = entity_id,
controls = {},
}
end,
}
function player_fns.spawn_player_for(peer_id)
GamePrint("spawning player for "..peer_id)
local host_ent = ctx.players[ctx.host_id].entity
local x, y = EntityGetFirstHitboxCenter(host_ent)
local new = EntityLoad("mods/quant.ew/files/entities/client.xml", x, y)
local new_playerdata = player_fns.make_playerdata_for(new)
ctx.players[peer_id] = new_playerdata
end
print("Players initialized")
return player_fns

120
init.lua Executable file
View file

@ -0,0 +1,120 @@
dofile_once("mods/quant.ew/NoitaPatcher/load.lua")
local np = require("noitapatcher")
dofile_once( "data/scripts/lib/utilities.lua" )
np.InstallShootProjectileFiredCallbacks()
np.EnableGameSimulatePausing(false)
np.InstallDamageDetailsPatch()
np.SilenceLogs("Warning - streaming didn\'t find any chunks it could stream away...\n")
local player_fns = dofile_once("mods/quant.ew/files/src/player_fns.lua")
local net = dofile_once("mods/quant.ew/files/src/net.lua")
local ctx = dofile_once("mods/quant.ew/files/src/ctx.lua")
-- all functions below are optional and can be left out
--[[
function OnModPreInit()
print("Mod - OnModPreInit()") -- First this is called for all mods
end
function OnModInit()
print("Mod - OnModInit()") -- After that this is called for all mods
end
function OnModPostInit()
print("Mod - OnModPostInit()") -- Then this is called for all mods
end
function OnWorldPostUpdate() -- This is called every time the game has finished updating the world
GamePrint( "Post-update hook " .. tostring(GameGetFrameNum()) )
end
]]--
function OnWorldInitialized() -- This is called once the game world is initialized. Doesn't ensure any world chunks actually exist. Use OnPlayerSpawned to ensure the chunks around player have been loaded or created.
GamePrint( "OnWorldInitialized() " .. tostring(GameGetFrameNum()) )
ctx.init()
end
local my_player = nil
local other_player = nil
function OnPlayerSpawned( player_entity ) -- This runs when player entity has been created
GamePrint( "OnPlayerSpawned() - Player entity id: " .. tostring(player_entity) )
local x, y = EntityGetFirstHitboxCenter(player_entity)
-- local other = EntityLoad("mods/quant.ew/files/entities/client.xml", x, y)
-- np.SetPlayerEntity(player_entity)
my_player = player_fns.make_playerdata_for(player_entity)
ctx.players[ctx.host_id] = my_player
-- other_player = player_fns.make_playerdata_for(other)
end
function OnWorldPreUpdate() -- This is called every time the game is about to start updating the world
if my_player == nil then return end
GamePrint( "Pre-update hook " .. tostring(GameGetFrameNum()) )
local events = net.get_events()
for i=1,#events do
local event = events[i]
GamePrint("event "..i.." "..event.kind)
if event.kind == "connect" then
player_fns.spawn_player_for(event.peer_id)
end
end
-- local sinp = player_fns.serialize_inputs(my_player)
-- player_fns.deserialize_inputs(sinp, other_player)
end
function register_localizations(translation_file, clear_count)
clear_count = clear_count or 0
local loc_content = ModTextFileGetContent("data/translations/common.csv") -- Gets the original translations of the game
local append_content = ModTextFileGetContent(translation_file) -- Gets my own translations file
-- Split the append_content into lines
local lines = {}
for line in append_content:gmatch("[^\n]+") do
table.insert(lines, line)
end
-- Remove the first clear_count lines
for i = 1, clear_count do
table.remove(lines, 1)
end
-- Reconstruct append_content after removing clear_count lines
local new_append_content = table.concat(lines, "\n")
-- if loc_content does not end with a new line, add one
if not loc_content:match("\n$") then
loc_content = loc_content .. "\n"
end
-- Concatenate loc_content and new_append_content without extra newline character
local new_content = loc_content .. new_append_content .. "\n"
-- Set the new content to the file
ModTextFileSetContent("data/translations/common.csv", new_content)
end
function OnModPreInit()
register_localizations("mods/quant.ew/translations.csv", 1)
if ctx.players ~= nil then
print("ctx.players is not nil")
else
print("ctx.players is nil")
end
end
function OnModPostInit()
end
print("entangled_worlds init ok")

18
mod.xml Executable file
View file

@ -0,0 +1,18 @@
<Mod
name="Quant's Entangled worlds"
description="Yet another Noita multiplayer mod"
request_no_api_restrictions="1"
is_game_mode="0"
translation_xml_path=""
translation_csv_path=""
>
</Mod>
<!--
April 1 2021 - a mod declared as game mode (is_game_mode="1") can enable support for multiple save slots via game_mode_supports_save_slots="1"
-->
<!--
If a mod requires access to the full lua api e.g. os.* io.* it has to request acesss via 'request_no_api_restrictions="1"'.
It is recommended that other options are explored first, as we may completely disable those APIs in the future.
By default mods have access to the table, string, math and bit libraries and to the APIs exposed by the game.
-->

4
translations.csv Normal file
View file

@ -0,0 +1,4 @@
,en,ru,pt-br,es-es,de,fr-fr,it,pl,zh-cn,jp,ko,,NOTES use \n for newline,max length
entangled_mode,Entangled Worlds,,,,,,,,,,,,,
entangled_dev,Dev,,,,,,,,,,,,,
entangled_lobby_string,Running,,,,,,,,,,,,,
1 en ru pt-br es-es de fr-fr it pl zh-cn jp ko NOTES – use \n for newline max length
2 entangled_mode Entangled Worlds
3 entangled_dev Dev
4 entangled_lobby_string Running