refactor a bunch of things to get ready for rust world sync

This commit is contained in:
bgkillas 2025-07-13 21:06:05 -04:00
parent 4ff165b815
commit 067570672d
32 changed files with 540 additions and 3126 deletions

24
blob_guy/Cargo.lock generated
View file

@ -675,9 +675,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -1301,6 +1301,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iced-x86"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
dependencies = [
"lazy_static",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
@ -1497,6 +1506,12 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
@ -1578,9 +1593,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
@ -1683,6 +1698,7 @@ version = "1.6.0"
dependencies = [
"base64",
"eyre",
"iced-x86",
"libloading",
"noita_api_macro",
"object",

6
ewext/Cargo.lock generated
View file

@ -94,9 +94,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -139,7 +139,6 @@ dependencies = [
"backtrace",
"bimap",
"eyre",
"iced-x86",
"libloading",
"noita_api",
"rand",
@ -261,6 +260,7 @@ version = "1.6.0"
dependencies = [
"base64",
"eyre",
"iced-x86",
"libloading",
"noita_api_macro",
"object 0.37.1",

View file

@ -18,7 +18,6 @@ opt-level = 3
[dependencies]
backtrace = "0.3.74"
iced-x86 = "1.21.0"
eyre = "0.6.12"
noita_api = {path = "../noita_api"}
shared = {path = "../shared"}

View file

@ -2,43 +2,51 @@
#[unsafe(no_mangle)]
pub extern "C" fn _unwind_resume() {}
use addr_grabber::{grab_addrs, grabbed_fns, grabbed_globals};
use bimap::BiHashMap;
use eyre::{Context, OptionExt, bail};
use modules::{Module, ModuleCtx, entity_sync::EntitySync};
use net::NetManager;
use noita::{ParticleWorldState, ntypes::Entity, pixel::NoitaPixelRun};
use noita_api::add_lua_fn;
use noita_api::addr_grabber::{grab_addrs, grabbed_fns, grabbed_globals};
use noita_api::noita::types::Entity;
use noita_api::noita::world::ParticleWorldState;
use noita_api::{
DamageModelComponent, EntityID, VariableStorageComponent,
lua::{
LUA, LuaGetValue, LuaState, RawString, ValuesOnStack,
LUA, LuaGetValue, LuaState, RawString,
lua_bindings::{LUA_REGISTRYINDEX, lua_State},
},
};
use rustc_hash::{FxHashMap, FxHashSet};
use shared::des::{Gid, RemoteDes};
use shared::{Destination, NoitaInbound, NoitaOutbound, PeerId, SpawnOnce, WorldPos};
#[cfg(target_arch = "x86")]
use std::arch::asm;
use std::array::IntoIter;
use std::backtrace::Backtrace;
use std::mem::MaybeUninit;
use std::{
arch::asm,
borrow::Cow,
cell::{LazyCell, RefCell},
ffi::{c_int, c_void},
ffi::c_int,
sync::{LazyLock, Mutex, OnceLock, TryLockError},
time::Instant,
};
use std::{num::NonZero, sync::MutexGuard};
mod addr_grabber;
mod modules;
mod net;
pub mod noita;
thread_local! {
static STATE: LazyCell<RefCell<ExtState>> = LazyCell::new(|| {
#[cfg(debug_assertions)]
println!("Initializing ExtState");
ExtState::default().into()
ExtState {
modules: Default::default(),
player_entity_map: Default::default(),
fps_by_player: Default::default(),
dont_spawn: Default::default(),
cam_pos: Default::default(),
}.into()
});
}
@ -92,14 +100,33 @@ impl Drop for TimeTracker {
}
}*/
#[derive(Default)]
struct Modules {
entity_sync: Option<EntitySync>,
pub struct WorldSync {
pub particle_world_state: MaybeUninit<ParticleWorldState>,
pub world_num: u8,
}
impl Default for WorldSync {
fn default() -> Self {
Self {
particle_world_state: MaybeUninit::uninit(),
world_num: 0,
}
}
}
#[derive(Default)]
struct Modules {
entity_sync: EntitySync,
world: WorldSync,
}
impl Modules {
fn iter_mut(&mut self) -> IntoIter<&mut dyn Module, 2> {
let modules: [&mut dyn Module; 2] = [&mut self.entity_sync, &mut self.world];
modules.into_iter()
}
}
struct ExtState {
particle_world_state: Option<ParticleWorldState>,
modules: Modules,
player_entity_map: BiHashMap<PeerId, EntityID>,
fps_by_player: FxHashMap<PeerId, u8>,
@ -118,46 +145,20 @@ impl ExtState {
}
}
fn init_particle_world_state(lua: LuaState) {
#[cfg(debug_assertions)]
println!("\nInitializing particle world state");
let world_pointer = lua.to_integer(1);
let chunk_map_pointer = lua.to_integer(2);
let material_list_pointer = lua.to_integer(3);
#[cfg(debug_assertions)]
println!("pws stuff: {world_pointer:?} {chunk_map_pointer:?}");
fn init_particle_world_state(lua: LuaState) -> eyre::Result<()> {
STATE.with(|state| {
state.borrow_mut().particle_world_state = Some(ParticleWorldState {
_world_ptr: world_pointer as *mut c_void,
chunk_map_ptr: chunk_map_pointer as *mut c_void,
material_list_ptr: material_list_pointer as _,
runner: Default::default(),
});
});
}
fn encode_area(lua: LuaState) -> ValuesOnStack {
let lua = lua.raw();
let start_x = unsafe { LUA.lua_tointeger(lua, 1) } as i32;
let start_y = unsafe { LUA.lua_tointeger(lua, 2) } as i32;
let end_x = unsafe { LUA.lua_tointeger(lua, 3) } as i32;
let end_y = unsafe { LUA.lua_tointeger(lua, 4) } as i32;
let encoded_buffer = unsafe { LUA.lua_tointeger(lua, 5) } as *mut NoitaPixelRun;
STATE.with(|state| {
let mut state = state.borrow_mut();
let pws = state.particle_world_state.as_mut().unwrap();
let runs = unsafe { pws.encode_area(start_x, start_y, end_x, end_y, encoded_buffer) };
unsafe { LUA.lua_pushinteger(lua, runs as isize) };
});
ValuesOnStack(1)
let world = &mut state.borrow_mut().modules.world;
world.particle_world_state = MaybeUninit::new(ParticleWorldState::new()?);
world.world_num = lua.to_integer(1) as u8;
Ok(())
})
}
pub fn ephemerial(entity_id: u32) -> eyre::Result<()> {
unsafe {
let entity_manager = grabbed_globals().entity_manager.read();
let mut entity: *mut Entity;
let entity: *mut Entity;
#[cfg(target_arch = "x86")]
asm!(
"mov ecx, {entity_manager}",
"push {entity_id:e}",
@ -169,10 +170,16 @@ pub fn ephemerial(entity_id: u32) -> eyre::Result<()> {
out("ecx") _,
out("eax") entity,
);
if entity.is_null() {
#[cfg(not(target_arch = "x86"))]
{
std::hint::black_box((entity_manager, grabbed_fns().get_entity));
entity = Default::default();
}
if let Some(entity) = entity.as_mut() {
entity.filename_index = 0;
} else {
bail!("Entity {entity_id} not found");
}
entity.cast::<c_void>().offset(0x8).cast::<u32>().write(0);
}
Ok(())
}
@ -229,9 +236,7 @@ fn netmanager_recv(_lua: LuaState) -> eyre::Result<Option<RawString>> {
NoitaInbound::Ready { .. } => bail!("Unexpected Ready message"),
NoitaInbound::ProxyToDes(proxy_to_des) => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Some(entity_sync) = &mut state.modules.entity_sync
&& let Err(e) = entity_sync.handle_proxytodes(proxy_to_des)
{
if let Err(e) = state.modules.entity_sync.handle_proxytodes(proxy_to_des) {
let _ = print_error(e);
}
})?,
@ -240,22 +245,26 @@ fn netmanager_recv(_lua: LuaState) -> eyre::Result<Option<RawString>> {
message: shared::RemoteMessage::RemoteDes(remote_des),
} => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Some(entity_sync) = &mut state.modules.entity_sync {
match entity_sync.handle_remotedes(
source,
remote_des,
netmanager,
&state.player_entity_map,
&mut state.dont_spawn,
&mut state.cam_pos,
) {
Ok(()) => {}
Err(s) => {
let _ = print_error(s);
}
match state.modules.entity_sync.handle_remotedes(
source,
remote_des,
netmanager,
&state.player_entity_map,
&mut state.dont_spawn,
&mut state.cam_pos,
) {
Ok(()) => {}
Err(s) => {
let _ = print_error(s);
}
}
})?,
NoitaInbound::ProxyToWorldSync(msg) => ExtState::with_global(|state| {
let _lock = IN_MODULE_LOCK.lock().unwrap();
if let Err(e) = state.modules.world.handle_remote(msg) {
let _ = print_error(e);
}
})?,
}
}
Ok(None)
@ -294,11 +303,11 @@ fn on_world_initialized(lua: LuaState) {
std::thread::current().id()
);
grab_addrs(lua);
STATE.with(|state| {
let modules = &mut state.borrow_mut().modules;
modules.entity_sync = Some(EntitySync::default());
ExtState::with_global(|state| {
state.modules.world.particle_world_state =
MaybeUninit::new(ParticleWorldState::new().unwrap());
})
.unwrap()
}
static IN_MODULE_LOCK: Mutex<()> = Mutex::new(());
@ -318,8 +327,8 @@ fn with_every_module(
camera_pos: &mut state.cam_pos,
};
let mut errs = Vec::new();
for module in state.modules.entity_sync.iter_mut() {
if let Err(e) = f(&mut ctx, module as &mut dyn Module) {
for module in state.modules.iter_mut() {
if let Err(e) = f(&mut ctx, module) {
errs.push(e);
}
}
@ -424,17 +433,6 @@ fn probe(_lua: LuaState) {
});
}
fn __gc(_lua: LuaState) {
#[cfg(debug_assertions)]
println!(
"ewext collected in thread {:?}",
std::thread::current().id()
);
NETMANAGER.lock().unwrap().take();
// TODO this doesn't actually work because it's a thread local
STATE.with(|state| state.take());
}
pub(crate) fn print_error(error: eyre::Report) -> eyre::Result<()> {
let lua = LuaState::current()?;
lua.get_global(c"EwextPrintError");
@ -476,12 +474,10 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
// Detect module unload. Adapted from NoitaPatcher.
LUA.lua_newuserdata(lua, 0);
LUA.lua_createtable(lua, 0, 0);
add_lua_fn!(__gc);
LUA.lua_setmetatable(lua, -2);
LUA.lua_setfield(lua, LUA_REGISTRYINDEX, c"luaclose_ewext".as_ptr());
add_lua_fn!(init_particle_world_state);
add_lua_fn!(encode_area);
add_lua_fn!(make_ephemerial);
add_lua_fn!(on_world_initialized);
add_lua_fn!(test_fn);
@ -512,12 +508,7 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
peer_n = peer_n.overflowing_add(rng).0
}
let gid = peer_n.overflowing_mul(rng).0;
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
entity_sync.sync_projectile(
state.modules.entity_sync.sync_projectile(
EntityID(NonZero::try_from(entity)?),
Gid(gid),
peer,
@ -529,12 +520,10 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn des_item_thrown(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
entity_sync.cross_item_thrown(LuaGetValue::get(lua, -1)?)?;
.cross_item_thrown(LuaGetValue::get(lua, -1)?)?;
Ok(())
})?
}
@ -542,11 +531,6 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn des_death_notify(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed = EntityID::try_from(lua.to_integer(1))
.wrap_err("Expected to have a valid entity_killed")?;
let wait_on_kill = lua.to_bool(2);
@ -557,7 +541,7 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
.wrap_err("Expected to have a valid filepath")?;
let entity_responsible = EntityID::try_from(lua.to_integer(6)).ok();
let pos = WorldPos::from_f64(x, y);
entity_sync.cross_death_notify(
state.modules.entity_sync.cross_death_notify(
entity_killed,
wait_on_kill,
pos,
@ -571,15 +555,10 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn notrack(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed: Option<EntityID> = LuaGetValue::get(lua, -1)?;
let entity_killed =
entity_killed.ok_or_eyre("Expected to have a valid entity_killed")?;
entity_sync.notrack_entity(entity_killed);
state.modules.entity_sync.notrack_entity(entity_killed);
Ok(())
})?
}
@ -587,15 +566,10 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn track(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let entity_killed: Option<EntityID> = LuaGetValue::get(lua, -1)?;
let entity_killed =
entity_killed.ok_or_eyre("Expected to have a valid entity_killed")?;
entity_sync.track_entity(entity_killed);
state.modules.entity_sync.track_entity(entity_killed);
Ok(())
})?
}
@ -630,12 +604,7 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn find_by_gid(lua: LuaState) -> eyre::Result<Option<EntityID>> {
ExtState::with_global(|state| {
let gid = lua.to_string(1)?.parse::<u64>()?;
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
Ok(entity_sync.find_by_gid(Gid(gid)))
Ok(state.modules.entity_sync.find_by_gid(Gid(gid)))
})?
}
add_lua_fn!(find_by_gid);
@ -649,11 +618,6 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
let file = lua.to_string(5)?;
let gid = Gid(lua.to_string(6)?.parse::<u64>()?);
let is_mine = lua.to_bool(7);
let entity_sync = state
.modules
.entity_sync
.as_mut()
.ok_or_eyre("No entity sync module loaded")?;
let mut temp = try_lock_netmanager()?;
let net = temp.as_mut().ok_or_eyre("Netmanager not available")?;
if is_mine {
@ -669,7 +633,11 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
ry,
)),
})?;
for (has_interest, peer) in entity_sync.iter_peers(&state.player_entity_map) {
for (has_interest, peer) in state
.modules
.entity_sync
.iter_peers(&state.player_entity_map)
{
if has_interest {
net.send(&NoitaOutbound::RemoteMessage {
reliable: true,
@ -694,7 +662,7 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
})?;
}
}
} else if let Some(peer) = entity_sync.find_peer_by_gid(gid) {
} else if let Some(peer) = state.modules.entity_sync.find_peer_by_gid(gid) {
net.send(&NoitaOutbound::RemoteMessage {
reliable: true,
destination: Destination::Peer(*peer),
@ -733,24 +701,14 @@ pub unsafe extern "C" fn luaopen_ewext(lua: *mut lua_State) -> c_int {
fn set_log(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
state
.modules
.entity_sync
.as_mut()
.unwrap()
.set_perf(lua.to_bool(1));
state.modules.entity_sync.set_perf(lua.to_bool(1));
Ok(())
})?
}
add_lua_fn!(set_log);
fn set_cache(lua: LuaState) -> eyre::Result<()> {
ExtState::with_global(|state| {
state
.modules
.entity_sync
.as_mut()
.unwrap()
.set_cache(lua.to_bool(1));
state.modules.entity_sync.set_cache(lua.to_bool(1));
Ok(())
})?
}

View file

@ -9,6 +9,8 @@ use crate::net::NetManager;
pub(crate) mod entity_sync;
pub(crate) mod world_sync;
pub(crate) struct ModuleCtx<'a> {
pub(crate) net: &'a mut NetManager,
pub(crate) player_map: &'a mut BiHashMap<PeerId, EntityID>,

View file

@ -0,0 +1,42 @@
use crate::WorldSync;
use crate::modules::{Module, ModuleCtx};
use noita_api::noita::world::ParticleWorldState;
use shared::world_sync::ProxyToWorldSync;
impl Module for WorldSync {
fn on_world_update(&mut self, _ctx: &mut ModuleCtx) -> eyre::Result<()> {
std::hint::black_box(unsafe {
self.particle_world_state.assume_init_ref().encode_world()
})?;
std::hint::black_box(unsafe {
self.particle_world_state.assume_init_ref().decode_world()
})?;
//TODO
Ok(())
}
}
impl WorldSync {
pub fn handle_remote(&mut self, msg: ProxyToWorldSync) -> eyre::Result<()> {
match msg {
ProxyToWorldSync::Updates(updates) => {
for _chunk in updates {
//TODO
}
}
}
Ok(())
}
}
trait WorldData {
unsafe fn encode_world(&self) -> eyre::Result<()>;
unsafe fn decode_world(&self) -> eyre::Result<()>;
}
impl WorldData for ParticleWorldState {
unsafe fn encode_world(&self) -> eyre::Result<()> {
//TODO
Ok(())
}
unsafe fn decode_world(&self) -> eyre::Result<()> {
//TODO
Ok(())
}
}

View file

@ -1,101 +0,0 @@
use std::{ffi::c_void, mem};
pub(crate) mod ntypes;
pub(crate) mod pixel;
pub(crate) struct ParticleWorldState {
pub(crate) _world_ptr: *mut c_void,
pub(crate) chunk_map_ptr: *mut c_void,
pub(crate) material_list_ptr: *const c_void,
pub(crate) runner: pixel::PixelRunner<pixel::RawPixel>,
}
impl ParticleWorldState {
fn get_cell_raw(&self, x: i32, y: i32) -> Option<&ntypes::Cell> {
let x = x as isize;
let y = y as isize;
let chunk_index = (((((y) >> 9) - 256) & 511) * 512 + ((((x) >> 9) - 256) & 511)) * 4;
// Deref 1/3
let chunk_arr = unsafe { self.chunk_map_ptr.offset(8).cast::<*const c_void>().read() };
// Deref 2/3
let chunk = unsafe { chunk_arr.offset(chunk_index).cast::<*const c_void>().read() };
if chunk.is_null() {
return None;
}
// Deref 3/3
let pixel_array = unsafe { chunk.cast::<*const c_void>().read() };
let pixel = unsafe { pixel_array.offset((((y & 511) << 9) | x & 511) * 4) };
if pixel.is_null() {
return None;
}
unsafe { pixel.cast::<*const ntypes::Cell>().read().as_ref() }
}
fn get_cell_material_id(&self, cell: &ntypes::Cell) -> u16 {
let mat_ptr = cell.material_ptr();
let offset = unsafe { mat_ptr.cast::<c_void>().offset_from(self.material_list_ptr) };
(offset / ntypes::CELLDATA_SIZE) as u16
}
fn get_cell_type(&self, cell: &ntypes::Cell) -> Option<ntypes::CellType> {
unsafe { Some(cell.material_ptr().as_ref()?.cell_type) }
}
pub(crate) unsafe fn encode_area(
&mut self,
start_x: i32,
start_y: i32,
end_x: i32,
end_y: i32,
mut pixel_runs: *mut pixel::NoitaPixelRun,
) -> usize {
// Allow compiler to generate better code.
assert_eq!(start_x % 128, 0);
assert_eq!(start_y % 128, 0);
assert!((end_x - start_x) <= 128);
assert!((end_y - start_y) <= 128);
for y in start_y..end_y {
for x in start_x..end_x {
let mut raw_pixel = pixel::RawPixel {
material: 0,
flags: 0,
};
let cell = self.get_cell_raw(x, y);
if let Some(cell) = cell {
let cell_type = self.get_cell_type(cell).unwrap_or(ntypes::CellType::None);
match cell_type {
ntypes::CellType::None => {}
// Nobody knows how box2d pixels work.
ntypes::CellType::Solid => {}
ntypes::CellType::Liquid => {
raw_pixel.material = self.get_cell_material_id(cell);
let cell: &ntypes::LiquidCell = unsafe { mem::transmute(cell) };
raw_pixel.flags = cell.is_static as u8;
}
ntypes::CellType::Gas | ntypes::CellType::Fire => {
raw_pixel.material = self.get_cell_material_id(cell);
}
// ???
_ => {}
}
}
self.runner.put_pixel(raw_pixel);
}
}
let built_runner = self.runner.build();
let runs = built_runner.len();
for run in built_runner {
let noita_pixel_run = unsafe { pixel_runs.as_mut().unwrap() };
noita_pixel_run.length = (run.length - 1) as u16;
noita_pixel_run.material = run.data.material;
noita_pixel_run.flags = run.data.flags;
pixel_runs = unsafe { pixel_runs.offset(1) };
}
self.runner.clear();
runs
}
}

View file

@ -1,83 +0,0 @@
// Type defs borrowed from NoitaPatcher.
use std::ffi::{c_char, c_void};
pub(crate) const CELLDATA_SIZE: isize = 0x290;
#[repr(C)]
#[derive(Debug)]
pub(crate) struct StdString {
buffer: *const i8,
sso_buffer: [i8; 12],
size: usize,
capacity: usize,
}
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)]
#[expect(dead_code)]
pub(crate) enum CellType {
None = 0,
Liquid = 1,
Gas = 2,
Solid = 3,
Fire = 4,
Invalid = 4294967295,
}
#[repr(C)]
pub(crate) struct CellData {
name: StdString,
ui_name: StdString,
material_type: i32,
id_2: i32,
pub(crate) cell_type: CellType,
// Has a bunch of other fields that aren't that relevant.
}
#[repr(C)]
pub(crate) struct CellVTable {}
#[repr(C)]
pub(crate) struct Cell {
pub(crate) vtable: *const CellVTable,
hp: i32,
unknown1: [u8; 8],
is_burning: bool,
unknown2: [u8; 3],
material_ptr: *const CellData,
}
#[repr(C)]
pub(crate) struct LiquidCell {
cell: Cell,
x: i32,
y: i32,
unknown1: c_char,
unknown2: c_char,
pub(crate) is_static: bool,
// Has a bunch of other fields that aren't that relevant.
}
impl Cell {
pub(crate) fn material_ptr(&self) -> *const CellData {
self.material_ptr
}
}
#[repr(C)]
pub(crate) struct Entity {
_unknown0: [u8; 8],
_filename_index: u32,
// More stuff, not that relevant currently.
}
#[repr(C)]
pub(crate) struct EntityManager {
_fld: c_void,
// Unknown
}
#[repr(C)]
pub(crate) struct ThiscallFn(c_void);

View file

@ -1,78 +0,0 @@
#[repr(C, packed)]
pub(crate) struct NoitaPixelRun {
pub(crate) length: u16,
pub(crate) material: u16,
pub(crate) flags: u8,
}
/// Copied from proxy.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
/// Copied from proxy.
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug)]
pub(crate) struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
/// Copied from proxy.
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub(crate) struct PixelRunner<Pixel> {
pub(crate) current_pixel: Option<Pixel>,
pub(crate) current_run_len: u32,
pub(crate) runs: Vec<PixelRun<Pixel>>,
}
impl<Pixel: Eq + Copy> Default for PixelRunner<Pixel> {
fn default() -> Self {
Self::new()
}
}
impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
pub(crate) fn new() -> Self {
Self {
current_pixel: None,
current_run_len: 0,
runs: Vec::new(),
}
}
pub(crate) fn put_pixel(&mut self, pixel: Pixel) {
if let Some(current) = self.current_pixel {
if pixel != current {
self.runs.push(PixelRun {
length: self.current_run_len,
data: current,
});
self.current_pixel = Some(pixel);
self.current_run_len = 1;
} else {
self.current_run_len += 1;
}
} else {
self.current_pixel = Some(pixel);
self.current_run_len = 1;
}
}
pub(crate) fn build(&mut self) -> &[PixelRun<Pixel>] {
if self.current_run_len > 0 {
self.runs.push(PixelRun {
length: self.current_run_len,
data: self.current_pixel.expect("has current pixel"),
});
}
&mut self.runs
}
pub(crate) fn clear(&mut self) {
self.current_pixel = None;
self.current_run_len = 0;
self.runs.clear();
}
}

16
noita-proxy/Cargo.lock generated
View file

@ -890,9 +890,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -2625,9 +2625,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
@ -4041,9 +4041,9 @@ dependencies = [
[[package]]
name = "rgb"
version = "0.8.51"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
dependencies = [
"bytemuck",
]
@ -6410,9 +6410,9 @@ dependencies = [
[[package]]
name = "winresource"
version = "0.1.22"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179ac8923651ff1d15efbee760b4dd3679fd85fa5a8b2bb1109b7248f80e30f"
checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957"
dependencies = [
"toml",
"version_check",

View file

@ -55,13 +55,13 @@ use util::{args::Args, steam_helper::LobbyExtraData};
mod bookkeeping;
use crate::net::messages::NetMsg;
use crate::net::omni::OmniPeerId;
use crate::net::world::world_model::ChunkCoord;
use crate::player_cosmetics::{
display_player_skin, get_player_skin, player_path, player_select_current_color_slot,
player_skin_display_color_picker, shift_hue,
};
pub use bookkeeping::{mod_manager, releases, self_update};
use shared::WorldPos;
use shared::world_sync::ChunkCoord;
mod lobby_code;
pub mod net;
mod player_cosmetics;
@ -126,7 +126,7 @@ pub enum LocalHealthMode {
#[serde(default)]
pub struct GameSettings {
seed: u64,
world_num: u16,
world_num: u8,
debug_mode: Option<bool>,
use_constant_seed: bool,
duplicate: Option<bool>,
@ -1660,7 +1660,7 @@ impl App {
fn connect_screen(&mut self, ctx: &Context) {
egui::CentralPanel::default().show(ctx, |ui| {
if self.app_saved_state.times_started % 20 == 0 {
if self.app_saved_state.times_started.is_multiple_of(20) {
let image = egui::Image::new(egui::include_image!("../assets/longleg.png"))
.texture_options(TextureOptions::NEAREST);
image.paint_at(ui, ctx.screen_rect());

View file

@ -25,12 +25,12 @@ use std::{
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use world::{NoitaWorldUpdate, WorldManager};
use world::WorldManager;
use crate::lobby_code::LobbyKind;
use crate::mod_manager::{ModmanagerSettings, get_mods};
use crate::net::world::world_model::ChunkData;
use crate::net::world::world_model::chunk::{Pixel, PixelFlags};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::player_cosmetics::{PlayerPngDesc, create_player_png, get_player_skin};
use crate::steam_helper::LobbyExtraData;
use crate::{
@ -38,6 +38,7 @@ use crate::{
bookkeeping::save_state::{SaveState, SaveStateEntry},
};
use shared::des::ProxyToDes;
use shared::world_sync::{ChunkCoord, ProxyToWorldSync};
use tangled::Reliability;
use tracing::{error, info, warn};
mod audio;
@ -535,12 +536,10 @@ impl NetManager {
for msg in state.world.get_emitted_msgs() {
self.do_message_request(msg)
}
state.world.update();
let updates = state.world.get_noita_updates();
for update in updates {
state.try_ms_write(&ws_encode_proxy_bin(0, &update));
}
let updates = state.world.update();
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(ProxyToWorldSync::Updates(
updates,
)));
if state.had_a_disconnect {
self.broadcast(&NetMsg::NoitaDisconnected, Reliability::Reliable);
@ -718,6 +717,10 @@ impl NetManager {
sendm: &Sender<FxHashMap<u16, u32>>,
) {
match net_msg {
NetMsg::ForwardWorldSyncToProxy(msg) => state.world.handle_noita_msg(src, msg),
NetMsg::ForwardProxyToWorldSync(msg) => {
state.try_ms_write(&NoitaInbound::ProxyToWorldSync(msg));
}
NetMsg::AudioData(data, global, tx, ty, vol) => {
if !self.is_cess.load(Ordering::Relaxed) {
let audio = self.audio.lock().unwrap().clone();
@ -1144,8 +1147,6 @@ impl NetManager {
},
);
}
// Binary message to proxy
3 => self.handle_bin_message_to_proxy(&raw_msg[1..], state),
0 => {
let flags = String::from_utf8_lossy(&raw_msg[1..]).into();
let msg = NetMsg::Flags(flags);
@ -1167,6 +1168,19 @@ impl NetManager {
);
}
}
NoitaOutbound::WorldSyncToProxy(world_sync_msg) => {
if self.is_host() {
state
.world
.handle_noita_msg(self.peer.my_id(), world_sync_msg)
} else {
self.send(
self.peer.host_id(),
&NetMsg::ForwardWorldSyncToProxy(world_sync_msg),
Reliability::Reliable,
);
}
}
NoitaOutbound::RemoteMessage {
reliable,
destination,
@ -1432,29 +1446,6 @@ impl NetManager {
}
}
fn handle_bin_message_to_proxy(&self, msg: &[u8], state: &mut NetInnerState) {
let key = msg[0];
let data = &msg[1..];
match key {
// world frame
0 => {
let update = NoitaWorldUpdate::load(data);
state.world.add_update(update);
}
// world end
1 => {
let pos = data[1..]
.split(|b| *b == b':')
.map(|s| String::from_utf8_lossy(s).parse::<i32>().unwrap_or(0))
.collect::<Vec<i32>>();
state.world.add_end(data[0], &pos);
}
key => {
error!("Unknown bin msg from mod: {:?}", key)
}
}
}
fn end_run(&self, state: &mut NetInnerState) {
self.init_settings.save_state.reset();
{
@ -1468,7 +1459,7 @@ impl NetManager {
.modmanager_settings
.get_progress()
.unwrap_or_default();
if settings.world_num == u16::MAX {
if settings.world_num == u8::MAX {
settings.world_num = 0
} else {
settings.world_num += 1

View file

@ -1,9 +1,9 @@
use super::{omni::OmniPeerId, world::WorldNetMessage};
use crate::net::world::world_model::{ChunkCoord, ChunkData};
use crate::net::world::world_model::ChunkData;
use crate::{GameSettings, player_cosmetics::PlayerPngDesc};
use bitcode::{Decode, Encode};
use rustc_hash::FxHashMap;
use shared::world_sync::ChunkCoord;
pub(crate) type Destination = shared::Destination<OmniPeerId>;
pub(crate) struct MessageRequest<T> {
@ -27,7 +27,9 @@ pub(crate) enum NetMsg {
PlayerColor(PlayerPngDesc, bool, Option<OmniPeerId>, String),
RemoteMsg(shared::RemoteMessage),
ForwardDesToProxy(shared::des::DesToProxy),
ForwardWorldSyncToProxy(shared::world_sync::WorldSyncToProxy),
ForwardProxyToDes(shared::des::ProxyToDes),
ForwardProxyToWorldSync(shared::world_sync::ProxyToWorldSync),
NoitaDisconnected,
Flags(String),
RespondFlagNormal(String, bool),

View file

@ -4,22 +4,19 @@ use rand::{Rng, rng};
use rayon::iter::IntoParallelIterator;
use rayon::iter::ParallelIterator;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::f32::consts::TAU;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::time::Duration;
use std::{cmp, env, mem, thread};
use std::{cmp, mem, thread};
use tracing::{debug, info, warn};
use wide::f32x8;
use world_model::{
CHUNK_SIZE, ChunkCoord, ChunkData, ChunkDelta, WorldModel,
ChunkData, ChunkDelta, WorldModel,
chunk::{Chunk, Pixel},
};
pub use world_model::encoding::NoitaWorldUpdate;
use crate::bookkeeping::save_state::{SaveState, SaveStateEntry};
use super::{
@ -30,12 +27,6 @@ use super::{
pub mod world_model;
#[derive(Debug, Serialize, Deserialize)]
pub enum WorldUpdateKind {
Update(NoitaWorldUpdate),
End,
}
#[derive(Debug, Decode, Encode, Clone)]
pub(crate) enum WorldNetMessage {
// Authority request
@ -75,13 +66,13 @@ pub(crate) enum WorldNetMessage {
RelinquishAuthority {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
world_num: u8,
},
// Ttell how to update a chunk storage
// Tell how to update a chunk storage
UpdateStorage {
chunk: ChunkCoord,
chunk_data: Option<ChunkData>,
world_num: i32,
world_num: u8,
priority: Option<u8>,
},
// When listening
@ -200,7 +191,7 @@ pub(crate) struct WorldManager {
chunk_last_update: FxHashMap<ChunkCoord, u64>,
/// Stores last priority we used for that chunk, in case transfer fails and we'll need to request authority normally.
last_request_priority: FxHashMap<ChunkCoord, u8>,
world_num: i32,
world_num: u8,
pub materials: FxHashMap<u16, (u32, u32, CellType, u32)>,
is_storage_recent: FxHashSet<ChunkCoord>,
explosion_pointer: FxHashMap<ChunkCoord, Vec<usize>>,
@ -330,64 +321,23 @@ impl WorldManager {
self.chunk_storage.clone()
}
pub(crate) fn add_update(&mut self, update: NoitaWorldUpdate) {
self.outbound_model
.apply_noita_update(&update, &mut self.is_storage_recent);
}
pub(crate) fn add_end(&mut self, priority: u8, pos: &[i32]) {
let updated_chunks = self
.outbound_model
.updated_chunks()
.iter()
.copied()
.collect::<Vec<_>>();
self.current_update += 1;
let chunks_to_send: Vec<Vec<(OmniPeerId, u8)>> = updated_chunks
.iter()
.map(|chunk| self.chunk_updated_locally(*chunk, priority, pos))
.collect();
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
for (chunk, who_sending) in updated_chunks.iter().zip(chunks_to_send.iter()) {
let Some(delta) = self.outbound_model.get_chunk_delta(*chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(*peer)
.or_default()
.push((delta.clone(), *pri));
}
}
let mut emit_queue = Vec::new();
for (peer, chunkpacket) in chunk_packet {
emit_queue.push((
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
));
}
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
fn chunk_updated_locally(
&mut self,
chunk: ChunkCoord,
priority: u8,
pos: &[i32],
pos: Option<(i32, i32, i32, i32, bool)>,
world_num: u8,
) -> Vec<(OmniPeerId, u8)> {
if pos.len() == 6 {
self.my_pos = (pos[0], pos[1]);
self.cam_pos = (pos[2], pos[3]);
self.is_notplayer = pos[4] == 1;
if self.world_num != pos[5] {
self.world_num = pos[5];
if let Some((px, py, cx, cy, is_not)) = pos {
self.my_pos = (px, py);
self.cam_pos = (cx, cy);
self.is_notplayer = is_not;
if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
} else if self.world_num != pos[0] {
self.world_num = pos[0];
} else if self.world_num != world_num {
self.world_num = world_num;
self.reset();
}
let entry = self.chunk_state.entry(chunk).or_insert_with(|| {
@ -508,7 +458,7 @@ impl WorldManager {
chunks_to_send
}
pub(crate) fn update(&mut self) {
pub(crate) fn update(&mut self) -> Vec<NoitaWorldUpdate> {
fn should_kill(
my_pos: (i32, i32),
cam_pos: (i32, i32),
@ -638,18 +588,11 @@ impl WorldManager {
}
retain
});
self.get_noita_updates()
}
pub(crate) fn get_noita_updates(&mut self) -> Vec<Vec<u8>> {
// Sends random data to noita to check if it crashes.
if env::var_os("NP_WORLD_SYNC_TEST").is_some() && self.current_update % 10 == 0 {
let chunk_data = ChunkData::make_random();
self.inbound_model
.apply_chunk_data(ChunkCoord(0, 0), &chunk_data)
}
let updates = self.inbound_model.get_all_noita_updates();
self.inbound_model.reset_change_tracking();
updates
pub(crate) fn get_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
self.inbound_model.get_all_noita_updates()
}
pub(crate) fn reset(&mut self) {
@ -3137,6 +3080,7 @@ use crate::net::world::world_model::chunk::PixelFlags;
use rand::seq::SliceRandom;
#[cfg(test)]
use serial_test::serial;
use shared::world_sync::{CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, WorldSyncToProxy};
#[cfg(test)]
#[test]
#[serial]
@ -3406,3 +3350,52 @@ fn test_cut_perf() {
}
println!("total micros: {}", total / iters);
}
impl WorldManager {
pub fn handle_noita_msg(&mut self, _: OmniPeerId, msg: WorldSyncToProxy) {
match msg {
WorldSyncToProxy::Updates(updates) => {
for update in updates {
self.outbound_model
.apply_noita_update(update, &mut self.is_storage_recent)
}
}
WorldSyncToProxy::End(pos, priority, world_num) => {
let updated_chunks = self
.outbound_model
.updated_chunks()
.iter()
.copied()
.collect::<Vec<_>>();
self.current_update += 1;
let chunks_to_send: Vec<Vec<(OmniPeerId, u8)>> = updated_chunks
.iter()
.map(|chunk| self.chunk_updated_locally(*chunk, priority, pos, world_num))
.collect();
let mut chunk_packet: HashMap<OmniPeerId, Vec<(ChunkDelta, u8)>> = HashMap::new();
for (chunk, who_sending) in updated_chunks.iter().zip(chunks_to_send.iter()) {
let Some(delta) = self.outbound_model.get_chunk_delta(*chunk, false) else {
continue;
};
for (peer, pri) in who_sending {
chunk_packet
.entry(*peer)
.or_default()
.push((delta.clone(), *pri));
}
}
let mut emit_queue = Vec::new();
for (peer, chunkpacket) in chunk_packet {
emit_queue.push((
Destination::Peer(peer),
WorldNetMessage::ChunkPacket { chunkpacket },
));
}
for (dst, msg) in emit_queue {
self.emit_msg(dst, msg)
}
self.outbound_model.reset_change_tracking();
}
}
}
}

View file

@ -3,18 +3,13 @@ use std::sync::Arc;
use bitcode::{Decode, Encode};
use chunk::{Chunk, CompactPixel, Pixel, PixelFlags};
use encoding::{NoitaWorldUpdate, PixelRun, PixelRunner};
use encoding::PixelRunner;
use rustc_hash::{FxHashMap, FxHashSet};
use shared::world_sync::{CHUNK_SIZE, ChunkCoord, NoitaWorldUpdate, PixelRun};
use tracing::info;
pub(crate) mod chunk;
pub mod encoding;
pub(crate) const CHUNK_SIZE: usize = 128;
#[derive(Debug, Encode, Decode, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ChunkCoord(pub i32, pub i32);
#[derive(Default)]
pub(crate) struct WorldModel {
chunks: FxHashMap<ChunkCoord, Chunk>,
@ -38,7 +33,7 @@ pub(crate) struct ChunkDelta {
}
impl ChunkData {
pub(crate) fn make_random() -> Self {
/*pub(crate) fn make_random() -> Self {
let mut runner = PixelRunner::new();
for i in 0..CHUNK_SIZE * CHUNK_SIZE {
runner.put_pixel(
@ -51,7 +46,7 @@ impl ChunkData {
}
let runs = runner.build();
ChunkData { runs }
}
}*/
#[cfg(test)]
pub(crate) fn new(mat: u16) -> Self {
@ -123,17 +118,17 @@ impl WorldModel {
self.updated_chunks.insert(chunk_coord);
}*/
fn get_pixel(&self, x: i32, y: i32) -> Pixel {
/*fn get_pixel(&self, x: i32, y: i32) -> Pixel {
let (chunk_coord, offset) = Self::get_chunk_coords(x, y);
self.chunks
.get(&chunk_coord)
.map(|chunk| chunk.pixel(offset))
.unwrap_or_default()
}
}*/
pub fn apply_noita_update(
&mut self,
update: &NoitaWorldUpdate,
update: NoitaWorldUpdate,
changed: &mut FxHashSet<ChunkCoord>,
) {
fn set_pixel(pixel: Pixel, chunk: &mut Chunk, offset: usize) -> bool {
@ -145,21 +140,23 @@ impl WorldModel {
false
}
}
let header = &update.header;
let runs = &update.runs;
let mut x = 0;
let mut y = 0;
let (mut chunk_coord, _) = Self::get_chunk_coords(header.x, header.y);
let mut chunk = self.chunks.entry(chunk_coord).or_default();
for run in runs {
let (start_x, start_y) = (
update.coord.0 * CHUNK_SIZE as i32,
update.coord.1 * CHUNK_SIZE as i32,
);
let mut chunk_coord = update.coord;
let mut chunk = self.chunks.entry(update.coord).or_default();
for run in update.runs {
let flags = if run.data.flags > 0 {
PixelFlags::Fluid
} else {
PixelFlags::Normal
};
for _ in 0..run.length {
let xs = header.x + x;
let ys = header.y + y;
let xs = start_x + x;
let ys = start_y + y;
let (new_chunk_coord, offset) = Self::get_chunk_coords(xs, ys);
if chunk_coord != new_chunk_coord {
chunk_coord = new_chunk_coord;
@ -178,37 +175,32 @@ impl WorldModel {
changed.remove(&chunk_coord);
}
}
x += 1;
if x == i32::from(header.w) + 1 {
if x == CHUNK_SIZE as i32 {
x = 0;
y += 1;
} else {
x += 1;
}
}
}
}
pub fn get_noita_update(&self, x: i32, y: i32, w: u32, h: u32) -> NoitaWorldUpdate {
assert!(w <= 256);
assert!(h <= 256);
let mut runner = PixelRunner::new();
for j in 0..(h as i32) {
for i in 0..(w as i32) {
runner.put_pixel(self.get_pixel(x + i, y + j).to_raw())
}
}
runner.into_noita_update(x, y, (w - 1) as u8, (h - 1) as u8)
}
pub fn get_all_noita_updates(&self) -> Vec<Vec<u8>> {
pub fn get_all_noita_updates(&mut self) -> Vec<NoitaWorldUpdate> {
let mut updates = Vec::new();
for chunk_coord in &self.updated_chunks {
let update = self.get_noita_update(
chunk_coord.0 * (CHUNK_SIZE as i32),
chunk_coord.1 * (CHUNK_SIZE as i32),
CHUNK_SIZE as u32,
CHUNK_SIZE as u32,
);
updates.push(update.save());
for coord in self.updated_chunks.drain() {
if let Some(chunk) = self.chunks.get_mut(&coord) {
chunk.clear_changed();
let mut runner = PixelRunner::new();
for j in 0..CHUNK_SIZE {
for i in 0..CHUNK_SIZE {
runner.put_pixel(chunk.pixel(i + j * CHUNK_SIZE).to_raw())
}
}
updates.push(NoitaWorldUpdate {
coord,
runs: runner.build(),
});
}
}
updates
}

View file

@ -1,12 +1,9 @@
use std::num::NonZeroU16;
use super::{ChunkData, encoding::PixelRunner};
use bitcode::{Decode, Encode};
use crossbeam::atomic::AtomicCell;
use super::{
CHUNK_SIZE, ChunkData,
encoding::{PixelRunner, RawPixel},
};
use shared::world_sync::{CHUNK_SIZE, RawPixel};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)]
pub enum PixelFlags {

View file

@ -1,46 +1,12 @@
use bitcode::{Decode, Encode};
use bytemuck::{AnyBitPattern, NoUninit, bytes_of, pod_read_unaligned};
use serde::{Deserialize, Serialize};
use std::mem::size_of;
#[derive(Debug, Clone, Copy, AnyBitPattern, NoUninit, Serialize, Deserialize, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct Header {
pub x: i32,
pub y: i32,
pub w: u8,
pub h: u8,
pub run_count: u16,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct NoitaWorldUpdate {
pub(crate) header: Header,
pub(crate) runs: Vec<PixelRun<RawPixel>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
struct ByteParser<'a> {
use shared::world_sync::PixelRun;
/*struct ByteParser<'a> {
data: &'a [u8],
}
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode)]
pub struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
}*/
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub struct PixelRunner<Pixel> {
current_pixel: Option<Pixel>,
current_run_len: u32,
current_run_len: u16,
runs: Vec<PixelRun<Pixel>>,
}
@ -86,24 +52,7 @@ impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
}
}
impl PixelRunner<RawPixel> {
/// Note: w/h are actualy width/height -1
pub fn into_noita_update(self, x: i32, y: i32, w: u8, h: u8) -> NoitaWorldUpdate {
let runs = self.build();
NoitaWorldUpdate {
header: Header {
x,
y,
w,
h,
run_count: runs.len() as u16,
},
runs,
}
}
}
impl<'a> ByteParser<'a> {
/*impl<'a> ByteParser<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data }
}
@ -117,7 +66,7 @@ impl<'a> ByteParser<'a> {
fn next_run(&mut self) -> PixelRun<RawPixel> {
PixelRun {
length: u32::from(self.next::<u16>()) + 1,
length: self.next::<u16>() + 1,
data: RawPixel {
material: self.next(),
flags: self.next(),
@ -125,37 +74,4 @@ impl<'a> ByteParser<'a> {
}
}
}
impl NoitaWorldUpdate {
pub fn load(data: &[u8]) -> Self {
let mut parser = ByteParser::new(data);
let header: Header = parser.next();
let mut runs = Vec::with_capacity(header.run_count.into());
for _ in 0..header.run_count {
runs.push(parser.next_run());
}
assert!(parser.data.is_empty());
Self { header, runs }
}
pub fn save(&self) -> Vec<u8> {
let header = Header {
run_count: self.runs.len() as u16,
..self.header
};
let mut buf = Vec::new();
buf.extend_from_slice(bytes_of(&header));
for run in &self.runs {
let len = u16::try_from(run.length - 1).unwrap();
buf.extend_from_slice(bytes_of(&len));
buf.extend_from_slice(bytes_of(&run.data.material));
buf.extend_from_slice(bytes_of(&run.data.flags));
}
buf
}
}
*/

20
noita_api/Cargo.lock generated
View file

@ -58,9 +58,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@ -128,6 +128,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iced-x86"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
dependencies = [
"lazy_static",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -140,6 +149,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libloading"
version = "0.8.8"
@ -171,6 +186,7 @@ version = "1.6.0"
dependencies = [
"base64",
"eyre",
"iced-x86",
"libloading",
"noita_api_macro",
"object",

View file

@ -12,4 +12,5 @@ base64 = "0.22.1"
rustc-hash = "2.0.0"
smallvec = "1.15.1"
object = "0.37.1"
rayon = "1.10.0"
rayon = "1.10.0"
iced-x86 = "1.21.0"

View file

@ -1,10 +1,8 @@
use std::{mem, os::raw::c_void, ptr, sync::OnceLock};
use crate::lua::LuaState;
use crate::noita::types::{EntityManager, ThiscallFn};
use iced_x86::{Decoder, DecoderOptions, Mnemonic};
use noita_api::lua::LuaState;
use crate::noita::ntypes::{EntityManager, ThiscallFn};
static GRABBED: OnceLock<Grabbed> = OnceLock::new();
pub(crate) unsafe fn grab_addr_from_instruction(
@ -41,42 +39,16 @@ struct Grabbed {
unsafe impl Sync for Grabbed {}
unsafe impl Send for Grabbed {}
pub(crate) struct GrabbedGlobals {
pub struct GrabbedGlobals {
// These 3 actually point to a pointer.
pub(crate) _game_global: *mut usize,
pub(crate) _world_state_entity: *mut usize,
pub(crate) entity_manager: *const *mut EntityManager,
pub entity_manager: *const *mut EntityManager,
}
pub(crate) struct GrabbedFns {
pub(crate) get_entity: *const ThiscallFn, //unsafe extern "C" fn(*const EntityManager, u32) -> *mut Entity,
pub struct GrabbedFns {
pub get_entity: *const ThiscallFn, //unsafe extern "C" fn(*const EntityManager, u32) -> *mut Entity,
}
pub(crate) fn grab_addrs(lua: LuaState) {
lua.get_global(c"GameGetWorldStateEntity");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let world_state_entity =
unsafe { grab_addr_from_instruction(base, 0x007aa7ce - 0x007aa540, Mnemonic::Mov).cast() };
#[cfg(debug_assertions)]
println!(
"World state entity addr: 0x{:x}",
world_state_entity as usize
);
lua.pop_last();
lua.get_global(c"GameGetFrameNum");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let load_game_global =
unsafe { grab_addr_from_instruction(base, 0x007bf3c9 - 0x007bf140, Mnemonic::Call) }; // CALL load_game_global
#[cfg(debug_assertions)]
println!("Load game global addr: 0x{:x}", load_game_global as usize);
let game_global = unsafe {
grab_addr_from_instruction(load_game_global, 0x00439c17 - 0x00439bb0, Mnemonic::Mov).cast()
};
#[cfg(debug_assertions)]
println!("Game global addr: 0x{:x}", game_global as usize);
lua.pop_last();
pub fn grab_addrs(lua: LuaState) {
lua.get_global(c"EntityGetFilename");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let get_entity = unsafe {
@ -86,30 +58,22 @@ pub(crate) fn grab_addrs(lua: LuaState) {
Mnemonic::Call,
))
};
#[cfg(debug_assertions)]
println!("get_entity addr: 0x{:x}", get_entity as usize);
let entity_manager =
unsafe { grab_addr_from_instruction(base, 0x00797821 - 0x00797570, Mnemonic::Mov).cast() };
#[cfg(debug_assertions)]
println!("entity_manager addr: 0x{:x}", entity_manager as usize);
lua.pop_last();
GRABBED
.set(Grabbed {
globals: GrabbedGlobals {
_game_global: game_global,
_world_state_entity: world_state_entity,
entity_manager,
},
globals: GrabbedGlobals { entity_manager },
fns: GrabbedFns { get_entity },
})
.ok();
}
pub(crate) fn grabbed_fns() -> &'static GrabbedFns {
pub fn grabbed_fns() -> &'static GrabbedFns {
&GRABBED.get().expect("to be initialized early").fns
}
pub(crate) fn grabbed_globals() -> &'static GrabbedGlobals {
pub fn grabbed_globals() -> &'static GrabbedGlobals {
&GRABBED.get().expect("to be initialized early").globals
}

View file

@ -16,6 +16,7 @@ use std::{
pub mod lua;
pub mod serialize;
pub use noita_api_macro::add_lua_fn;
pub mod addr_grabber;
pub mod noita;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -33,7 +33,9 @@ pub fn get_functions() -> eyre::Result<(&'static types::CellVTable, *mut types::
0xb1, 0x70, 0x00, 0x60, 0xb1, 0x70, 0x00, 0xb0, 0xf5, 0x70, 0x00, 0xd0, 0xf5, 0x70, 0x00,
0xf0, 0xcd, 0x70, 0x00, 0x50, 0xf7, 0x70, 0x00, 0xe0, 0xc0, 0x4a, 0x00, 0xf0, 0xf7, 0x70,
0x00, 0x20, 0xc0, 0x4a, 0x00, 0x60, 0xf1, 0x70, 0x00, 0xf0, 0xea, 0x70, 0x00, 0x90, 0xef,
0x70, 0x00, 0x60, 0xf3, 0x70, 0x00, 0x50, 0xaf, 0x70, 0x00, 0xd0, 0xb1, 0x70, 0x00,
0x70, 0x00, 0x60, 0xf3, 0x70, 0x00, 0x50, 0xaf, 0x70, 0x00, 0xd0, 0xb1, 0x70,
0x00,
//TODO i should search for a function in the vtable then find the vtable prob
];
let start = rdata.address() as *const c_void;
let cellvtable_ptr = unsafe {

View file

@ -102,10 +102,10 @@ impl Debug for ChunkMap {
#[derive(Debug)]
pub struct GridWorldVTable {
//ptr is 0x10013bc
unknown: [*const c_void; 3],
pub get_chunk_map: *const c_void,
unknownmagic: *const c_void,
unknown2: [*const c_void; 29],
unknown: [*const ThiscallFn; 3],
pub get_chunk_map: *const ThiscallFn,
unknownmagic: *const ThiscallFn,
unknown2: [*const ThiscallFn; 29],
}
#[repr(C)]
@ -473,6 +473,7 @@ pub union CellVTable {
gas: GasCellVTable,
//ptr is 0xff8a6c
solid: SolidCellVTable,
//ptr is 0x10096e0
fire: FireCellVTable,
}
@ -485,69 +486,73 @@ impl Debug for CellVTable {
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct SolidCellVTable {
unknown0: *const c_void,
unknown1: *const c_void,
unknown2: *const c_void,
unknown3: *const c_void,
unknown4: *const c_void,
unknown5: *const c_void,
unknown6: *const c_void,
unknown0: *const ThiscallFn,
unknown1: *const ThiscallFn,
unknown2: *const ThiscallFn,
unknown3: *const ThiscallFn,
unknown4: *const ThiscallFn,
unknown5: *const ThiscallFn,
unknown6: *const ThiscallFn,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct NoneCellVTable {
unknown: [*const c_void; 41],
unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GasCellVTable {}
pub struct GasCellVTable {
unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FireCellVTable {}
pub struct FireCellVTable {
unknown: [*const ThiscallFn; 41],
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct LiquidCellVTable {
pub destroy: *const c_void,
pub get_cell_type: *const c_void,
unknown01: *const c_void,
unknown02: *const c_void,
unknown03: *const c_void,
pub get_color: *const c_void,
unknown04: *const c_void,
pub set_color: *const c_void,
unknown05: *const c_void,
unknown06: *const c_void,
unknown07: *const c_void,
unknown08: *const c_void,
pub get_material: *const c_void,
unknown09: *const c_void,
unknown10: *const c_void,
unknown11: *const c_void,
unknown12: *const c_void,
unknown13: *const c_void,
unknown14: *const c_void,
unknown15: *const c_void,
pub get_position: *const c_void,
unknown16: *const c_void,
unknown17: *const c_void,
unknown18: *const c_void,
unknown19: *const c_void,
unknown20: *const c_void,
unknown21: *const c_void,
unknown22: *const c_void,
unknown23: *const c_void,
pub is_burning: *const c_void,
unknown24: *const c_void,
unknown25: *const c_void,
unknown26: *const c_void,
pub stop_burning: *const c_void,
unknown27: *const c_void,
unknown28: *const c_void,
unknown29: *const c_void,
unknown30: *const c_void,
unknown31: *const c_void,
pub remove: *const c_void,
unknown32: *const c_void,
pub destroy: *const ThiscallFn,
pub get_cell_type: *const ThiscallFn,
unknown01: *const ThiscallFn,
unknown02: *const ThiscallFn,
unknown03: *const ThiscallFn,
pub get_color: *const ThiscallFn,
unknown04: *const ThiscallFn,
pub set_color: *const ThiscallFn,
unknown05: *const ThiscallFn,
unknown06: *const ThiscallFn,
unknown07: *const ThiscallFn,
unknown08: *const ThiscallFn,
pub get_material: *const ThiscallFn,
unknown09: *const ThiscallFn,
unknown10: *const ThiscallFn,
unknown11: *const ThiscallFn,
unknown12: *const ThiscallFn,
unknown13: *const ThiscallFn,
unknown14: *const ThiscallFn,
unknown15: *const ThiscallFn,
pub get_position: *const ThiscallFn,
unknown16: *const ThiscallFn,
unknown17: *const ThiscallFn,
unknown18: *const ThiscallFn,
unknown19: *const ThiscallFn,
unknown20: *const ThiscallFn,
unknown21: *const ThiscallFn,
unknown22: *const ThiscallFn,
unknown23: *const ThiscallFn,
pub is_burning: *const ThiscallFn,
unknown24: *const ThiscallFn,
unknown25: *const ThiscallFn,
unknown26: *const ThiscallFn,
pub stop_burning: *const ThiscallFn,
unknown27: *const ThiscallFn,
unknown28: *const ThiscallFn,
unknown29: *const ThiscallFn,
unknown30: *const ThiscallFn,
unknown31: *const ThiscallFn,
pub remove: *const ThiscallFn,
unknown32: *const ThiscallFn,
}
#[repr(C)]
#[derive(Debug)]
@ -558,7 +563,7 @@ pub struct Position {
}
#[repr(C)]
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Copy)]
pub struct Cell {
pub vtable: &'static CellVTable,
@ -585,10 +590,10 @@ pub enum FullCell {
impl From<&Cell> for FullCell {
fn from(value: &Cell) -> Self {
match value.material.cell_type {
CellType::Liquid => FullCell::LiquidCell(value.get_liquid().clone()),
CellType::Fire => FullCell::FireCell(value.get_fire().clone()),
CellType::Gas => FullCell::GasCell(value.get_gas().clone()),
CellType::None | CellType::Solid => FullCell::Cell(value.clone()),
CellType::Liquid => FullCell::LiquidCell(*value.get_liquid()),
CellType::Fire => FullCell::FireCell(*value.get_fire()),
CellType::Gas => FullCell::GasCell(*value.get_gas()),
CellType::None | CellType::Solid => FullCell::Cell(*value),
}
}
}
@ -627,37 +632,99 @@ unsafe impl Sync for CellPtr {}
unsafe impl Send for CellPtr {}
#[repr(C)]
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct FireCell {
pub cell: Cell,
pub x: isize,
pub y: isize,
unknown1: u8,
unknown2: u8,
unknown3: u8,
unknown4: u8,
pub lifetime: isize,
unknown: isize,
}
impl FireCell {
///# Safety
pub unsafe fn create(
mat: &'static CellData,
vtable: &'static CellVTable,
world: *mut GridWorld,
) -> Self {
let lifetime = if let Some(world) = unsafe { world.as_mut() } {
world.rng *= 0x343fd;
world.rng += 0x269ec3;
(world.rng >> 0x10 & 0x7fff) % 0x15
} else {
-1
};
let mut cell = Cell::create(mat, vtable);
cell.is_burning = true;
Self {
cell,
x: 0,
y: 0,
lifetime,
unknown: 1,
}
}
}
#[repr(C)]
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct GasCell {
pub cell: Cell,
unknown5: isize,
unknown6: isize,
pub x: isize,
pub y: isize,
unknown1: u8,
unknown2: u8,
unknown3: u8,
unknown4: u8,
unknown5: isize,
unknown6: isize,
unknown7: isize,
pub color: Color,
pub not_color: Color,
unknown7: isize,
unknown8: isize,
lifetime: isize,
}
impl GasCell {
///# Safety
pub unsafe fn create(
mat: &'static CellData,
vtable: &'static CellVTable,
world: *mut GridWorld,
) -> Self {
let (bool, lifetime) = if let Some(world) = unsafe { world.as_mut() } {
let life = ((mat.lifetime as f32 * 0.3) as u64).max(1);
world.rng *= 0x343fd;
world.rng += 0x269ec3;
(
(world.rng >> 0x10 & 0x7fff) % 0x65 < 0x32,
(((world.rng >> 0x10 & 0x7fff) as u64 % (life * 2 + 1)) - life) as isize,
)
} else {
(false, -1)
};
let mut cell = Cell::create(mat, vtable);
cell.is_burning = true;
Self {
cell,
unknown5: if bool { 1 } else { 0xff },
unknown6: 0,
x: 0,
y: 0,
unknown1: 0,
unknown2: 0,
unknown3: 0,
unknown4: 0,
unknown7: 0,
unknown8: 0,
color: mat.default_primary_color,
lifetime,
}
}
}
#[repr(C)]
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct LiquidCell {
pub cell: Cell,
pub x: isize,
@ -760,3 +827,16 @@ pub struct GameGlobal {
unknown2: [isize; 11],
pub pause_state: isize,
}
#[repr(C)]
pub struct Entity {
_unknown0: [u8; 8],
pub filename_index: u32,
// More stuff, not that relevant currently.
}
#[repr(C)]
pub struct EntityManager {
_fld: c_void,
// Unknown
}
#[repr(C)]
pub struct ThiscallFn(c_void);

View file

@ -187,9 +187,7 @@ end
local DEST_PROXY = 1
local DEST_BROADCAST = 2
local DEST_PROXY_BIN = 3
local DEST_FLAGS = 0
local MOD_RELIABLE = 4 -- 0b101
function net.send_internal(msg, dest, reliable)
@ -228,10 +226,6 @@ function net.proxy_send(key, value)
net.send_internal(key .. " " .. value, DEST_PROXY)
end
function net.proxy_bin_send(key, value)
ewext.netmanager_send(string.char(DEST_PROXY_BIN, key) .. value)
end
function net.proxy_notify_game_over()
net.proxy_send("game_over", 1)
end

View file

@ -1,914 +0,0 @@
local item_sync = dofile_once("mods/quant.ew/files/system/item_sync.lua")
local effect_sync = dofile_once("mods/quant.ew/files/system/game_effect_sync/game_effect_sync.lua")
local stain_sync = dofile_once("mods/quant.ew/files/system/effect_data_sync/effect_data_sync.lua")
local ffi = require("ffi")
local rpc = net.new_rpc_namespace()
local EnemyData = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
})
-- Variant of EnemyData for when we don't have any motion (or no VelocityComponent).
local EnemyDataNoMotion = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y" },
})
local EnemyDataWorm = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy", "tx", "ty" },
})
local EnemyDataKolmi = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
bool = { "enabled" },
vecfloat = { "legs" },
})
local EnemyDataMom = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
vecbool = { "orbs" },
})
local EnemyDataFish = util.make_type({
u32 = { "enemy_id" },
f32 = { "x", "y", "vx", "vy" },
u8 = { "r" },
})
local HpData = util.make_type({
u32 = { "enemy_id" },
f32 = { "hp", "max_hp" },
})
local should_wait = {}
local first = true
local FULL_TURN = math.pi * 2
local frame = 0
local enemy_sync = {}
local unsynced_enemys = {}
local dead_entities = {}
--this basically never happens, doesn't seem that useful anymore. Perhaps should be removed to conserve memory.
--local confirmed_kills = {}
local spawned_by_us = {}
-- HACK
local times_spawned_last_minute = {}
local DISTANCE_LIMIT = 128 * 6
for filename, _ in pairs(constants.phys_sync_allowed) do
util.add_tag_to(filename, "prop_physics")
-- Idk it just causes the minecart to not appear at all.
-- util.replace_text_in(filename, 'kill_entity_after_initialized="1"', 'kill_entity_after_initialized="0"')
end
util.add_cross_call("ew_es_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
local damage = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
table.insert(dead_entities, { enemy_id, responsible, ComponentGetValue2(damage, "wait_for_kill_flag_on_death") })
end)
local function world_exists_for(entity)
local x, y = EntityGetFirstHitboxCenter(entity)
local w, h = 5, 5 -- TODO
w = w * 0.5
h = h * 0.5
return DoesWorldExistAt(x - w, y - h, x + w, y + h)
end
local function table_extend(to, from)
for _, e in ipairs(from) do
to[#to + 1] = e
end
end
local function table_extend_filtered(to, from, filter)
for _, e in ipairs(from) do
if filter(e) then
to[#to + 1] = e
end
end
end
local function get_sync_entities(return_all)
local entities = EntityGetWithTag("enemy") or {}
table_extend(entities, EntityGetWithTag("ew_enemy_sync_extra"))
table_extend(entities, EntityGetWithTag("plague_rat"))
table_extend(entities, EntityGetWithTag("seed_f"))
table_extend(entities, EntityGetWithTag("seed_e"))
table_extend(entities, EntityGetWithTag("seed_d"))
table_extend(entities, EntityGetWithTag("seed_c"))
table_extend(entities, EntityGetWithTag("perk_fungus_tiny"))
table_extend(entities, EntityGetWithTag("helpless_animal"))
table_extend_filtered(entities, EntityGetWithTag("hittable"), function(ent)
local name = EntityGetName(ent)
local file = EntityGetFilename(ent)
return name == "$item_essence_stone"
or name == "$animal_fish_giga"
or file == "data/entities/buildings/spittrap_left.xml"
or file == "data/entities/buildings/spittrap_right.xml"
or file == "data/entities/buildings/thundertrap_left.xml"
or file == "data/entities/buildings/thundertrap_right.xml"
or file == "data/entities/buildings/arrowtrap_left.xml"
or file == "data/entities/buildings/arrowtrap_right.xml"
or file == "data/entities/buildings/firetrap_left.xml"
or file == "data/entities/buildings/firetrap_right.xml"
--data/entities/buildings/statue_trap_left.xml
--data/entities/buildings/statue_trap_right.xml
end)
table_extend_filtered(entities, EntityGetWithTag("prop_physics"), function(ent)
local f = EntityGetFilename(ent)
if f ~= nil then
return constants.phys_sync_allowed[f]
end
return true
end)
local entities2 = {}
if return_all then
table_extend_filtered(entities2, entities, function(ent)
return not EntityHasTag(ent, "ew_no_enemy_sync")
end)
else
table_extend_filtered(entities2, entities, function(ent)
local x, y = EntityGetTransform(ent)
local has_anyone = EntityHasTag(ent, "worm")
or EntityGetFirstComponent(ent, "BossHealthBarComponent") ~= nil
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "ew_peer") ~= 0
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "polymorphed_player") ~= 0
return has_anyone and not EntityHasTag(ent, "ew_no_enemy_sync")
end)
end
return entities2
end
local was_held = {}
function enemy_sync.host_upload_entities()
local entities = get_sync_entities()
local enemy_data_list = {}
for i, enemy_id in ipairs(entities) do
if not world_exists_for(enemy_id) then
goto continue
end
local filename = EntityGetFilename(enemy_id)
filename = constants.interned_filename_to_index[filename] or filename
local x, y, rot = EntityGetTransform(enemy_id)
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
local vx, vy = 0, 0
if character_data ~= nil then
vx, vy = ComponentGetValue2(character_data, "mVelocity")
else
local velocity = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
if velocity ~= nil then
vx, vy = ComponentGetValue2(velocity, "mVelocity")
end
end
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
if ai_component ~= 0 and ai_component ~= nil then
ComponentSetValue2(ai_component, "max_distance_to_cam_to_start_hunting", math.pow(2, 29))
end
local phys_info = util.get_phys_info(enemy_id, true)
if phys_info == nil then
goto continue
end
local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
if has_hp then
util.ensure_component_present(enemy_id, "LuaComponent", "ew_death_notify", {
script_death = "mods/quant.ew/files/resource/cbs/death_notify.lua",
})
end
-- TODO: figure out how to sync those.
-- local laser_sight_data = nil
-- local laser_sight = EntityGetFirstComponentIncludingDisabled(enemy_id, "SpriteComponent", "laser_sight")
-- if laser_sight ~= nil and laser_sight ~= 0 then
-- -- local x, y, r =
-- end
local death_triggers = {}
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_death")
if script ~= nil and script ~= "" then
table.insert(death_triggers, constants.interned_filename_to_index[script] or script)
end
end
local en_data
local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
if EntityHasTag(enemy_id, "boss_centipede") then
local legs = {}
for _, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
local lx, ly = ComponentGetValue2(limb, "end_position")
table.insert(legs, lx)
table.insert(legs, ly)
end
en_data = EnemyDataKolmi({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
enabled = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent", "disabled_at_start") ~= nil,
legs = legs,
})
elseif EntityHasTag(enemy_id, "boss_wizard") then
local orbs = { false, false, false, false, false, false, false, false }
for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
local n = ComponentGetValue2(var, "value_int")
orbs[n] = true
end
end
en_data = EnemyDataMom({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
orbs = orbs,
})
elseif worm ~= nil then
local tx, ty = ComponentGetValue2(worm, "mTargetVec")
en_data = EnemyDataWorm({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
tx = tx,
ty = ty,
})
elseif math.abs(vx) < 0.01 and math.abs(vy) < 0.01 then
en_data = EnemyDataNoMotion({
enemy_id = enemy_id,
x = x,
y = y,
})
elseif EntityGetFirstComponentIncludingDisabled(enemy_id, "AdvancedFishAIComponent") ~= nil then
en_data = EnemyDataFish({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
r = math.floor((rot % FULL_TURN) / FULL_TURN * 255),
})
else
en_data = EnemyData({
enemy_id = enemy_id,
x = x,
y = y,
vx = vx,
vy = vy,
})
end
local wand
local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
if inv ~= nil then
local item = ComponentGetValue2(inv, "mActualActiveItem")
if item ~= nil and EntityGetIsAlive(item) then
if not EntityHasTag(item, "ew_global_item") then
item_sync.make_item_global(item)
else
wand = item_sync.get_global_item_id(item)
if wand == nil then
EntityRemoveTag(item, "ew_global_item")
goto continue
end
if not item_sync.is_my_item(wand) then
item_sync.take_authority(wand)
end
was_held[wand] = true
end
end
end
local effect_data = effect_sync.get_sync_data(enemy_id, true)
local has_laser
local animations = {}
for _, sprite in ipairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
local animation
if sprite ~= nil then
animation = ComponentGetValue2(sprite, "rect_animation")
end
table.insert(animations, animation)
if ComponentHasTag(sprite, "laser_sight") then
has_laser = true
end
end
local laser
if has_laser and EntityGetName(enemy_id) ~= "$animal_turret" then
local ai = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
if ai ~= nil then
local target = ComponentGetValue2(ai, "mGreatestPrey")
local peer = player_fns.get_player_data_by_local_entity_id(target)
if peer ~= nil then
laser = peer.peer_id
end
end
end
local dont_cull = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent") ~= nil
or worm ~= nil
or EntityHasTag(enemy_id, "seed_f")
or EntityHasTag(enemy_id, "seed_e")
or EntityHasTag(enemy_id, "seed_d")
or EntityHasTag(enemy_id, "seed_c")
or EntityGetFilename(enemy_id) == "data/entities/buildings/essence_eater.xml"
local stains = stain_sync.get_stains(enemy_id)
table.insert(
enemy_data_list,
{ filename, en_data, phys_info, wand, effect_data, animations, dont_cull, death_triggers, stains, laser }
)
::continue::
end
rpc.handle_enemy_data(enemy_data_list, first)
first = false
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
end
local function host_upload_health()
local entities = get_sync_entities()
local enemy_health_list = {}
for i, enemy_id in ipairs(entities) do
if not world_exists_for(enemy_id) then
goto continue
end
local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
if has_hp then
table.insert(
enemy_health_list,
HpData({
enemy_id = enemy_id,
hp = hp,
max_hp = max_hp,
})
)
end
::continue::
end
if #enemy_health_list > 0 then
rpc.handle_enemy_health(enemy_health_list)
end
end
function enemy_sync.client_cleanup()
local entities = get_sync_entities(true)
for _, enemy_id in ipairs(entities) do
if not EntityHasTag(enemy_id, "ew_replicated") then
EntityKill(enemy_id)
elseif not spawned_by_us[enemy_id] then
EntityKill(enemy_id)
end
end
for remote_id, enemy_data in pairs(ctx.entity_by_remote_id) do
if frame > enemy_data.frame then
EntityKill(enemy_data.id)
ctx.entity_by_remote_id[remote_id] = nil
end
end
end
function enemy_sync.on_world_update_host()
local rt = math.floor(tonumber(ModSettingGet("quant.ew.enemy_sync") or 2) or 2 + 0.5)
local n = 0
if rt == 3 then
n = 2
elseif rt == 2 then
n = 1
end
if rt == 1 or GameGetFrameNum() % rt == n then
enemy_sync.host_upload_entities()
end
if GameGetFrameNum() % 10 == 5 then
host_upload_health()
end
for wand, _ in pairs(was_held) do
if EntityGetRootEntity(wand) == wand then
was_held[wand] = nil
if item_sync.is_my_item(item_sync.get_global_item_id(wand)) then
item_sync.make_item_global(wand)
end
end
end
end
function enemy_sync.on_world_update_client()
if GameGetFrameNum() % 12 == 1 then
enemy_sync.client_cleanup()
end
if GameGetFrameNum() % (60 * 60) == 1 then
times_spawned_last_minute = {}
end
end
local kolmi_spawn
local function sync_enemy(enemy_info_raw, force_no_cull, host_fps)
local filename = enemy_info_raw[1]
filename = constants.interned_index_to_filename[filename] or filename
local en_data = enemy_info_raw[2]
local dont_cull = enemy_info_raw[7]
local death_triggers = enemy_info_raw[8]
local stains = enemy_info_raw[9]
local has_laser = enemy_info_raw[10]
local remote_enemy_id = en_data.enemy_id
local x, y = en_data.x, en_data.y
if not force_no_cull and not dont_cull then
local my_x, my_y = EntityGetTransform(ctx.my_player.entity)
if my_x == nil then
goto continue
end
local c_x, c_y = GameGetCameraPos()
local dx, dy = my_x - x, my_y - y
local cdx, cdy = c_x - x, c_y - y
if
dx * dx + dy * dy > DISTANCE_LIMIT * DISTANCE_LIMIT
and cdx * cdx + cdy * cdy > DISTANCE_LIMIT * DISTANCE_LIMIT
then
if ctx.entity_by_remote_id[remote_enemy_id] ~= nil then
EntityKill(ctx.entity_by_remote_id[remote_enemy_id].id)
ctx.entity_by_remote_id[remote_enemy_id] = nil
end
unsynced_enemys[remote_enemy_id] = enemy_info_raw
goto continue
else
unsynced_enemys[remote_enemy_id] = nil
end
else
unsynced_enemys[remote_enemy_id] = nil
end
local vx = 0
local vy = 0
if ffi.typeof(en_data) ~= EnemyDataNoMotion then
vx, vy = en_data.vx, en_data.vy
end
local phys_infos = enemy_info_raw[3]
local gid = enemy_info_raw[4]
local effects = enemy_info_raw[5]
local animation = enemy_info_raw[6]
local has_died = filename == nil
local frame_now = GameGetFrameNum()
--[[if confirmed_kills[remote_enemy_id] then
goto continue
end]]
if
ctx.entity_by_remote_id[remote_enemy_id] ~= nil
and not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
then
ctx.entity_by_remote_id[remote_enemy_id] = nil
end
if ctx.entity_by_remote_id[remote_enemy_id] == nil then
if filename == nil or filename == "" or not ModDoesFileExist(filename) then
goto continue
end
times_spawned_last_minute[remote_enemy_id] = (times_spawned_last_minute[remote_enemy_id] or 0) + 1
if times_spawned_last_minute[remote_enemy_id] > 5 then
if times_spawned_last_minute[remote_enemy_id] == 6 then
print("Entity has been spawned again more than 5 times in last minute, skipping " .. filename)
end
goto continue
end
local enemy_id
enemy_id = EntityLoad(filename, x, y)
if enemy_id == nil then
print("entity is nil " .. tostring(filename))
goto continue
end
spawned_by_us[enemy_id] = true
EntityAddTag(enemy_id, "ew_replicated")
EntityAddTag(enemy_id, "polymorphable_NOT")
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_damage_received")
if
(
script ~= nil
and (
script == "data/scripts/animals/leader_damage.lua"
or script == "data/scripts/animals/giantshooter_death.lua"
or script == "data/scripts/animals/blob_damage.lua"
)
)
or ComponentGetValue2(com, "script_source_file")
== "data/scripts/props/suspended_container_physics_objects.lua"
then
EntityRemoveComponent(enemy_id, com)
end
end
EntityAddComponent2(enemy_id, "LuaComponent", {
_tags = "ew_immortal",
script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua",
})
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
end
for _, name in ipairs({
"AnimalAIComponent",
"PhysicsAIComponent",
"CameraBoundComponent",
"AdvancedFishAIComponent",
"AIAttackComponent",
}) do
local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, name)
if ai_component ~= 0 then
EntityRemoveComponent(enemy_id, ai_component)
end
end
ctx.entity_by_remote_id[remote_enemy_id] = { id = enemy_id, frame = frame_now }
for _, phys_component in ipairs(EntityGetComponent(enemy_id, "PhysicsBody2Component") or {}) do
if phys_component ~= nil and phys_component ~= 0 then
ComponentSetValue2(phys_component, "destroy_body_if_entity_destroyed", true)
end
end
-- Make sure stuff doesn't decide to explode on clients by itself.
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_damage_percent", 0)
ComponentSetValue2(expl_component, "physics_body_modified_death_probability", 0)
ComponentSetValue2(expl_component, "explode_on_death_percent", 0)
end
local pick_up = EntityGetFirstComponentIncludingDisabled(enemy_id, "ItemPickUpperComponent")
if pick_up ~= nil then
EntityRemoveComponent(enemy_id, pick_up)
end
for _, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent", "character") or {}) do
ComponentRemoveTag(sprite, "character")
end
local ghost = EntityGetFirstComponentIncludingDisabled(enemy_id, "GhostComponent")
if ghost ~= nil then
ComponentSetValue2(ghost, "die_if_no_home", false)
end
if not EntityHasTag(enemy_id, "effectable_prop") then
util.make_ephemerial(enemy_id)
end
end
local enemy_data_new = ctx.entity_by_remote_id[remote_enemy_id]
enemy_data_new.frame = frame_now
local enemy_id = enemy_data_new.id
if not has_died then
local laser = EntityGetFirstComponentIncludingDisabled(enemy_id, "LaserEmitterComponent", "ew_laser")
if has_laser then
if laser == nil then
laser = EntityAddComponent2(enemy_id, "LaserEmitterComponent", { _tags = "ew_laser" })
ComponentObjectSetValue2(laser, "laser", "max_cell_durability_to_destroy", 0)
ComponentObjectSetValue2(laser, "laser", "damage_to_cells", 0)
ComponentObjectSetValue2(laser, "laser", "max_length", 1024)
ComponentObjectSetValue2(laser, "laser", "beam_radius", 0)
ComponentObjectSetValue2(laser, "laser", "beam_particle_chance", 80)
ComponentObjectSetValue2(laser, "laser", "beam_particle_fade", 0)
ComponentObjectSetValue2(laser, "laser", "hit_particle_chance", 0)
ComponentObjectSetValue2(laser, "laser", "audio_enabled", false)
ComponentObjectSetValue2(laser, "laser", "damage_to_entities", 0)
ComponentObjectSetValue2(laser, "laser", "beam_particle_type", 225)
end
local target = ctx.players[has_laser].entity
local lx, ly = EntityGetTransform(target)
if lx ~= nil then
local did_hit, _, _ = RaytracePlatforms(x, y, lx, ly)
ComponentSetValue2(laser, "is_emitting", not did_hit)
if not did_hit then
local dy = ly - y
local dx = lx - x
local theta = math.atan2(dy, dx)
ComponentSetValue2(laser, "laser_angle_add_rad", theta)
ComponentObjectSetValue2(laser, "laser", "max_length", math.sqrt(dx * dx + dy * dy))
end
end
elseif laser ~= nil then
ComponentSetValue2(laser, "is_emitting", false)
end
if not util.set_phys_info(enemy_id, phys_infos, host_fps) or enemy_id == kolmi_spawn then
local m = host_fps / ctx.my_player.fps
vx, vy = vx * m, vy * m
local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
if character_data ~= nil then
ComponentSetValue2(character_data, "mVelocity", vx, vy)
else
local velocity_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
if velocity_data ~= nil then
ComponentSetValue2(velocity_data, "mVelocity", vx, vy)
end
end
if ffi.typeof(en_data) == EnemyDataFish then
EntitySetTransform(enemy_id, x, y, en_data.r / 255 * FULL_TURN)
else
EntitySetTransform(enemy_id, x, y)
end
end
local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
if worm ~= nil and ffi.typeof(en_data) == EnemyDataWorm then
local tx, ty = en_data.tx, en_data.ty
ComponentSetValue2(worm, "mTargetVec", tx, ty)
end
if ffi.typeof(en_data) == EnemyDataKolmi and en_data.enabled then
if kolmi_spawn ~= enemy_id then
for _, c in ipairs(EntityGetComponentIncludingDisabled(enemy_id, "LuaComponent") or {}) do
EntityRemoveComponent(enemy_id, c)
end
kolmi_spawn = enemy_id
end
EntitySetComponentsWithTagEnabled(enemy_id, "enabled_at_start", false)
EntitySetComponentsWithTagEnabled(enemy_id, "disabled_at_start", true)
for i, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
ComponentSetValue2(limb, "end_position", en_data.legs[2 * i - 2], en_data.legs[2 * i - 1])
end
end
local indexed = {}
for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
local script = ComponentGetValue2(com, "script_death")
local has = false
for _, inx in ipairs(death_triggers) do
local script2 = constants.interned_index_to_filename[inx] or inx
if script == script2 then
has = true
indexed[script] = true
end
end
if not has then
ComponentSetValue2(com, "script_death", "")
end
end
for _, inx in ipairs(death_triggers) do
local script = constants.interned_index_to_filename[inx] or inx
if indexed[script] == nil then
EntityAddComponent(enemy_id, "LuaComponent", {
script_death = script,
execute_every_n_frame = "-1",
})
end
end
if ffi.typeof(en_data) == EnemyDataMom then
local orbs = en_data.orbs
for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
local damage_component = EntityGetFirstComponentIncludingDisabled(child, "DamageModelComponent")
if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
local n = ComponentGetValue2(var, "value_int")
if orbs[n - 1] then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
else
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
EntityKill(child)
end
end
end
end
effect_sync.apply_effects(effects, enemy_id, true)
if stains ~= nil then
stain_sync.sync_stains(stains, enemy_id)
end
end
local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
local item
if inv ~= nil then
item = ComponentGetValue2(inv, "mActualActiveItem")
end
if gid ~= nil and (item == nil or item == 0 or not EntityGetIsAlive(item)) then
local wand = item_sync.find_by_gid(gid)
if wand ~= nil and EntityGetIsAlive(wand) then
EntityAddTag(wand, "ew_client_item")
local inventory
for _, child in pairs(EntityGetAllChildren(enemy_id) or {}) do
if EntityGetName(child) == "inventory_quick" then
inventory = child
end
end
if inventory == nil then
inventory = EntityCreateNew("inventory_quick")
EntityAddChild(enemy_id, inventory)
end
if EntityGetParent(wand) ~= inventory then
if EntityGetParent(wand) ~= 0 then
EntityRemoveFromParent(wand)
end
EntityAddChild(inventory, wand)
end
np.SetActiveHeldEntity(enemy_id, wand, false, false)
elseif should_wait[gid] == nil or should_wait[gid] < GameGetFrameNum() then
item_sync.rpc.request_send_again(gid)
should_wait[gid] = GameGetFrameNum() + 15
end
end
for i, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
if animation[i] ~= nil then
ComponentSetValue2(sprite, "rect_animation", animation[i])
ComponentSetValue2(sprite, "next_rect_animation", animation[i])
end
end
::continue::
end
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
--[[if confirmed_kills[remote_id] then
GamePrint("Remote id has been killed already..?")
goto continue
end
confirmed_kills[remote_id] = true]]
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
if unsynced_enemys[remote_id] ~= nil then
sync_enemy(unsynced_enemys[remote_id], true, 60)
end
local enemy_data = ctx.entity_by_remote_id[remote_id]
if enemy_data ~= nil and EntityGetIsAlive(enemy_data.id) then
local enemy_id = enemy_data.id
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
ComponentSetValue2(damage_component, "ui_report_damage", false)
ComponentSetValue2(damage_component, "hp", 2 ^ -38)
end
-- Enable explosion back
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg + 0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
if not remote_data[3] then
EntityKill(enemy_id)
else
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
ComponentSetValue2(damage_component, "kill_now", true)
end
ctx.entity_by_remote_id[remote_id] = nil
end
::continue::
end
end
function rpc.handle_enemy_data(enemy_data, is_first)
if is_first then
for _, n in pairs(ctx.entity_by_remote_id) do
EntityKill(n.id)
end
ctx.entity_by_remote_id = {}
end
frame = GameGetFrameNum()
for _, enemy_info_raw in ipairs(enemy_data) do
sync_enemy(enemy_info_raw, false, ctx.rpc_player_data.fps)
end
end
function rpc.handle_enemy_health(enemy_health_data)
for _, en_data in ipairs(enemy_health_data) do
local remote_enemy_id = en_data.enemy_id
local hp = en_data.hp
local max_hp = en_data.max_hp
if
ctx.entity_by_remote_id[remote_enemy_id] == nil
or not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
then
goto continue
end
local enemy_data = ctx.entity_by_remote_id[remote_enemy_id]
local enemy_id = enemy_data.id
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp - hp
if dmg > 0 then
-- Make sure the enemy doesn't die from the next EntityInflictDamage.
if EntityGetName(enemy_id) ~= "$animal_boss_sky" then
util.set_ent_health(enemy_id, { dmg * 2, dmg * 2 })
else
util.set_ent_health(enemy_id, { hp + dmg, max_hp })
end
-- Deal damage, so that game displays damage numbers.
EntityInflictDamage(enemy_id, dmg, "DAMAGE_CURSE", "", "NONE", 0, 0, GameGetWorldStateEntity())
end
util.set_ent_health(enemy_id, { hp, max_hp })
::continue::
end
end
function enemy_sync.on_projectile_fired(
shooter_id,
projectile_id,
initial_rng,
position_x,
position_y,
target_x,
target_y,
send_message,
unknown1,
multicast_index,
unknown3
)
local not_a_player = not EntityHasTag(shooter_id, "ew_no_enemy_sync")
and not EntityHasTag(shooter_id, "player_unit")
and not EntityHasTag(shooter_id, "ew_client")
if not_a_player and ctx.is_host then
local projectileComponent = EntityGetFirstComponentIncludingDisabled(projectile_id, "ProjectileComponent")
if projectileComponent ~= nil then
local entity_that_shot = ComponentGetValue2(projectileComponent, "mEntityThatShot")
if entity_that_shot == 0 then
rpc.replicate_projectile(
util.serialize_entity(projectile_id),
position_x,
position_y,
target_x,
target_y,
shooter_id,
initial_rng
)
end
end
end
end
rpc.opts_reliable()
function rpc.replicate_projectile(seri_ent, position_x, position_y, target_x, target_y, remote_source_ent, rng)
if rng ~= nil then
np.SetProjectileSpreadRNG(rng)
end
if ctx.entity_by_remote_id[remote_source_ent] == nil then
return
end
local source_ent = ctx.entity_by_remote_id[remote_source_ent].id
local ent = util.deserialize_entity(seri_ent)
GameShootProjectile(source_ent, position_x, position_y, target_x, target_y, ent)
end
return enemy_sync

View file

@ -25,13 +25,8 @@ end
function module.on_world_initialized()
initial_world_state_entity = GameGetWorldStateEntity()
ewext.on_world_initialized()
local grid_world = world_ffi.get_grid_world()
local chunk_map = grid_world.vtable.get_chunk_map(grid_world)
grid_world = tonumber(ffi.cast("intptr_t", grid_world))
chunk_map = tonumber(ffi.cast("intptr_t", chunk_map))
local material_list = tonumber(ffi.cast("intptr_t", world_ffi.get_material_ptr(0)))
ewext.init_particle_world_state(grid_world, chunk_map, material_list)
ewext.on_world_initialized(ctx.proxy_opt.world_num)
ewext.init_particle_world_state()
ewext.module_on_world_init()
log = ModSettingGet("quant.ew.log_performance") or false
ewext.set_log(log)

View file

@ -1,865 +0,0 @@
-- Synchronizes item pickup and item drop
ModLuaFileAppend("data/scripts/items/utility_box.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
ModLuaFileAppend("data/scripts/items/chest_random_super.lua", "mods/quant.ew/files/resource/cbs/chest_sync.lua")
dofile_once("data/scripts/lib/coroutines.lua")
local rpc = net.new_rpc_namespace()
local item_sync = {}
local pending_remove = {}
local pickup_handlers = {}
local dead_entities = {}
local frame = {}
local gid_last_frame_updated = {}
local wait_on_send = {}
local wait_for_gid = {}
function rpc.open_chest(gid)
if wait_for_gid[gid] == nil or wait_for_gid[gid] < 10000 then
wait_for_gid[gid] = GameGetFrameNum() + 36000
wait_on_send[gid] = GameGetFrameNum() + 36000
local ent = item_sync.find_by_gid(gid)
if ent ~= nil then
local file
local name = EntityGetFilename(ent)
if name == "data/entities/items/pickup/utility_box.xml" then
file = "data/scripts/items/utility_box.lua"
elseif name == "data/entities/items/pickup/chest_random_super.xml" then
file = "data/scripts/items/chest_random_super.lua"
elseif name == "data/entities/items/pickup/chest_random.xml" then
file = "data/scripts/items/chest_random.lua"
end
if file ~= nil then
EntityAddComponent2(ent, "LuaComponent", {
script_source_file = file,
execute_on_added = true,
call_init_function = true,
})
end
end
end
end
util.add_cross_call("ew_chest_opened", function(chest_id)
local gid = item_sync.get_global_item_id(chest_id)
if gid ~= nil then
wait_for_gid[gid] = GameGetFrameNum() + 36000
wait_on_send[gid] = GameGetFrameNum() + 36000
rpc.open_chest(gid)
end
end)
util.add_cross_call("ew_item_death_notify", function(enemy_id, responsible_id)
local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
local responsible
if player_data ~= nil then
responsible = player_data.peer_id
else
responsible = responsible_id
end
local gid = item_sync.get_global_item_id(enemy_id)
if gid ~= nil then
table.insert(dead_entities, { gid, responsible })
end
end)
function item_sync.ensure_notify_component(ent)
local notify = EntityGetFirstComponentIncludingDisabled(ent, "LuaComponent", "ew_notify_component")
if notify == nil then
EntityAddComponent2(ent, "LuaComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_notify_component,ew_remove_on_send",
script_throw_item = "mods/quant.ew/files/resource/cbs/item_notify.lua",
script_item_picked_up = "mods/quant.ew/files/resource/cbs/item_notify.lua",
})
end
end
local function mark_in_inventory(my_player)
local items = inventory_helper.get_all_inventory_items(my_player)
for _, ent in pairs(items) do
if not EntityHasTag(ent, "polymorphed_player") then
item_sync.ensure_notify_component(ent)
end
end
end
local function allocate_global_id()
local current = tonumber(GlobalsGetValue("ew_global_item_id", "1"))
GlobalsSetValue("ew_global_item_id", tostring(current + 1))
return ctx.my_id .. ":" .. current
end
-- Try to guess if the item is in world.
local function is_item_on_ground(item)
return EntityGetRootEntity(item) == item
end
function item_sync.get_global_item_id(item)
local gid = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid == nil then
return nil
end
local ret = ComponentGetValue2(gid, "value_string")
return ret
end
local function is_wand(ent)
if ent == nil or ent == 0 then
return false
end
local ability = EntityGetFirstComponentIncludingDisabled(ent, "AbilityComponent")
if ability == nil then
return false
end
return ComponentGetValue2(ability, "use_gun_script") == true
end
local function is_safe_to_remove()
return not ctx.is_wand_pickup
end
function item_sync.remove_item_with_id(gid)
local item_ent_id = item_sync.find_by_gid(gid)
if is_safe_to_remove() or not is_wand(item_ent_id) then
item_sync.remove_item_with_id_now(gid)
else
table.insert(pending_remove, gid)
EntitySetTransform(item_ent_id, 0, 0)
util.make_ephemerial(item_ent_id)
end
end
local find_by_gid_cache = {}
function item_sync.find_by_gid(gid)
if find_by_gid_cache[gid] ~= nil then
if
EntityGetIsAlive(find_by_gid_cache[gid])
and EntityHasTag(find_by_gid_cache[gid], "ew_global_item")
and is_item_on_ground(find_by_gid_cache[gid])
then
return find_by_gid_cache[gid]
else
find_by_gid_cache[gid] = nil
end
end
--print("find_by_gid: searching")
local candidate
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local i_gid = item_sync.get_global_item_id(item)
if i_gid ~= nil then
find_by_gid_cache[i_gid] = item
if i_gid == gid then
if is_item_on_ground(item) then
return item
else
candidate = item
end
end
end
end
return candidate
end
function item_sync.remove_item_with_id_now(gid)
local item = item_sync.find_by_gid(gid)
if item ~= nil then
find_by_gid_cache[gid] = nil
for _, audio in ipairs(EntityGetComponent(item, "AudioComponent") or {}) do
if string.sub(ComponentGetValue2(audio, "event_root"), 1, 10) == "collision/" then
EntitySetComponentIsEnabled(item, audio, false)
end
end
EntityKill(item)
return item
end
end
function item_sync.host_localize_item(gid, peer_id)
if ctx.item_prevent_localize[gid] then
print("Item localize for " .. gid .. " prevented")
return
end
ctx.item_prevent_localize[gid] = true
if table.contains(pending_remove, gid) then
print("Item localize prevented, already taken")
return
end
local item_ent_id = item_sync.find_by_gid(gid)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
if peer_id ~= ctx.my_id then
item_sync.remove_item_with_id(gid)
end
rpc.item_localize(peer_id, gid)
if peer_id == ctx.my_id then
item_sync.take_authority(gid)
else
rpc.hand_authority_over_to(peer_id, gid)
end
end
local function make_global(item, give_authority_to)
if not EntityGetIsAlive(item) then
print("Thrown item vanished before we could send it")
return
end
item_sync.ensure_notify_component(item)
local gid_component =
EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
local gid
if gid_component == nil then
gid = allocate_global_id()
if give_authority_to ~= nil then
gid = give_authority_to .. ":" .. gid
end
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "enabled_in_world,enabled_in_hand,enabled_in_inventory,ew_global_item_id",
value_string = gid,
})
else
gid = ComponentGetValue2(gid_component, "value_string")
end
--local vel = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
--if vel then
-- local vx, vy = ComponentGetValue2(vel, "mVelocity")
--end
local item_data = inventory_helper.serialize_single_item(item)
item_data.gid = gid
local _, _, has_hp = util.get_ent_health(item)
if has_hp then
util.ensure_component_present(item, "LuaComponent", "ew_item_death_notify", {
script_death = "mods/quant.ew/files/resource/cbs/item_death_notify.lua",
})
end
ctx.item_prevent_localize[gid] = false
rpc.item_globalize(item_data)
if wait_on_send[gid] ~= nil then
wait_on_send[gid] = GameGetFrameNum() + 30
end
end
function item_sync.make_item_global(item, instant, give_authority_to)
EntityAddTag(item, "ew_global_item")
if instant then
make_global(item, give_authority_to)
else
async(function()
wait(1) -- Wait 1 frame so that game sets proper velocity.
make_global(item, give_authority_to)
end)
end
end
local function remove_client_items_from_world()
if GameGetFrameNum() % 5 ~= 3 then
return
end
for _, item in ipairs(EntityGetWithTag("ew_client_item")) do
if is_item_on_ground(item) then
item_sync.remove_item_with_id(item_sync.get_global_item_id(item))
end
end
end
local function is_peers_item(gid, peer)
if gid == nil then
return false
end
return string.sub(gid, 1, 16) == peer
end
function item_sync.is_my_item(gid)
if gid == nil then
return false
end
return string.sub(gid, 1, 16) == ctx.my_id
end
function item_sync.take_authority(gid)
if gid ~= nil and not item_sync.is_my_item(gid) then
local new_id = allocate_global_id()
rpc.give_authority_to(gid, new_id)
end
end
rpc.opts_everywhere()
rpc.opts_reliable()
function rpc.give_authority_to(gid, new_id)
local item
local to_remove = {}
for _, ent in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local i_gid = item_sync.get_global_item_id(ent)
if i_gid == gid then
if item == nil then
item = ent
else
table.insert(to_remove, gid)
end
end
end
find_by_gid_cache[gid] = nil
if table.contains(pending_remove, gid) then
for i, id in ipairs(pending_remove) do
if id == gid then
table.remove(pending_remove, i)
break
end
end
table.insert(pending_remove, new_id)
end
for _, g in ipairs(to_remove) do
item_sync.remove_item_with_id(g)
end
if item ~= nil then
find_by_gid_cache[new_id] = item
local var = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
ComponentSetValue2(var, "value_string", new_id)
end
end
rpc.opts_reliable()
function rpc.hand_authority_over_to(peer_id, gid)
if peer_id == ctx.my_id then
if item_sync.find_by_gid(gid) ~= nil then
item_sync.take_authority(gid)
elseif wait_for_gid[gid] == nil then
rpc.request_send_again(gid)
wait_for_gid[gid] = GameGetFrameNum() + 300
end
end
end
rpc.opts_reliable()
function rpc.handle_death_data(death_data)
for _, remote_data in ipairs(death_data) do
local remote_id = remote_data[1]
local responsible_entity = 0
local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
if peer_data ~= nil then
responsible_entity = peer_data.entity
elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
end
local enemy_id = item_sync.find_by_gid(remote_id)
if enemy_id ~= nil and EntityGetIsAlive(enemy_id) then
local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
if immortal ~= 0 then
EntityRemoveComponent(enemy_id, immortal)
end
local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
if protection_component_id ~= 0 then
EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
end
local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
ComponentSetValue2(damage_component, "ui_report_damage", false)
ComponentSetValue2(damage_component, "hp", 2 ^ -38)
end
-- Enable explosion back
local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
if expl_component ~= nil and expl_component ~= 0 then
ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
end
local current_hp = util.get_ent_health(enemy_id)
local dmg = current_hp
if dmg > 0 then
EntityInflictDamage(enemy_id, dmg + 0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
end
EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
EntityKill(enemy_id)
end
::continue::
end
end
local DISTANCE_LIMIT = 128 * 4
local ignore = {}
local function send_item_positions(all)
local position_data = {}
local cx, cy = GameGetCameraPos()
local cap = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item")) do
local gid = item_sync.get_global_item_id(item)
-- Only send info about items created by us.
local tg = EntityHasTag(item, "ew_no_spawn")
if gid ~= nil and item_sync.is_my_item(gid) and (is_item_on_ground(item) or tg) then
local x, y = EntityGetTransform(item)
local dx, dy = x - cx, y - cy
if
not tg
and (ignore[gid] == nil or ignore[gid] < GameGetFrameNum())
and dx * dx + dy * dy > 4 * DISTANCE_LIMIT * DISTANCE_LIMIT
then
local ent = EntityGetClosestWithTag(x, y, "ew_peer")
local nx, ny
local ndx, ndy
if ent ~= 0 then
nx, ny = EntityGetTransform(ent)
ndx, ndy = x - nx, y - ny
end
if ent == 0 or ndx * ndx + ndy * ndy > DISTANCE_LIMIT * DISTANCE_LIMIT then
ent = EntityGetClosestWithTag(x, y, "polymorphed_player")
if ent ~= 0 then
nx, ny = EntityGetTransform(ent)
ndx, ndy = x - nx, y - ny
end
if ent == 0 or ndx * ndx + ndy * ndy > DISTANCE_LIMIT * DISTANCE_LIMIT then
ignore[gid] = GameGetFrameNum() + 60
goto continue
end
end
local data = player_fns.get_player_data_by_local_entity_id(ent)
if data ~= nil then
local peer = data.peer_id
rpc.hand_authority_over_to(peer, gid)
ignore[gid] = nil
else
ignore[gid] = GameGetFrameNum() + 60
end
else
local phys_info = util.get_phys_info(item, true)
if
tg
or (
(phys_info[1][1] ~= nil or phys_info[2][1] ~= nil or all)
and (
#EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "ew_peer") ~= 0
or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "polymorphed_player") ~= 0
)
)
then
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
local cost = 0
if costcom ~= nil then
cost = ComponentGetValue2(costcom, "cost")
local vel = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if math.abs(cx - x) < DISTANCE_LIMIT and math.abs(cy - y) < DISTANCE_LIMIT then
if
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_stealable"
) ~= nil
then
ComponentSetValue2(costcom, "stealable", true)
ComponentSetValue2(vel, "gravity_y", 400)
elseif
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_float"
) ~= nil
then
ComponentSetValue2(vel, "gravity_y", 400)
end
else
if
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_stealable"
) ~= nil
then
ComponentSetValue2(costcom, "stealable", false)
ComponentSetValue2(vel, "gravity_y", 0)
elseif
EntityGetFirstComponentIncludingDisabled(
item,
"VariableStorageComponent",
"ew_try_float"
) ~= nil
then
ComponentSetValue2(vel, "gravity_y", 0)
end
end
end
position_data[gid] = { x, y, phys_info, cost }
if EntityHasTag(item, "egg_item") then
if
EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_egg") ~= nil
then
position_data[gid][5] = true
end
elseif tg then
local f = EntityGetFilename(item)
if cap[f] == nil then
cap[f] = tonumber(ModSettingGet("quant.ew.rocks") or 16) or 16
end
if cap[f] == 0 then
position_data[gid] = nil
goto continue
end
cap[f] = cap[f] - 1
position_data[gid][5] = false
local velocity = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if velocity ~= nil then
local vx, vy = ComponentGetValue2(velocity, "mVelocity")
position_data[gid][6] = { vx, vy }
end
end
end
end
end
::continue::
end
rpc.update_positions(position_data, all)
if #dead_entities > 0 then
rpc.handle_death_data(dead_entities)
end
dead_entities = {}
end
util.add_cross_call("ew_thrown", function(thrown_item)
if
thrown_item ~= nil
and (item_sync.get_global_item_id(thrown_item) == nil or item_sync.is_my_item(
item_sync.get_global_item_id(thrown_item)
))
and EntityGetFirstComponentIncludingDisabled(thrown_item, "VariableStorageComponent", "ew_egg") == nil
then
item_sync.make_item_global(thrown_item)
end
end)
util.add_cross_call("ew_picked", function(picked_item)
if picked_item ~= nil and EntityHasTag(picked_item, "ew_global_item") then
local gid = item_sync.get_global_item_id(picked_item)
if gid ~= nil then
if ctx.is_host then
item_sync.host_localize_item(gid, ctx.my_id)
else
rpc.item_localize_req(gid)
end
end
end
end)
function item_sync.on_world_update()
-- TODO check that we not removing item we are going to pick now, instead of checking if picker gui is open.
if is_safe_to_remove() then
if #pending_remove > 0 then
local gid = table.remove(pending_remove)
item_sync.remove_item_with_id_now(gid)
end
end
if GameGetFrameNum() % 120 == 35 then
for _, ent in ipairs(EntityGetWithTag("mimic_potion")) do
if not EntityHasTag(ent, "polymorphed_player") and is_item_on_ground(ent) then
if not EntityHasTag(ent, "ew_global_item") then
if ctx.is_host then
item_sync.make_item_global(ent)
else
EntityKill(ent)
end
end
end
end
for _, wand in ipairs(EntityGetWithTag("wand")) do
local com = EntityGetFirstComponentIncludingDisabled(wand, "ItemComponent")
if com ~= nil then
ComponentSetValue2(com, "item_pickup_radius", 256)
end
end
end
local rt = math.floor(tonumber(ModSettingGet("quant.ew.item_sync") or 4) or 4 + 0.5)
local n = 0
if rt == 5 then
n = 3
elseif rt == 3 then
n = 1
elseif rt == 4 then
n = 2
end
if GameGetFrameNum() % 60 == 3 then
send_item_positions(true)
elseif rt == 1 or GameGetFrameNum() % rt == n then
send_item_positions(false)
end
if GameGetFrameNum() % 30 == 23 then
for gid, num in pairs(wait_for_gid) do
if num < GameGetFrameNum() then
wait_for_gid[gid] = nil
end
end
end
if GameGetFrameNum() % 5 == 4 then
mark_in_inventory(ctx.my_player)
end
remove_client_items_from_world()
end
function item_sync.on_should_send_updates()
if not ctx.is_host then
return
end
local item_list = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
if is_item_on_ground(item) and not EntityHasTag(item, "mimic_potion") then
local item_data = inventory_helper.serialize_single_item(item)
local gid = item_sync.get_global_item_id(item)
if gid ~= nil then
item_data.gid = gid
table.insert(item_list, item_data)
end
end
end
rpc.initial_items(item_list)
end
function item_sync.on_draw_debug_window(imgui)
local mx, my = DEBUG_GetMouseWorld()
local ent = EntityGetClosestWithTag(mx, my, "ew_global_item")
if ent ~= nil and ent ~= 0 then
if imgui.CollapsingHeader("Item gid") then
local x, y = EntityGetTransform(ent)
GameCreateSpriteForXFrames("mods/quant.ew/files/resource/debug/marker.png", x, y, true, 0, 0, 1, true)
local gid = item_sync.get_global_item_id(ent)
imgui.Text("GID: " .. tostring(gid))
local prevented = ctx.item_prevent_localize[gid]
if prevented then
imgui.Text("Localize prevented")
else
imgui.Text("Localize allowed")
end
local on_ground, reason = is_item_on_ground(ent)
if on_ground then
imgui.Text("On ground: " .. reason)
else
imgui.Text("Not on ground: " .. reason)
end
end
end
end
local function add_stuff_to_globalized_item(item, gid)
EntityAddTag(item, "ew_global_item")
item_sync.ensure_notify_component(item)
local gid_c = EntityGetFirstComponentIncludingDisabled(item, "VariableStorageComponent", "ew_global_item_id")
if gid_c == nil then
EntityAddComponent2(item, "VariableStorageComponent", {
_tags = "ew_global_item_id",
value_string = gid,
})
else
ComponentSetValue2(gid_c, "value_string", gid)
end
ctx.item_prevent_localize[gid] = false
end
rpc.opts_reliable()
function rpc.initial_items(item_list)
-- Only run once ever, as it tends to duplicate items otherwise
if GameHasFlagRun("ew_initial_items") then
return
end
GameAddFlagRun("ew_initial_items")
for _, item_data in ipairs(item_list) do
local item = item_sync.find_by_gid(item_data.gid)
if item == nil then
local item_new = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item_new, item_data.gid)
end
end
end
rpc.opts_reliable()
function rpc.item_globalize(item_data)
if wait_for_gid[item_data.gid] ~= nil then
if wait_for_gid[item_data.gid] > GameGetFrameNum() + 10000 then
return
end
wait_for_gid[item_data.gid] = GameGetFrameNum() + 30
end
local a = item_sync.find_by_gid(item_data.gid)
if is_safe_to_remove() or not is_wand(a) then
local k = item_sync.remove_item_with_id_now(item_data.gid)
local n = item_sync.find_by_gid(item_data.gid)
if n ~= nil and k ~= n then
return
end
else
local n = item_sync.find_by_gid(item_data.gid)
if n ~= nil then
return
end
end
local item = inventory_helper.deserialize_single_item(item_data)
add_stuff_to_globalized_item(item, item_data.gid)
for _, com in ipairs(EntityGetComponent(item, "VariableStorageComponent") or {}) do
if ComponentGetValue2(com, "name") == "throw_time" then
ComponentSetValue2(com, "value_int", GameGetFrameNum())
end
end
local damage_component = EntityGetFirstComponentIncludingDisabled(item, "DamageModelComponent")
if damage_component and damage_component ~= 0 then
ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
EntityAddComponent2(item, "LuaComponent", {
_tags = "ew_immortal",
script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua",
})
end
end
rpc.opts_reliable()
function rpc.item_localize(l_peer_id, item_id)
local item_ent_id = item_sync.find_by_gid(item_id)
if item_ent_id ~= nil then
for _, handler in ipairs(pickup_handlers) do
handler(item_ent_id)
end
end
if l_peer_id ~= ctx.my_id then
item_sync.remove_item_with_id(item_id)
end
end
rpc.opts_reliable()
function rpc.item_localize_req(gid)
if not ctx.is_host then
return
end
item_sync.host_localize_item(gid, ctx.rpc_peer_id)
end
local function cleanup(peer)
for gid, num in pairs(gid_last_frame_updated[peer]) do
if frame[peer] > num then
local item = item_sync.find_by_gid(gid)
if is_item_on_ground(item) then
item_sync.remove_item_with_id(gid)
gid_last_frame_updated[peer][gid] = nil
end
end
end
local is_duplicate = {}
for _, item in ipairs(EntityGetWithTag("ew_global_item") or {}) do
local gid = item_sync.get_global_item_id(item)
if gid ~= nil and is_peers_item(gid, peer) then
if is_duplicate[gid] then
item_sync.remove_item_with_id(gid)
else
is_duplicate[gid] = true
end
end
end
end
function rpc.kill_egg(gid)
item_sync.remove_item_with_id_now(gid)
end
function rpc.update_positions(position_data, all)
if frame[ctx.rpc_peer_id] == nil or all then
frame[ctx.rpc_peer_id] = GameGetFrameNum()
if gid_last_frame_updated[ctx.rpc_peer_id] == nil then
gid_last_frame_updated[ctx.rpc_peer_id] = {}
end
end
local cx, cy = GameGetCameraPos()
for gid, el in pairs(position_data) do
if table.contains(pending_remove, gid) then
goto continue
end
local x, y = el[1], el[2]
local name = EntityGetFilename(item)
local is_chest = name == "data/entities/items/pickup/utility_box.xml"
or name == "data/entities/items/pickup/chest_random_super.xml"
or name == "data/entities/items/pickup/chest_random.xml"
if is_chest or el[5] ~= nil or (math.abs(x - cx) < DISTANCE_LIMIT and math.abs(y - cy) < DISTANCE_LIMIT) then
if el[5] == nil then
gid_last_frame_updated[ctx.rpc_peer_id][gid] = frame[ctx.rpc_peer_id]
end
local phys_info = el[3]
local price = el[4]
local item = item_sync.find_by_gid(gid)
if item ~= nil then
if not util.set_phys_info(item, phys_info, ctx.rpc_player_data.fps) then
EntitySetTransform(item, x, y)
if el[6] ~= nil then
local vx, vy = el[6][1], el[6][2]
local velocity = EntityGetFirstComponentIncludingDisabled(item, "VelocityComponent")
if velocity ~= nil then
ComponentSetValue2(velocity, "mVelocity", vx, vy)
end
end
end
local costcom = EntityGetFirstComponentIncludingDisabled(item, "ItemCostComponent")
if costcom ~= nil then
if price == 0 then
EntitySetComponentsWithTagEnabled(item, "shop_cost", false)
ComponentSetValue2(costcom, "cost", 0)
else
EntitySetComponentsWithTagEnabled(item, "shop_cost", true)
ComponentSetValue2(costcom, "cost", price)
end
end
elseif wait_for_gid[gid] == nil then
if el[5] == true then
rpc.kill_egg(gid)
elseif el[5] ~= false then
util.log("Requesting again " .. gid)
rpc.request_send_again(gid)
wait_for_gid[gid] = GameGetFrameNum() + 300
end
end
end
::continue::
end
if all then
cleanup(ctx.rpc_peer_id)
end
end
function rpc.request_send_again(gid)
if gid ~= nil and not item_sync.is_my_item(gid) then
return
end
local item = item_sync.find_by_gid(gid)
if item == nil then
util.log("Requested to send item again, but this item wasn't found: " .. gid)
return
end
if wait_on_send[gid] == nil or wait_on_send[gid] < GameGetFrameNum() then
wait_on_send[gid] = GameGetFrameNum() + 240
item_sync.make_item_global(item)
end
end
ctx.cap.item_sync = {
globalize = item_sync.make_item_global,
register_pickup_handler = function(handler)
table.insert(pickup_handlers, handler)
end,
}
item_sync.rpc = rpc
return item_sync

View file

@ -1,318 +0,0 @@
--- World read / write functionality.
---@module 'noitapatcher.nsew.world'
local world = {}
local ffi = require("ffi")
local world_ffi = require("noitapatcher.nsew.world_ffi")
print("get_cell: " .. tostring(world_ffi.get_cell))
local C = ffi.C
ffi.cdef([[
enum ENCODE_CONST {
// Maximum amount of runs 128*128 pixels can result in, plus one just in case.
PIXEL_RUN_MAX = 16385,
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.last_material_id = 0
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
]]
--- 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.
-- @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(start_x_ini, start_y_ini, end_x_ini, end_y_ini, encoded_area)
local start_x = ffi.cast("int32_t", start_x_ini)
local start_y = ffi.cast("int32_t", start_y_ini)
local end_x = ffi.cast("int32_t", end_x_ini)
local end_y = ffi.cast("int32_t", end_y_ini)
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 > 128 or height > 128 then
print("Invalid world part, dimension greater than 128")
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
encoded_area.header.pixel_run_count = ewext.encode_area(
start_x_ini,
start_y_ini,
end_x_ini,
end_y_ini,
tonumber(ffi.cast("intptr_t", encoded_area.pixel_runs))
)
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 new_material == -1 then
goto next_pixel
end
if ppixel[0] ~= nil then
local pixel = ppixel[0]
local cell_type = pixel.vtable.get_cell_type(pixel)
if cell_type == C.CELL_TYPE_SOLID then
goto next_pixel
end
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
if new_material > world.last_material_id then
goto next_pixel
end
local mat_ptr = world_ffi.get_material_ptr(new_material)
if mat_ptr == nil then
goto next_pixel
end
local pixel = world_ffi.construct_cell(grid_world, x, y, mat_ptr, nil)
if pixel == nil then
-- TODO: This can happen when the material texture has a
-- transparent pixel at the given coordinate. There's
-- probably a better way to deal with this, but for now
-- we skip positions like this.
goto next_pixel
end
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
::next_pixel::
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

@ -1,226 +0,0 @@
local world_ffi = require("noitapatcher.nsew.world_ffi")
local world = dofile_once("mods/quant.ew/files/system/world_sync/world.lua")
local rect = require("noitapatcher.nsew.rect")
local ffi = require("ffi")
-- local rpc = net.new_rpc_namespace()
--local rect_optimiser = rect.Optimiser_new()
local encoded_area = world.EncodedArea()
local world_sync = {}
local KEY_WORLD_FRAME = 0
local KEY_WORLD_END = 1
local CHUNK_SIZE = 128
local iter_fast = 0
local iter_slow = 0
local iter_slow_2 = 0
--[[local function do_benchmark()
local world_ffi = require("noitapatcher.nsew.world_ffi")
local grid_world = world_ffi.get_grid_world()
local chunk_map = grid_world.vtable.get_chunk_map(grid_world)
local start = GameGetRealWorldTimeSinceStarted()
local iters = 10000
for i=1, iters do
world.encode_area(chunk_map, 0, 0, 128, 128, encode_area)
-- world_ffi.get_cell(chunk_map, 0, 0)
end
local end_time = GameGetRealWorldTimeSinceStarted()
local elapsed = (end_time - start) * 1000 * 1000 * 1000 / (iters * 128 * 128)
print("Benchmark:", elapsed, "ns/pixel")
end]]
function world_sync.on_world_initialized()
local c = 0
while true do
local name = CellFactory_GetName(c)
if name == "unknown" then
break
end
c = c + 1
end
c = c - 1
print("Last material id: " .. c)
world.last_material_id = c
-- do_benchmark()
end
local function send_chunks(cx, cy)
local chx, chy = cx * CHUNK_SIZE, cy * CHUNK_SIZE
local crect = rect.Rectangle(chx, chy, chx + CHUNK_SIZE, chy + CHUNK_SIZE)
if DoesWorldExistAt(crect.left, crect.top, crect.right, crect.bottom) then
local area = world.encode_area(crect.left, crect.top, crect.right, crect.bottom, encoded_area)
if area ~= nil then
--if ctx.proxy_opt.debug then
-- GameCreateSpriteForXFrames("mods/quant.ew/files/resource/debug/box_128x128.png", crect.left+64, crect.top + 64, true, 0, 0, 11, true)
--end
local str = ffi.string(area, world.encoded_size(area))
net.proxy_bin_send(KEY_WORLD_FRAME, str)
end
end
end
local int = 4 -- ctx.proxy_opt.world_sync_interval
local function get_all_chunks(ocx, ocy, pos_data, priority, give_0)
--local grid_world = world_ffi.get_grid_world()
--local chunk_map = grid_world.vtable.get_chunk_map(grid_world)
--local thread_impl = grid_world.mThreadImpl
if GameGetFrameNum() % int == 0 then
send_chunks(ocx, ocy)
local pri = priority
if give_0 then
pri = 0
end
net.proxy_bin_send(KEY_WORLD_END, string.char(pri) .. pos_data)
elseif GameGetFrameNum() % int == 2 then
if iter_fast == 0 then
send_chunks(ocx + 1, ocy)
send_chunks(ocx + 1, ocy + 1)
elseif iter_fast == 1 then
send_chunks(ocx, ocy + 1)
send_chunks(ocx - 1, ocy + 1)
elseif iter_fast == 2 then
send_chunks(ocx - 1, ocy)
send_chunks(ocx - 1, ocy - 1)
else
send_chunks(ocx, ocy - 1)
send_chunks(ocx + 1, ocy - 1)
end
net.proxy_bin_send(KEY_WORLD_END, string.char(math.min(priority + 1, 16)) .. pos_data)
iter_fast = iter_fast + 1
if iter_fast == 4 then
iter_fast = 0
end
elseif GameGetFrameNum() % (int * 4) == 3 then
if iter_slow == 0 then
send_chunks(ocx + 2, ocy - 1)
send_chunks(ocx + 2, ocy)
send_chunks(ocx + 2, ocy + 1)
send_chunks(ocx + 2, ocy + 2)
elseif iter_slow == 1 then
send_chunks(ocx + 1, ocy + 2)
send_chunks(ocx, ocy + 2)
send_chunks(ocx - 1, ocy + 2)
send_chunks(ocx - 2, ocy + 2)
elseif iter_slow == 2 then
send_chunks(ocx - 2, ocy + 1)
send_chunks(ocx - 2, ocy)
send_chunks(ocx - 2, ocy - 1)
send_chunks(ocx - 2, ocy - 2)
else
send_chunks(ocx - 1, ocy - 2)
send_chunks(ocx, ocy - 2)
send_chunks(ocx + 1, ocy - 2)
send_chunks(ocx + 2, ocy - 2)
end
net.proxy_bin_send(KEY_WORLD_END, string.char(math.min(priority + 2, 16)) .. pos_data)
iter_slow = iter_slow + 1
if iter_slow == 4 then
iter_slow = 0
end
elseif (priority == 0 and not GameHasFlagRun("ending_game_completed")) and GameGetFrameNum() % (int * 3) == 1 then
if iter_slow_2 == 0 then
send_chunks(ocx + 3, ocy)
send_chunks(ocx + 3, ocy + 1)
send_chunks(ocx + 3, ocy + 2)
send_chunks(ocx + 3, ocy + 3)
elseif iter_slow_2 == 1 then
send_chunks(ocx + 2, ocy + 3)
send_chunks(ocx + 1, ocy + 3)
send_chunks(ocx, ocy + 3)
send_chunks(ocx - 1, ocy + 3)
elseif iter_slow_2 == 2 then
send_chunks(ocx - 2, ocy + 3)
send_chunks(ocx - 3, ocy + 3)
send_chunks(ocx - 3, ocy + 2)
send_chunks(ocx - 3, ocy + 1)
elseif iter_slow_2 == 3 then
send_chunks(ocx - 3, ocy)
send_chunks(ocx - 3, ocy - 1)
send_chunks(ocx - 3, ocy - 2)
send_chunks(ocx - 3, ocy - 3)
elseif iter_slow_2 == 4 then
send_chunks(ocx - 2, ocy - 3)
send_chunks(ocx - 1, ocy - 3)
send_chunks(ocx, ocy - 3)
send_chunks(ocx + 1, ocy - 3)
else
send_chunks(ocx + 2, ocy - 3)
send_chunks(ocx + 3, ocy - 3)
send_chunks(ocx + 3, ocy - 2)
send_chunks(ocx + 3, ocy - 1)
end
net.proxy_bin_send(KEY_WORLD_END, string.char(math.min(priority + 2, 16)) .. pos_data)
iter_slow_2 = iter_slow_2 + 1
if iter_slow_2 == 6 then
iter_slow_2 = 0
end
end
end
local wait
function world_sync.on_world_update()
if ctx.run_ended or (wait ~= nil and wait > GameGetFrameNum()) then
return
end
wait = nil
int = math.floor(tonumber(ModSettingGet("quant.ew.world_sync") or 4) or 4 + 0.5)
local cx, cy = GameGetCameraPos()
cx, cy = math.floor(cx / CHUNK_SIZE), math.floor(cy / CHUNK_SIZE)
local player_data = ctx.my_player
if not EntityGetIsAlive(player_data.entity) then
return
end
local px, py = EntityGetTransform(player_data.entity)
-- Original Chunk x/y
local ocx, ocy = math.floor(px / CHUNK_SIZE), math.floor(py / CHUNK_SIZE)
local n = 0
if EntityHasTag(ctx.my_player.entity, "ew_notplayer") or GameHasFlagRun("ending_game_completed") then
n = 1
end
local pos_data
if GameGetFrameNum() % int ~= 0 and GameGetFrameNum() % (int * 4) == 3 then
pos_data = ocx .. ":" .. ocy .. ":" .. cx .. ":" .. cy .. ":" .. n .. ":" .. ctx.proxy_opt.world_num
else
pos_data = ctx.proxy_opt.world_num
end
if math.abs(cx - ocx) > 2 or math.abs(cy - ocy) > 2 then
if ctx.spectating_over_peer_id ~= nil and ctx.spectating_over_peer_id ~= ctx.my_id then
if GameGetFrameNum() % 3 ~= 2 then
get_all_chunks(cx, cy, pos_data, 16, false)
else
get_all_chunks(ocx, ocy, pos_data, 16, true)
end
else
wait = GameGetFrameNum() + 30
end
else
local pri = 0
if EntityHasTag(ctx.my_player.entity, "ew_notplayer") then
pri = 16
end
get_all_chunks(ocx, ocy, pos_data, pri, true)
end
end
local PixelRun_const_ptr = ffi.typeof("struct PixelRun const*")
function world_sync.handle_world_data(datum)
local grid_world = world_ffi.get_grid_world()
local header = ffi.cast("struct EncodedAreaHeader const*", ffi.cast("char const*", datum))
local runs = ffi.cast(PixelRun_const_ptr, ffi.cast("const char*", datum) + ffi.sizeof(world.EncodedAreaHeader))
world.decode(grid_world, header, runs)
end
net.net_handling.proxy[0] = function(_, value)
world_sync.handle_world_data(value)
end
return world_sync

View file

@ -91,7 +91,7 @@ local function load_modules()
ctx.dofile_and_add_hooks("mods/quant.ew/files/system/weather_sync.lua")
ctx.load_system("polymorph")
ctx.load_system("world_sync")
-- ctx.load_system("world_sync")
-- ctx.load_system("spawn_hooks")
ctx.dofile_and_add_hooks("mods/quant.ew/files/system/proxy_info.lua")

View file

@ -4,6 +4,7 @@ pub mod message_socket;
pub mod basic_types;
pub mod des;
pub mod world_sync;
pub use basic_types::*;
@ -44,6 +45,7 @@ pub enum NoitaInbound {
my_peer_id: PeerId,
},
ProxyToDes(des::ProxyToDes),
ProxyToWorldSync(world_sync::ProxyToWorldSync),
RemoteMessage {
source: PeerId,
message: RemoteMessage,
@ -54,6 +56,7 @@ pub enum NoitaInbound {
pub enum NoitaOutbound {
Raw(Vec<u8>),
DesToProxy(des::DesToProxy),
WorldSyncToProxy(world_sync::WorldSyncToProxy),
RemoteMessage {
reliable: bool,
destination: Destination<PeerId>,

35
shared/src/world_sync.rs Normal file
View file

@ -0,0 +1,35 @@
use bitcode::{Decode, Encode};
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub struct PixelRun<Pixel> {
pub length: u16,
pub data: Pixel,
}
pub const CHUNK_SIZE: usize = 128;
#[derive(Debug, Encode, Decode, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ChunkCoord(pub i32, pub i32);
#[derive(Debug, Encode, Decode, Clone)]
pub struct NoitaWorldUpdate {
pub coord: ChunkCoord,
pub runs: Vec<PixelRun<RawPixel>>,
}
#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone, Copy)]
pub struct RawPixel {
pub material: u16,
pub flags: u8,
}
#[derive(Debug, Encode, Decode, Clone)]
pub enum WorldSyncToProxy {
Updates(Vec<NoitaWorldUpdate>),
End(Option<(i32, i32, i32, i32, bool)>, u8, u8),
}
#[derive(Debug, Encode, Decode, Clone)]
pub enum ProxyToWorldSync {
Updates(Vec<NoitaWorldUpdate>),
}