mirror of
https://github.com/IntQuant/noita_entangled_worlds.git
synced 2025-10-19 15:13:16 +00:00
Initial
This commit is contained in:
commit
ce608c9bdf
19 changed files with 1662 additions and 0 deletions
24
NoitaPatcher/load.lua
Normal file
24
NoitaPatcher/load.lua
Normal 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
|
BIN
NoitaPatcher/noitapatcher.dll
Normal file
BIN
NoitaPatcher/noitapatcher.dll
Normal file
Binary file not shown.
151
NoitaPatcher/noitapatcher.lua
Normal file
151
NoitaPatcher/noitapatcher.lua
Normal 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
|
BIN
NoitaPatcher/noitapatcher.pdb
Normal file
BIN
NoitaPatcher/noitapatcher.pdb
Normal file
Binary file not shown.
11
NoitaPatcher/noitapatcher/nsew/native_dll.lua
Normal file
11
NoitaPatcher/noitapatcher/nsew/native_dll.lua
Normal 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
|
BIN
NoitaPatcher/noitapatcher/nsew/nsew_native.dll
Normal file
BIN
NoitaPatcher/noitapatcher/nsew/nsew_native.dll
Normal file
Binary file not shown.
134
NoitaPatcher/noitapatcher/nsew/rect.lua
Normal file
134
NoitaPatcher/noitapatcher/nsew/rect.lua
Normal 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
|
239
NoitaPatcher/noitapatcher/nsew/world.lua
Normal file
239
NoitaPatcher/noitapatcher/nsew/world.lua
Normal 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
|
276
NoitaPatcher/noitapatcher/nsew/world_ffi.lua
Normal file
276
NoitaPatcher/noitapatcher/nsew/world_ffi.lua
Normal 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
4
compatibility.xml
Executable file
|
@ -0,0 +1,4 @@
|
|||
<Mod
|
||||
version_built_with="1"
|
||||
>
|
||||
</Mod>
|
9
files/entities/chunk_loader.xml
Normal file
9
files/entities/chunk_loader.xml
Normal 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
326
files/entities/client.xml
Normal 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
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
10
files/src/ctx.lua
Normal 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
15
files/src/net.lua
Normal 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
321
files/src/player_fns.lua
Normal 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
120
init.lua
Executable 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
18
mod.xml
Executable 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
4
translations.csv
Normal 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,,,,,,,,,,,,,
|
|
Loading…
Add table
Add a link
Reference in a new issue