current state
This commit is contained in:
+30
-15
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::mem::size_of;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::sync::mpsc::{self, Receiver, SyncSender};
|
||||
@@ -15,13 +15,21 @@ use shared::{
|
||||
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 entity_id: u32,
|
||||
pub last_seen: Instant,
|
||||
pub send_phase: u8,
|
||||
/// Tick-addressed actions received since the last drain, in arrival order —
|
||||
/// `(target_tick, action)`. Window resolution, replacement and the lateness rule
|
||||
/// all live in the sim; this is only the per-tick hand-off buffer, bounded so a
|
||||
/// flood between drains cannot grow it.
|
||||
pub pending_actions: VecDeque<(u32, u16)>,
|
||||
pub last_cache: [ChunkEntry; 9],
|
||||
}
|
||||
|
||||
/// Hand-off buffer bound. Drained every tick; an honest client sends at most one
|
||||
/// action per movement window, so a handful is already generous.
|
||||
const PENDING_ACTIONS_MAX: usize = 8;
|
||||
|
||||
pub struct Net {
|
||||
clients: HashMap<SocketAddr, NetClient>,
|
||||
new_addrs: Vec<SocketAddr>,
|
||||
@@ -70,10 +78,10 @@ impl Net {
|
||||
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],
|
||||
last_seen: Instant::now(),
|
||||
send_phase: (entity_id % 2) as u8,
|
||||
pending_actions: VecDeque::new(),
|
||||
last_cache: [ChunkEntry { chunk_id: 0, version: 0 }; 9],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,9 +106,15 @@ impl Net {
|
||||
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;
|
||||
// `target_tick == 0` marks pure keep-alive / cache-ack packets — they
|
||||
// must never enter the schedule. Everything else (including NOOPs, which
|
||||
// *retract* the addressed window) is handed to the sim in arrival order.
|
||||
let target_tick = pkt.target_tick;
|
||||
if target_tick != 0 && client.pending_actions.len() < PENDING_ACTIONS_MAX {
|
||||
client.pending_actions.push_back((target_tick, pkt.player_action));
|
||||
}
|
||||
} else if !self.new_addrs.contains(&addr) {
|
||||
self.new_addrs.push(addr);
|
||||
}
|
||||
@@ -122,11 +136,12 @@ impl Net {
|
||||
self.clients.retain(|_, c| c.last_seen.elapsed().as_secs() < 10);
|
||||
}
|
||||
|
||||
pub fn drain_actions(&mut self) -> Vec<(u32, u16)> {
|
||||
pub fn drain_actions(&mut self) -> Vec<(u32, u32, u16)> {
|
||||
self.clients.values_mut()
|
||||
.filter_map(|c| {
|
||||
let action = c.pending_action.take()?;
|
||||
Some((c.entity_id, action))
|
||||
.flat_map(|c| {
|
||||
let entity_id = c.entity_id;
|
||||
c.pending_actions.drain(..)
|
||||
.map(move |(target_tick, action)| (entity_id, target_tick, action))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
+62
-24
@@ -1,10 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use shared::player_action;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use shared::{ACTION_WINDOW_HORIZON, TICKS_PER_MOVE};
|
||||
use crate::world::World;
|
||||
|
||||
pub struct Sim {
|
||||
pub world: World,
|
||||
pending: HashMap<u32, u16>,
|
||||
/// Scheduled actions per entity, keyed by movement window (`tick / TICKS_PER_MOVE`).
|
||||
/// The window slot is the whole ordering model: an action addressed to an occupied
|
||||
/// window *replaces* it (retraction and rescheduling by the client, dedup of
|
||||
/// retransmits), a late action only fills the next window if it is empty, and
|
||||
/// anything past `ACTION_WINDOW_HORIZON` is dropped — so no client can grow server
|
||||
/// memory or move faster than one action per window.
|
||||
pending: HashMap<u32, BTreeMap<u32, u16>>,
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
@@ -12,35 +18,67 @@ impl Sim {
|
||||
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);
|
||||
pub fn tick(&mut self, tick: u32, actions: &[(u32, u32, u16)]) {
|
||||
// The earliest window still addressable at this tick. On a movement tick that is
|
||||
// the window executing *this call* — actions arriving the same tick still count.
|
||||
let min_window = tick.div_ceil(TICKS_PER_MOVE);
|
||||
|
||||
for &(entity_id, target_tick, action) in actions {
|
||||
let slots = self.pending.entry(entity_id).or_default();
|
||||
let window = target_tick.div_ceil(TICKS_PER_MOVE);
|
||||
if window < min_window {
|
||||
// Late. A movement action keeps its *order* instead of its time: it
|
||||
// fills the first still-empty upcoming window, so bunched late arrivals
|
||||
// don't collapse onto one slot and eat each other. A late NOOP is
|
||||
// dropped — once its window has passed, its cancellation intent is
|
||||
// ambiguous, and as a gap-filler it would block real steps (worst case
|
||||
// one stale step executes; the client's reconciliation handles that).
|
||||
if shared::action_delta(action).is_some() {
|
||||
let gap = (min_window..min_window + ACTION_WINDOW_HORIZON as u32)
|
||||
.find(|w| !slots.contains_key(w));
|
||||
if let Some(w) = gap {
|
||||
slots.insert(w, action);
|
||||
}
|
||||
}
|
||||
} else if window - min_window < ACTION_WINDOW_HORIZON as u32 {
|
||||
slots.insert(window, action); // newest addressing wins
|
||||
}
|
||||
}
|
||||
if tick.is_multiple_of(4) {
|
||||
let pending = std::mem::take(&mut self.pending);
|
||||
self.entity_tick(&pending);
|
||||
|
||||
if tick.is_multiple_of(TICKS_PER_MOVE) {
|
||||
self.entity_tick(tick / TICKS_PER_MOVE);
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_tick(&mut self, actions: &HashMap<u32, u16>) {
|
||||
for (&entity_id, &action) in actions {
|
||||
if action == player_action::NOOP {
|
||||
continue;
|
||||
}
|
||||
fn entity_tick(&mut self, window: u32) {
|
||||
// Exactly one action per entity per movement window, then the window is gone.
|
||||
let actions: Vec<(u32, u16)> = self.pending.iter_mut()
|
||||
.filter_map(|(&id, slots)| {
|
||||
let action = slots.remove(&window);
|
||||
slots.retain(|&w, _| w > window); // drop anything the timeline passed by
|
||||
action.map(|a| (id, a))
|
||||
})
|
||||
.collect();
|
||||
self.pending.retain(|_, slots| !slots.is_empty());
|
||||
|
||||
for (entity_id, action) in actions {
|
||||
let delta = match shared::action_delta(action) {
|
||||
Some(d) => d,
|
||||
None => continue, // NOOP or garbage
|
||||
};
|
||||
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));
|
||||
let from = (pos.0 as i32, pos.1 as i32);
|
||||
let to = (from.0 + delta.0, from.1 + delta.1);
|
||||
// `step_allowed` is the shared movement rule (chessboard geometry, no corner
|
||||
// cutting) — the client predicts with the exact same function.
|
||||
let allowed = shared::step_allowed(from, to, |x, y| {
|
||||
self.world.tile_flags(x as i16, y as i16).map_or(true, |f| f.collidable())
|
||||
});
|
||||
if allowed {
|
||||
self.world.move_entity(entity_id, (to.0 as i16, to.1 as i16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user