Resumed to project. INIT

This commit is contained in:
2026-06-17 08:23:13 +02:00
commit fd02ed6d8b
51 changed files with 5067 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "server"
version = "0.1.0"
edition = "2024"
[dependencies]
shared = { path = "../shared" }
bytemuck = "1.24.0"
+102
View File
@@ -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
}
}
+19
View File
@@ -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,
}
}
}
+62
View File
@@ -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);
}
}
+241
View File
@@ -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
}
+47
View File
@@ -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));
}
}
}
}
+126
View File
@@ -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
}
}