Resumed to project. INIT
This commit is contained in:
Executable
+8
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
shared = { path = "../shared" }
|
||||
bytemuck = "1.24.0"
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct TileFlags(pub u8);
|
||||
|
||||
impl TileFlags {
|
||||
pub const COLLIDABLE: u8 = 1 << 0;
|
||||
pub const OPAQUE: u8 = 1 << 1;
|
||||
pub const LIQUID: u8 = 1 << 2;
|
||||
|
||||
pub fn collidable(self) -> bool { self.0 & Self::COLLIDABLE != 0 }
|
||||
pub fn opaque(self) -> bool { self.0 & Self::OPAQUE != 0 }
|
||||
pub fn liquid(self) -> bool { self.0 & Self::LIQUID != 0 }
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct TileDef { pub tile_id: u16, pub flags: TileFlags }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PaletteFullError;
|
||||
|
||||
pub struct Chunk {
|
||||
pub palette: [TileDef; 64],
|
||||
pub pal_count: u8,
|
||||
pub tiles: Box<[u8; 1024]>,
|
||||
pub version: u16,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
/// Runtime use: all tiles empty, pal_count=1, version=1.
|
||||
/// Clients with version=0 will always request this chunk.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
palette: [TileDef::default(); 64],
|
||||
pal_count: 1,
|
||||
tiles: Box::new([0u8; 1024]),
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find existing palette entry or insert a new one.
|
||||
/// Index 0 is always the null sentinel; this short-circuits for default TileDef.
|
||||
/// Returns (palette_index, was_new). Does NOT bump version.
|
||||
fn find_or_insert_raw(&mut self, def: TileDef) -> Result<(u8, bool), PaletteFullError> {
|
||||
if def == TileDef::default() {
|
||||
return Ok((0, false));
|
||||
}
|
||||
for i in 1..self.pal_count as usize {
|
||||
if self.palette[i] == def {
|
||||
return Ok((i as u8, false));
|
||||
}
|
||||
}
|
||||
if self.pal_count >= 64 {
|
||||
return Err(PaletteFullError);
|
||||
}
|
||||
let idx = self.pal_count;
|
||||
self.palette[idx as usize] = def;
|
||||
self.pal_count += 1;
|
||||
Ok((idx, true))
|
||||
}
|
||||
|
||||
/// World gen path. Calls the closure for every tile coordinate.
|
||||
/// version=0 so the first StatePacket causes all clients to request it.
|
||||
pub fn generate(f: impl Fn(u8, u8) -> TileDef) -> Result<Self, PaletteFullError> {
|
||||
let mut chunk = Self {
|
||||
palette: [TileDef::default(); 64],
|
||||
pal_count: 1,
|
||||
tiles: Box::new([0u8; 1024]),
|
||||
version: 0,
|
||||
};
|
||||
for ly in 0u8..32 {
|
||||
for lx in 0u8..32 {
|
||||
let def = f(lx, ly);
|
||||
let (idx, _) = chunk.find_or_insert_raw(def)?;
|
||||
chunk.tiles[ly as usize * 32 + lx as usize] = idx;
|
||||
}
|
||||
}
|
||||
chunk.version = 1;
|
||||
Ok(chunk)
|
||||
}
|
||||
|
||||
/// Set a tile at local coords. Bumps version if tile index or palette changed.
|
||||
pub fn set_tile(&mut self, lx: u8, ly: u8, def: TileDef) -> Result<(), PaletteFullError> {
|
||||
let (idx, was_new) = self.find_or_insert_raw(def)?;
|
||||
let slot = &mut self.tiles[ly as usize * 32 + lx as usize];
|
||||
let changed = *slot != idx;
|
||||
*slot = idx;
|
||||
if changed || was_new {
|
||||
self.version = self.version.wrapping_add(1);
|
||||
if self.version == 0 {
|
||||
self.version = 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_tile(&self, lx: u8, ly: u8) -> TileDef {
|
||||
self.palette[self.tiles[ly as usize * 32 + lx as usize] as usize]
|
||||
}
|
||||
|
||||
pub fn tile_flags(&self, lx: u8, ly: u8) -> TileFlags {
|
||||
self.get_tile(lx, ly).flags
|
||||
}
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
pub struct Entity {
|
||||
pub id: u32,
|
||||
pub type_id: u16, // 0 = player
|
||||
pub pos: (i16, i16),
|
||||
pub hp: u16,
|
||||
pub hp_max: u16,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
pub fn new(id: u32, type_id: u16, pos: (i16, i16), hp: u16) -> Self {
|
||||
Self {
|
||||
id,
|
||||
type_id,
|
||||
pos,
|
||||
hp,
|
||||
hp_max: hp,
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
mod chunk;
|
||||
mod entity;
|
||||
mod world;
|
||||
mod sim;
|
||||
mod net;
|
||||
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chunk::{Chunk, TileDef, TileFlags};
|
||||
use world::World;
|
||||
use sim::Sim;
|
||||
use net::Net;
|
||||
|
||||
const TICK_HZ: u32 = 24;
|
||||
const TICK_DURATION: Duration = Duration::from_micros(1_000_000 / TICK_HZ as u64);
|
||||
|
||||
fn build_world() -> World {
|
||||
let mut w = World::new();
|
||||
for cy in -1i16..=1 {
|
||||
for cx in -1i16..=1 {
|
||||
let chunk = Chunk::generate(|lx, ly| {
|
||||
let (wx, wy) = world::local_to_tile(cx, cy, lx, ly);
|
||||
let border = wx == -32 || wx == 31 || wy == -32 || wy == 31;
|
||||
if border {
|
||||
TileDef { tile_id: 1, flags: TileFlags(TileFlags::COLLIDABLE | TileFlags::OPAQUE) }
|
||||
} else {
|
||||
TileDef::default()
|
||||
}
|
||||
}).expect("palette overflow during world gen");
|
||||
w.set_chunk(cx, cy, chunk);
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let world = build_world();
|
||||
let mut sim = Sim::new(world);
|
||||
|
||||
let mut net = Net::new("127.0.0.1:7777").expect("failed to bind UDP socket");
|
||||
|
||||
let mut next_tick = Instant::now() + TICK_DURATION;
|
||||
let mut tick = 0u32;
|
||||
loop {
|
||||
net.drain_recv();
|
||||
net.evict_stale();
|
||||
|
||||
for addr in net.drain_new_clients() {
|
||||
let entity_id = sim.world.spawn_entity(0, (0, 0), 100);
|
||||
net.add_client(addr, entity_id);
|
||||
println!("client connected: {addr} → entity {entity_id}");
|
||||
}
|
||||
|
||||
let actions = net.drain_actions();
|
||||
sim.tick(tick, &actions);
|
||||
net.broadcast(&sim.world, tick);
|
||||
thread::sleep(next_tick.saturating_duration_since(Instant::now()));
|
||||
next_tick += TICK_DURATION;
|
||||
tick = tick.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
Executable
+241
@@ -0,0 +1,241 @@
|
||||
use std::collections::HashMap;
|
||||
use std::mem::size_of;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::sync::mpsc::{self, Receiver, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
use bytemuck::bytes_of;
|
||||
use shared::{
|
||||
ActionPacket, ChunkEntry, ChunkPacket, EntityEntry, EntityPacket,
|
||||
Header, PingPacket, PongPacket, StatePacket,
|
||||
MAGIC, VERSION, packet_type,
|
||||
};
|
||||
|
||||
use crate::world::World;
|
||||
|
||||
pub struct NetClient {
|
||||
pub entity_id: u32,
|
||||
pub last_seen: Instant,
|
||||
pub send_phase: u8,
|
||||
pub pending_action: Option<u16>,
|
||||
pub last_cache: [ChunkEntry; 9],
|
||||
}
|
||||
|
||||
pub struct Net {
|
||||
clients: HashMap<SocketAddr, NetClient>,
|
||||
new_addrs: Vec<SocketAddr>,
|
||||
recv_rx: Receiver<(SocketAddr, Vec<u8>)>,
|
||||
send_tx: SyncSender<(SocketAddr, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl Net {
|
||||
pub fn new(addr: &str) -> std::io::Result<Self> {
|
||||
let socket = UdpSocket::bind(addr)?;
|
||||
|
||||
let (recv_tx, recv_rx) = mpsc::sync_channel::<(SocketAddr, Vec<u8>)>(256);
|
||||
let (send_tx, send_rx) = mpsc::sync_channel::<(SocketAddr, Vec<u8>)>(256);
|
||||
|
||||
let recv_sock = socket.try_clone()?;
|
||||
thread::spawn(move || {
|
||||
let mut buf = [0u8; 1200];
|
||||
loop {
|
||||
match recv_sock.recv_from(&mut buf) {
|
||||
Ok((n, addr)) => {
|
||||
if recv_tx.send((addr, buf[..n].to_vec())).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let send_sock = socket.try_clone()?;
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
match send_rx.recv() {
|
||||
Ok((addr, data)) => { let _ = send_sock.send_to(&data, addr); }
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// socket is dropped here; send/recv threads own their copies
|
||||
drop(socket);
|
||||
|
||||
Ok(Net { clients: HashMap::new(), new_addrs: Vec::new(), recv_rx, send_tx })
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self, addr: SocketAddr, entity_id: u32) {
|
||||
self.clients.insert(addr, NetClient {
|
||||
entity_id,
|
||||
last_seen: Instant::now(),
|
||||
send_phase: (entity_id % 2) as u8,
|
||||
pending_action: None,
|
||||
last_cache: [ChunkEntry { chunk_id: 0, version: 0 }; 9],
|
||||
});
|
||||
}
|
||||
|
||||
pub fn drain_recv(&mut self) {
|
||||
loop {
|
||||
match self.recv_rx.try_recv() {
|
||||
Ok((addr, data)) => self.handle_packet(addr, data),
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_packet(&mut self, addr: SocketAddr, data: Vec<u8>) {
|
||||
if data.len() < size_of::<Header>() {
|
||||
return;
|
||||
}
|
||||
let header: Header = bytemuck::pod_read_unaligned(&data[..size_of::<Header>()]);
|
||||
if header.magic != MAGIC || header.version != VERSION {
|
||||
return;
|
||||
}
|
||||
|
||||
if header.packet_type == packet_type::ACTION && data.len() == size_of::<ActionPacket>() {
|
||||
let pkt: ActionPacket = bytemuck::pod_read_unaligned(&data);
|
||||
if let Some(client) = self.clients.get_mut(&addr) {
|
||||
client.pending_action = Some(pkt.player_action);
|
||||
client.last_seen = Instant::now();
|
||||
client.last_cache = pkt.cache;
|
||||
} else if !self.new_addrs.contains(&addr) {
|
||||
self.new_addrs.push(addr);
|
||||
}
|
||||
} else if header.packet_type == packet_type::PING && data.len() == size_of::<PingPacket>() {
|
||||
let pkt: PingPacket = bytemuck::pod_read_unaligned(&data);
|
||||
let pong = PongPacket {
|
||||
header: Header::new(packet_type::PONG),
|
||||
timestamp_ms: pkt.timestamp_ms,
|
||||
};
|
||||
let _ = self.send_tx.send((addr, bytes_of(&pong).to_vec()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain_new_clients(&mut self) -> Vec<SocketAddr> {
|
||||
std::mem::take(&mut self.new_addrs)
|
||||
}
|
||||
|
||||
pub fn evict_stale(&mut self) {
|
||||
self.clients.retain(|_, c| c.last_seen.elapsed().as_secs() < 10);
|
||||
}
|
||||
|
||||
pub fn drain_actions(&mut self) -> Vec<(u32, u16)> {
|
||||
self.clients.values_mut()
|
||||
.filter_map(|c| {
|
||||
let action = c.pending_action.take()?;
|
||||
Some((c.entity_id, action))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn broadcast(&self, world: &World, tick: u32) {
|
||||
for (addr, client) in &self.clients {
|
||||
if tick % 2 != client.send_phase as u32 { continue; }
|
||||
|
||||
// Step 1 — resolve player position
|
||||
let Some(entity) = world.entities.get(&client.entity_id) else { continue };
|
||||
let pos = entity.pos;
|
||||
let (player_cx, player_cy) = crate::world::tile_to_chunk(pos.0, pos.1);
|
||||
|
||||
// Step 2 — build 9-slot chunk manifest
|
||||
let mut chunks = [ChunkEntry { chunk_id: 0, version: 0 }; 9];
|
||||
for dy in -1i16..=1 {
|
||||
for dx in -1i16..=1 {
|
||||
let slot = ((dy + 1) * 3 + (dx + 1)) as usize;
|
||||
let cx = player_cx + dx;
|
||||
let cy = player_cy + dy;
|
||||
chunks[slot] = ChunkEntry {
|
||||
chunk_id: shared::chunk_id(cx, cy),
|
||||
version: world.chunk(cx, cy).map_or(0, |c| c.version),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3 — build EntityEntry list + checksum
|
||||
let viewport_entities = world.entities_in_viewport(pos);
|
||||
let mut entries: Vec<EntityEntry> = viewport_entities.iter().map(|e| EntityEntry {
|
||||
id: e.id,
|
||||
type_id: e.type_id,
|
||||
pos_x: e.pos.0,
|
||||
pos_y: e.pos.1,
|
||||
hp: e.hp,
|
||||
hp_max: e.hp_max,
|
||||
elo: 0,
|
||||
entity_flags: 0,
|
||||
meta_len: 0,
|
||||
}).collect();
|
||||
if entries.len() > 66 {
|
||||
entries.truncate(66); // TODO: multi-datagram
|
||||
}
|
||||
let checksum = fnv1a_entities(&entries);
|
||||
|
||||
// Step 4 — send StatePacket (72 bytes)
|
||||
let state = StatePacket {
|
||||
header: Header::new(packet_type::STATE),
|
||||
tick,
|
||||
chunks,
|
||||
player_entity_id: client.entity_id,
|
||||
entity_checksum: checksum,
|
||||
};
|
||||
let _ = self.send_tx.send((*addr, bytes_of(&state).to_vec()));
|
||||
|
||||
// Step 5 — send EntityPacket (1200 bytes)
|
||||
let mut pkt = <EntityPacket as bytemuck::Zeroable>::zeroed();
|
||||
pkt.header = Header::new(packet_type::ENTITY);
|
||||
pkt.tick = tick;
|
||||
pkt.entity_count = entries.len() as u8;
|
||||
pkt.packet_flags = 0; // no more-flag; single datagram
|
||||
for (i, e) in entries.iter().enumerate() { pkt.entities[i] = *e; }
|
||||
let _ = self.send_tx.send((*addr, bytes_of(&pkt).to_vec()));
|
||||
|
||||
// Step 6 — send ChunkPackets for cache misses
|
||||
for slot in 0..9 {
|
||||
let current = chunks[slot];
|
||||
let cached = client.last_cache[slot];
|
||||
let miss = cached.chunk_id != current.chunk_id || cached.version != current.version;
|
||||
if miss {
|
||||
let (cx, cy) = shared::chunk_coords(current.chunk_id);
|
||||
if let Some(chunk) = world.chunk(cx, cy) {
|
||||
let mut cpkt = <ChunkPacket as bytemuck::Zeroable>::zeroed();
|
||||
cpkt.header = Header::new(packet_type::CHUNK);
|
||||
cpkt.chunk = current;
|
||||
cpkt.pal_count = chunk.pal_count;
|
||||
for i in 0..64 { cpkt.palette[i] = chunk.palette[i].tile_id; }
|
||||
cpkt.tiles = pack_tiles(&chunk.tiles);
|
||||
let _ = self.send_tx.send((*addr, bytes_of(&cpkt).to_vec()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fnv1a_entities(entries: &[EntityEntry]) -> u32 {
|
||||
const FNV_BASIS: u32 = 2_166_136_261;
|
||||
const FNV_PRIME: u32 = 16_777_619;
|
||||
let mut h = FNV_BASIS;
|
||||
for e in entries {
|
||||
for &byte in bytes_of(e) {
|
||||
h ^= byte as u32;
|
||||
h = h.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
fn pack_tiles(tiles: &[u8; 1024]) -> [u8; 768] {
|
||||
let mut raw = [0u8; 768];
|
||||
for i in 0..256 {
|
||||
let ia = tiles[i*4] & 0x3F;
|
||||
let ib = tiles[i*4+1] & 0x3F;
|
||||
let ic = tiles[i*4+2] & 0x3F;
|
||||
let id = tiles[i*4+3] & 0x3F;
|
||||
raw[i*3] = ia | (ib << 6);
|
||||
raw[i*3+1] = (ib >> 2) | (ic << 4);
|
||||
raw[i*3+2] = (ic >> 4) | (id << 2);
|
||||
}
|
||||
raw
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
use std::collections::HashMap;
|
||||
use shared::player_action;
|
||||
use crate::world::World;
|
||||
|
||||
pub struct Sim {
|
||||
pub world: World,
|
||||
pending: HashMap<u32, u16>,
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
pub fn new(world: World) -> Self {
|
||||
Self { world, pending: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, tick: u32, actions: &[(u32, u16)]) {
|
||||
for &(entity_id, action) in actions {
|
||||
self.pending.insert(entity_id, action);
|
||||
}
|
||||
if tick.is_multiple_of(4) {
|
||||
let pending = std::mem::take(&mut self.pending);
|
||||
self.entity_tick(&pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_tick(&mut self, actions: &HashMap<u32, u16>) {
|
||||
for (&entity_id, &action) in actions {
|
||||
if action == player_action::NOOP {
|
||||
continue;
|
||||
}
|
||||
let pos = match self.world.entities.get(&entity_id) {
|
||||
Some(e) => e.pos,
|
||||
None => continue,
|
||||
};
|
||||
let (nx, ny) = match action {
|
||||
player_action::NORTH => (pos.0, pos.1 - 1),
|
||||
player_action::EAST => (pos.0 + 1, pos.1),
|
||||
player_action::SOUTH => (pos.0, pos.1 + 1),
|
||||
player_action::WEST => (pos.0 - 1, pos.1),
|
||||
_ => continue,
|
||||
};
|
||||
let blocked = self.world.tile_flags(nx, ny).map_or(true, |f| f.collidable());
|
||||
if !blocked {
|
||||
self.world.move_entity(entity_id, (nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::chunk::{Chunk, TileDef, TileFlags, PaletteFullError};
|
||||
use crate::entity::Entity;
|
||||
|
||||
pub fn tile_to_chunk(tx: i16, ty: i16) -> (i16, i16) {
|
||||
(tx >> 5, ty >> 5)
|
||||
}
|
||||
|
||||
pub fn tile_to_local(tx: i16, ty: i16) -> (u8, u8) {
|
||||
((tx & 31) as u8, (ty & 31) as u8)
|
||||
}
|
||||
|
||||
pub fn local_to_tile(cx: i16, cy: i16, lx: u8, ly: u8) -> (i16, i16) {
|
||||
((cx << 5) | lx as i16, (cy << 5) | ly as i16)
|
||||
}
|
||||
|
||||
pub struct World {
|
||||
chunks: HashMap<(i16, i16), Chunk>,
|
||||
pub entities: HashMap<u32, Entity>,
|
||||
chunk_entities: HashMap<(i16, i16), Vec<u32>>,
|
||||
next_entity_id: u32,
|
||||
}
|
||||
|
||||
impl World {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
chunks: HashMap::new(),
|
||||
entities: HashMap::new(),
|
||||
chunk_entities: HashMap::new(),
|
||||
next_entity_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chunk(&self, cx: i16, cy: i16) -> Option<&Chunk> {
|
||||
self.chunks.get(&(cx, cy))
|
||||
}
|
||||
|
||||
pub fn set_chunk(&mut self, cx: i16, cy: i16, chunk: Chunk) {
|
||||
self.chunks.insert((cx, cy), chunk);
|
||||
}
|
||||
|
||||
pub fn tile_flags(&self, wx: i16, wy: i16) -> Option<TileFlags> {
|
||||
let (cx, cy) = tile_to_chunk(wx, wy);
|
||||
let (lx, ly) = tile_to_local(wx, wy);
|
||||
self.chunks.get(&(cx, cy)).map(|c| c.tile_flags(lx, ly))
|
||||
}
|
||||
|
||||
pub fn set_tile(&mut self, wx: i16, wy: i16, def: TileDef) -> Result<(), PaletteFullError> {
|
||||
let (cx, cy) = tile_to_chunk(wx, wy);
|
||||
let (lx, ly) = tile_to_local(wx, wy);
|
||||
let chunk = self.chunks.entry((cx, cy)).or_insert_with(Chunk::new);
|
||||
chunk.set_tile(lx, ly, def)
|
||||
}
|
||||
|
||||
pub fn spawn_entity(&mut self, type_id: u16, pos: (i16, i16), hp: u16) -> u32 {
|
||||
let id = self.next_entity_id;
|
||||
self.next_entity_id += 1;
|
||||
let entity = Entity::new(id, type_id, pos, hp);
|
||||
self.add_entity(entity);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn add_entity(&mut self, entity: Entity) {
|
||||
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
|
||||
let id = entity.id;
|
||||
self.entities.insert(id, entity);
|
||||
self.chunk_entities.entry(chunk_key).or_default().push(id);
|
||||
}
|
||||
|
||||
pub fn remove_entity(&mut self, id: u32) {
|
||||
if let Some(entity) = self.entities.remove(&id) {
|
||||
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
|
||||
if let Some(list) = self.chunk_entities.get_mut(&chunk_key) {
|
||||
list.retain(|&eid| eid != id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_entity(&mut self, id: u32, new_pos: (i16, i16)) {
|
||||
if let Some(entity) = self.entities.get_mut(&id) {
|
||||
let old_chunk = tile_to_chunk(entity.pos.0, entity.pos.1);
|
||||
let new_chunk = tile_to_chunk(new_pos.0, new_pos.1);
|
||||
entity.pos = new_pos;
|
||||
if old_chunk != new_chunk {
|
||||
if let Some(list) = self.chunk_entities.get_mut(&old_chunk) {
|
||||
list.retain(|&eid| eid != id);
|
||||
}
|
||||
self.chunk_entities.entry(new_chunk).or_default().push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entities_in_chunk(&self, cx: i16, cy: i16) -> &[u32] {
|
||||
self.chunk_entities.get(&(cx, cy)).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Returns all entities within a 30×30 tile viewport centered on `center`.
|
||||
pub fn entities_in_viewport(&self, center: (i16, i16)) -> Vec<&Entity> {
|
||||
let half: i16 = 15;
|
||||
let min_x = center.0 - half;
|
||||
let max_x = center.0 + half - 1;
|
||||
let min_y = center.1 - half;
|
||||
let max_y = center.1 + half - 1;
|
||||
|
||||
let min_cx = tile_to_chunk(min_x, min_y).0;
|
||||
let max_cx = tile_to_chunk(max_x, max_y).0;
|
||||
let min_cy = tile_to_chunk(min_x, min_y).1;
|
||||
let max_cy = tile_to_chunk(max_x, max_y).1;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for cy in min_cy..=max_cy {
|
||||
for cx in min_cx..=max_cx {
|
||||
for &eid in self.entities_in_chunk(cx, cy) {
|
||||
if let Some(e) = self.entities.get(&eid) {
|
||||
if e.pos.0 >= min_x && e.pos.0 <= max_x
|
||||
&& e.pos.1 >= min_y && e.pos.1 <= max_y
|
||||
{
|
||||
result.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user