Abschlusskorrekturen M1c: Schläge der Gäste, Figuren und Fragebogen

- Der Host hält einen Schlag eines Gastes, der während der Abklingzeit
  ankommt, bis zum ersten Takt danach. Übers Internet ging sonst etwa
  jeder fünfte Schlag eines Gastes verloren.
- ENet drosselt die Figuren nie mehr; sonst konnten die Figuren eines
  von zwei Gästen dauerhaft stehen bleiben. Figuren gehen geordnet, und
  eine stille Verbindung gilt nach höchstens acht Sekunden als verloren.
- Positionen, die keine Zahlen sind, und Figuren von Spielern, die
  nicht online sind, werden verworfen.
- Ein Host mit angehaltener Simulation sagt das in der Statuszeile.
- Neue Tests: Schlag während der Abklingzeit, Zielauswahl von NetLink,
  verworfene Figuren, Neuabgleich über die Brücke.
- Der Fragebogen sagt, was jeder Rechner braucht, und die Freigabe in
  der Firewall gehört zum Host. CLAUDE.md beschreibt das Halten der
  Schläge, die Drosselung und die Zeitüberschreitung und ergänzt die
  Vertrauensregel um die Schlüssel.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-02 01:22:36 +02:00
parent 7f4bbd9606
commit be22e9f358
13 changed files with 258 additions and 27 deletions
+53 -4
View File
@@ -39,7 +39,13 @@ enum Link {
/// Accepted. The guest has the world, and its join runs in the next tick.
Joining { key: PlayerKey },
/// In the world as `player`. `actions` counts its actions in this tick.
Playing { player: PlayerId, actions: u32 },
/// `held` is the newest action that arrived while the player's cooldown
/// still ran; it goes into the first tick after the cooldown.
Playing {
player: PlayerId,
actions: u32,
held: Option<Action>,
},
}
/// The host of a shared world.
@@ -139,16 +145,30 @@ impl Host {
(Link::Greeting, Ok(ToHost::ForeignHello { .. })) => {
self.refuse(peer, Refusal::OtherProtocol);
}
(Link::Playing { player, actions }, Ok(ToHost::Act(action))) => {
(
Link::Playing {
player,
actions,
held,
},
Ok(ToHost::Act(action)),
) => {
if actions < MAX_ACTIONS_PER_TICK {
// A guest counts the cooldown in its own ticks, which
// lag the host's by the round trip. An action that
// arrives too early would be lost, so it waits.
let wait = held.is_some() || cooling_down(&self.world, player);
if !wait {
self.inbox.push(action.command(player));
}
self.links.insert(
peer,
Link::Playing {
player,
actions: actions + 1,
held: if wait { Some(action) } else { None },
},
);
self.inbox.push(action.command(player));
}
}
// The guest acted before its join ran. It cannot know its player
@@ -243,6 +263,7 @@ impl Host {
/// Runs the next tick with every queued command, sends it to every
/// accepted guest and returns what happened.
pub fn step(&mut self) -> Vec<Event> {
self.release_held_actions();
let commands = std::mem::take(&mut self.inbox);
let tick = self.world.tick();
let events = self.recording.step(&mut self.world, &commands);
@@ -268,7 +289,13 @@ impl Host {
for (peer, link) in self.links.iter_mut() {
match link {
Link::Joining { key } => match online_player(&self.world, key) {
Some(player) => *link = Link::Playing { player, actions: 0 },
Some(player) => {
*link = Link::Playing {
player,
actions: 0,
held: None,
}
}
None => refused.push(*peer),
},
Link::Playing { actions, .. } => *actions = 0,
@@ -283,6 +310,20 @@ impl Host {
events
}
/// Queues the held actions whose player's cooldown is over.
fn release_held_actions(&mut self) {
for link in self.links.values_mut() {
let Link::Playing { player, held, .. } = link else {
continue;
};
if held.is_some() && !cooling_down(&self.world, *player) {
if let Some(action) = held.take() {
self.inbox.push(action.command(*player));
}
}
}
}
/// Number of accepted guests, including those whose join has not run.
pub fn guest_count(&self) -> usize {
self.links
@@ -316,6 +357,14 @@ impl Host {
}
}
/// Whether `player` still waits for its cooldown in the next tick of
/// `world`, so that an action then would be dropped.
fn cooling_down(world: &World, player: PlayerId) -> bool {
world
.player(player)
.is_some_and(|state| world.tick() < state.next_action_tick)
}
/// The player with `key`, if it is online in `world`.
fn online_player(world: &World, key: &PlayerKey) -> Option<PlayerId> {
let id = world.find_player(key)?;