mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
dedicated: player-release ends a hand-over on the source server
`player-release <uuid> <hand-over> moved [announce]` takes a player that
`player-export <uuid> freeze` froze out of the world, because another
server has it now: the others see its entity and its list entry go, no
player file or record is written, the log and the leave event record the
departure ("moved to another server"), and the connection is closed. The
"left the game" line is neither sent nor logged unless `announce` is
added. `player-release <uuid> <hand-over> abort` lifts the freeze.
Both name the hand-over id the export returned, and are refused for a
player who is not in a hand-over, for another hand-over than its current
one, and for a player who died while frozen. Once the freeze has run out
the hand-over is over and a late `moved` is refused: the player has
played on since, and the target's record is older. So a stale or
repeated release cannot remove someone who is playing again.
A frozen player takes no damage (Abilities.invulnerable, which the void
and /kill still pass) and picks up no items, arrows or experience orbs,
so its record stays true until the release. A player who dies while
frozen all the same ends the hand-over with a warning.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dfba9cab74
commit
ebfab7fc89
8 files changed
+379
-22
No files matched your search
@@ -3,8 +3,8 @@
|
||||
//! (including their positions) and the loaded chunks; `console <line>` runs a console command and answers with the lines it logged;
|
||||
//! `server-events <since>` answers the typed events after that number (events.rs); `management <connection>
|
||||
//! <json>` edits the lists as vanilla's management protocol does (management.rs).
|
||||
//! `player-expect <uuid> [seconds]` and `player-export <uuid> [freeze]` are a proxy's, for a player it moves
|
||||
//! between two servers (transfer.rs).
|
||||
//! `player-expect <uuid> [seconds]`, `player-export <uuid> [freeze]` and `player-release <uuid> ...` are a
|
||||
//! proxy's, for a player it moves between two servers (transfer.rs).
|
||||
use super::{ServerState, capture_log};
|
||||
use crate::game::Game;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! A proxy's requests on the control endpoint, for a player it moves between two servers:
|
||||
//! `player-expect <uuid> [seconds]` (the target is told who is coming) and
|
||||
//! `player-export <uuid> [freeze]` (the source answers with the player's record). They run
|
||||
//! `player-expect <uuid> [seconds]` (the target is told who is coming),
|
||||
//! `player-export <uuid> [freeze]` (the source answers with the player's record) and
|
||||
//! `player-release <uuid> <hand-over> moved [announce] | abort` (the source lets the player
|
||||
//! go). They run
|
||||
//! between two ticks, on the server thread, so the player is never caught half-way through a
|
||||
//! tick. The rules are `lan/arrivals.rs`'s. All of them are refused while `server.toml`'s
|
||||
//! `[transfer]` is off.
|
||||
@@ -26,6 +28,10 @@ impl Game<'_> {
|
||||
None => json!({ "error": "the server is not open to players" }),
|
||||
},
|
||||
"player-export" => self.player_export(args),
|
||||
"player-release" => match self.lan.as_mut() {
|
||||
Some(host) => arrivals::release_request(&mut host.hub, args),
|
||||
None => json!({ "error": "the server is not open to players" }),
|
||||
},
|
||||
other => json!({ "error": format!("unknown request {other:?}") }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,7 +1536,9 @@ impl Game<'_> {
|
||||
guest: &mut horizon_client::lan::guest::Guest,
|
||||
) {
|
||||
self.guest_attachments_into(guest);
|
||||
if guest.placed {
|
||||
// A player a proxy moved to another server (`player-release moved`) is saved
|
||||
// there now, not here.
|
||||
if guest.placed && !guest.released {
|
||||
let _ = self.finish_player_autosaves(true);
|
||||
if host.owner.as_ref().is_some_and(|o| o.uuid == guest.uuid) {
|
||||
playerdata::save_owner(guest, &mut self.level, &self.world_root);
|
||||
@@ -1686,7 +1688,7 @@ impl Game<'_> {
|
||||
host: &LanHost,
|
||||
guest: &horizon_client::lan::guest::Guest,
|
||||
) {
|
||||
if !guest.placed || self.adv.tree.list.is_empty() {
|
||||
if !guest.placed || guest.released || self.adv.tree.list.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(uuid) = self.records_uuid(host, guest.uuid) else {
|
||||
@@ -2111,11 +2113,15 @@ impl Game<'_> {
|
||||
"INFO",
|
||||
&format!("{} lost connection: {}", guest.name, guest.left_reason),
|
||||
);
|
||||
super::dedicated::log(
|
||||
"INFO",
|
||||
&format!("System chat: {} left the game", guest.name),
|
||||
);
|
||||
} else if self.player.is_some() && guest.placed {
|
||||
// A player moved to another server without `announce` was not
|
||||
// announced as leaving, so the line is not logged either.
|
||||
if !guest.quiet_leave {
|
||||
super::dedicated::log(
|
||||
"INFO",
|
||||
&format!("System chat: {} left the game", guest.name),
|
||||
);
|
||||
}
|
||||
} else if self.player.is_some() && guest.placed && !guest.quiet_leave {
|
||||
// ServerGamePacketListenerImpl.removePlayerFromWorld.
|
||||
let who = self.player_text(&guest.name, guest.uuid);
|
||||
self.system_message(
|
||||
|
||||
@@ -457,7 +457,9 @@ impl Game<'_> {
|
||||
motion.flying_speed = g.flying_speed;
|
||||
// FireworkRocketItem.use boosts only a gliding player.
|
||||
motion.fall_flying = g.fall_flying;
|
||||
motion.invulnerable = motion.may_fly;
|
||||
// A player frozen for a hand-over takes no damage (`Abilities.invulnerable`, which the
|
||||
// void and `/kill` still pass), so its record stays true until the release.
|
||||
motion.invulnerable = motion.may_fly || g.handover.is_some();
|
||||
// `ServerPlayer.isInvulnerableTo`'s connection term, as of now.
|
||||
motion.client_loaded = g.client_loaded();
|
||||
let _ = motion.set_health(g.health, false);
|
||||
@@ -3929,9 +3931,13 @@ impl Game<'_> {
|
||||
self.lan_map_updates(host, conn);
|
||||
host.hub.flush_progress(conn);
|
||||
self.guest_dig_feedback(host, conn);
|
||||
self.guest_pickups(host, gi);
|
||||
self.guest_arrows(host, gi);
|
||||
self.guest_orbs(host, gi);
|
||||
// A player frozen for a hand-over picks nothing up: what it took would be
|
||||
// missing from its record, and lost when the release saves nothing.
|
||||
if host.hub.guests[gi].handover.is_none() {
|
||||
self.guest_pickups(host, gi);
|
||||
self.guest_arrows(host, gi);
|
||||
self.guest_orbs(host, gi);
|
||||
}
|
||||
self.guest_die(host, gi, conn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Players that are about to arrive from another server, and a connected
|
||||
//! player read out of this one: the control endpoint's `player-expect` and
|
||||
//! `player-export` (`game/dedicated/transfer.rs` runs them).
|
||||
//! player read out of this one and sent away: the control endpoint's
|
||||
//! `player-expect`, `player-export` and `player-release`
|
||||
//! (`game/dedicated/transfer.rs` runs them).
|
||||
//!
|
||||
//! A proxy that moves a player between two servers first tells the target
|
||||
//! server who is coming (`player-expect <uuid>`), and reads the player out of
|
||||
@@ -408,6 +409,54 @@ pub fn expect_request(hub: &mut Hub, args: &str, now: Instant) -> serde_json::Va
|
||||
}
|
||||
}
|
||||
|
||||
/// `player-release <uuid> <hand-over> moved [announce]` or `player-release <uuid> <hand-over>
|
||||
/// abort`: ends the hand-over the export with `freeze` started. `moved` takes the player out of
|
||||
/// the world, saving nothing and, unless `announce`, without the "left the game" line; `abort`
|
||||
/// lets it go on as it was. Both name the hand-over, so a late or repeated request for one that
|
||||
/// is over touches nothing.
|
||||
pub fn release_request(hub: &mut Hub, args: &str) -> serde_json::Value {
|
||||
let mut parts = args.split_whitespace();
|
||||
let Some(uuid) = parts.next().and_then(parse_uuid) else {
|
||||
return error("player-release needs the player's unique id");
|
||||
};
|
||||
let Some(id) = parts.next().and_then(parse_handover) else {
|
||||
return error("player-release needs the hand-over id the export gave");
|
||||
};
|
||||
let how = parts.next();
|
||||
let announce = match (how, parts.next()) {
|
||||
(Some("moved"), None) => false,
|
||||
(Some("moved"), Some("announce")) => true,
|
||||
(Some("abort"), None) => false,
|
||||
_ => {
|
||||
return error(
|
||||
"player-release <uuid> <hand-over> is followed by moved [announce] or abort",
|
||||
);
|
||||
}
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return error(
|
||||
"player-release takes a unique id, a hand-over id, moved or abort, and at most announce",
|
||||
);
|
||||
}
|
||||
if how == Some("moved") {
|
||||
return match hub.release_moved(uuid, id, announce) {
|
||||
Ok(()) => serde_json::json!({
|
||||
"ok": true,
|
||||
"released": "moved",
|
||||
"announced": announce,
|
||||
}),
|
||||
Err(why) => error(why),
|
||||
};
|
||||
}
|
||||
match hub.handover_of(uuid, id) {
|
||||
Ok(i) => {
|
||||
hub.guests[i].handover = None;
|
||||
serde_json::json!({ "ok": true, "released": "abort" })
|
||||
}
|
||||
Err(why) => error(why),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -572,6 +621,7 @@ mod hub_tests {
|
||||
guest::Guest,
|
||||
hub::{Hub, HubConfig, TickCtx},
|
||||
};
|
||||
use horizon_protocol::ids::play::clientbound as id;
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -642,6 +692,29 @@ mod hub_tests {
|
||||
(g, rx)
|
||||
}
|
||||
|
||||
/// The packets waiting in a channel, in order, each as its id and payload.
|
||||
fn packets(rx: &Receiver<Out>) -> Vec<(i32, Vec<u8>)> {
|
||||
let mut out = Vec::new();
|
||||
let mut one = |body: &[u8]| {
|
||||
let mut r = horizon_protocol::buf::Reader::new(body);
|
||||
let id = r.varint().unwrap();
|
||||
out.push((id, r.rest().to_vec()));
|
||||
};
|
||||
while let Ok(o) = rx.try_recv() {
|
||||
match o {
|
||||
Out::Packet(b) => one(&b),
|
||||
Out::Packets(bs) => bs.iter().for_each(|b| one(b)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The ids of the packets waiting in a channel, in order.
|
||||
fn ids(rx: &Receiver<Out>) -> Vec<i32> {
|
||||
packets(rx).into_iter().map(|(id, _)| id).collect()
|
||||
}
|
||||
|
||||
fn place(hub: &mut Hub, conn: u32) {
|
||||
let mut world = crate::world::World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||
let border = crate::worldborder::WorldBorder::default();
|
||||
@@ -1121,4 +1194,192 @@ mod hub_tests {
|
||||
assert!(hub.guests[i].left_reason.is_empty(), "not kicked");
|
||||
assert_eq!(hub.guests[i].last_seen.tracked_count(), 20);
|
||||
}
|
||||
|
||||
// ---- player-release ------------------------------------------------------
|
||||
|
||||
/// A watcher and a placed player frozen for a hand-over, and the hand-over's id.
|
||||
fn frozen_with_a_watcher() -> (Hub, Receiver<Out>, Receiver<Out>, String) {
|
||||
let mut hub = hub();
|
||||
let (mut watcher, seen) = guest(1, 1, "Watcher");
|
||||
watcher.placed = true;
|
||||
hub.guests.push(watcher);
|
||||
let (mut mover, mine) = guest(2, UUID_N, "Mover");
|
||||
mover.placed = true;
|
||||
hub.guests.push(mover);
|
||||
hub.ticks = 50;
|
||||
let record = super::export_record(&mut hub, 1, true, Default::default()).unwrap();
|
||||
let handover = record["handover"].as_str().unwrap().to_owned();
|
||||
let _ = (ids(&seen), ids(&mine));
|
||||
(hub, seen, mine, handover)
|
||||
}
|
||||
|
||||
fn poll(hub: &mut Hub) -> Vec<crate::lan::hub::HostEvent> {
|
||||
let mut world = crate::world::World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||
let border = crate::worldborder::WorldBorder::default();
|
||||
let mut next = 100;
|
||||
let mut ctx = tick_context(&mut world, &border, &mut next);
|
||||
hub.poll(&mut ctx)
|
||||
}
|
||||
|
||||
fn left_events(hub: &mut Hub) -> Vec<Guest> {
|
||||
poll(hub)
|
||||
.into_iter()
|
||||
.filter_map(|e| match e {
|
||||
crate::lan::hub::HostEvent::Left(g) => Some(*g),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Moved to another server: the others see it go, but nothing is said and nothing saved.
|
||||
#[test]
|
||||
fn a_moved_player_leaves_the_world_without_the_leave_line_and_is_not_saved() {
|
||||
let (mut hub, seen, mine, h) = frozen_with_a_watcher();
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} moved"));
|
||||
assert_eq!(
|
||||
reply,
|
||||
serde_json::json!({"ok": true, "released": "moved", "announced": false})
|
||||
);
|
||||
assert_eq!(hub.guests.len(), 1, "the mover is out of the hub");
|
||||
let told = ids(&seen);
|
||||
assert!(told.contains(&id::PLAYER_INFO_REMOVE), "{told:?}");
|
||||
assert!(told.contains(&id::REMOVE_ENTITIES), "{told:?}");
|
||||
assert!(!told.contains(&id::SYSTEM_CHAT), "no leave line: {told:?}");
|
||||
assert!(matches!(mine.try_recv(), Ok(Out::Disconnect(_))));
|
||||
// The game is told it left, released: it saves no file and no record.
|
||||
let left = left_events(&mut hub);
|
||||
assert_eq!(left.len(), 1);
|
||||
assert!(left[0].released && left[0].placed && left[0].handover.is_none());
|
||||
assert_eq!(left[0].left_reason, "moved to another server");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_moved_player_can_be_announced_as_leaving() {
|
||||
let (mut hub, seen, _mine, h) = frozen_with_a_watcher();
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} moved announce"));
|
||||
assert_eq!(reply["announced"], true);
|
||||
assert!(ids(&seen).contains(&id::SYSTEM_CHAT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_aborted_hand_over_lets_the_player_go_on_as_it_was() {
|
||||
let (mut hub, seen, mine, h) = frozen_with_a_watcher();
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} abort"));
|
||||
assert_eq!(reply, serde_json::json!({"ok": true, "released": "abort"}));
|
||||
assert_eq!(hub.guests.len(), 2);
|
||||
assert!(hub.guests[1].handover.is_none() && hub.guests[1].placed);
|
||||
assert_eq!(ids(&seen), Vec::<i32>::new());
|
||||
assert_eq!(ids(&mine), Vec::<i32>::new());
|
||||
assert!(left_events(&mut hub).is_empty());
|
||||
// The hand-over is over: a late `moved` for it is refused.
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} moved"));
|
||||
assert!(reply["error"].is_string(), "{reply}");
|
||||
assert_eq!(hub.guests.len(), 2);
|
||||
}
|
||||
|
||||
/// A release names the hand-over: one for another (an older one, or one the proxy
|
||||
/// mixed up) changes nothing.
|
||||
#[test]
|
||||
fn a_release_for_another_hand_over_is_refused_and_changes_nothing() {
|
||||
let (mut hub, seen, _mine, h) = frozen_with_a_watcher();
|
||||
let rest: String = h.chars().skip(1).collect();
|
||||
let other = format!("{}{rest}", if h.starts_with('0') { '1' } else { '0' });
|
||||
for how in ["moved", "abort"] {
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {other} {how}"));
|
||||
assert!(
|
||||
reply["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not the player's current hand-over"),
|
||||
"{reply}"
|
||||
);
|
||||
}
|
||||
assert_eq!(hub.guests.len(), 2);
|
||||
assert!(hub.guests[1].handover.is_some(), "still frozen");
|
||||
assert_eq!(ids(&seen), Vec::<i32>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_release_is_refused_for_a_player_who_is_not_frozen_and_changes_nothing() {
|
||||
let (mut hub, seen, _mine, h) = frozen_with_a_watcher();
|
||||
hub.guests[1].handover = None;
|
||||
for how in ["moved", "abort"] {
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} {how}"));
|
||||
assert!(
|
||||
reply["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not in a hand-over"),
|
||||
"{reply}"
|
||||
);
|
||||
}
|
||||
// Not connected at all.
|
||||
let reply = super::release_request(
|
||||
&mut hub,
|
||||
&format!("00000000000000000000000000000001 {h} moved"),
|
||||
);
|
||||
assert!(reply["error"].is_string());
|
||||
assert_eq!(hub.guests.len(), 2);
|
||||
assert_eq!(ids(&seen), Vec::<i32>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_release_must_say_which_hand_over_how_and_nothing_more() {
|
||||
let (mut hub, _seen, _mine, h) = frozen_with_a_watcher();
|
||||
for args in [
|
||||
String::new(),
|
||||
"nonsense moved".to_owned(),
|
||||
UUID.to_owned(),
|
||||
format!("{UUID} moved"),
|
||||
format!("{UUID} {h}"),
|
||||
format!("{UUID} not-an-id moved"),
|
||||
format!("{UUID} {h} later"),
|
||||
format!("{UUID} {h} moved quietly"),
|
||||
format!("{UUID} {h} abort announce"),
|
||||
format!("{UUID} {h} moved announce now"),
|
||||
] {
|
||||
let reply = super::release_request(&mut hub, &args);
|
||||
assert!(reply["error"].is_string(), "{args:?} gave {reply}");
|
||||
}
|
||||
assert!(
|
||||
hub.guests[1].handover.is_some(),
|
||||
"no refusal touched the player"
|
||||
);
|
||||
}
|
||||
|
||||
/// Once the freeze ran out the player played on, and the record the other server has is
|
||||
/// older than it: a late `moved` is refused, and the proxy fails the hand-over.
|
||||
#[test]
|
||||
fn a_release_that_comes_after_the_freeze_ran_out_is_refused() {
|
||||
let (mut hub, seen, mine, h) = frozen_with_a_watcher();
|
||||
let since = hub.guests[1].handover.as_ref().unwrap().since;
|
||||
hub.ticks = since + super::Settings::default().max_freeze_ticks;
|
||||
assert!(poll(&mut hub).iter().any(|e| is_warn(e, "frozen")));
|
||||
assert!(hub.guests[1].handover.is_none());
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} moved"));
|
||||
assert!(
|
||||
reply["error"].as_str().unwrap().contains("freeze ran out"),
|
||||
"{reply}"
|
||||
);
|
||||
assert_eq!(hub.guests.len(), 2, "the player stays");
|
||||
assert!(!ids(&seen).contains(&id::PLAYER_INFO_REMOVE));
|
||||
assert!(!matches!(mine.try_recv(), Ok(Out::Disconnect(_))));
|
||||
}
|
||||
|
||||
/// A frozen player takes no damage, but the void and `/kill` still kill it: its record is
|
||||
/// then not what is left, so `moved` is refused and the next tick ends the hand-over.
|
||||
#[test]
|
||||
fn a_player_who_died_while_frozen_cannot_be_released_as_moved() {
|
||||
let (mut hub, _seen, _mine, h) = frozen_with_a_watcher();
|
||||
hub.guests[1].dead = true;
|
||||
let reply = super::release_request(&mut hub, &format!("{UUID} {h} moved"));
|
||||
assert!(reply["error"].as_str().unwrap().contains("died"), "{reply}");
|
||||
assert_eq!(hub.guests.len(), 2);
|
||||
assert!(
|
||||
poll(&mut hub)
|
||||
.iter()
|
||||
.any(|e| is_warn(e, "died while frozen"))
|
||||
);
|
||||
assert!(hub.guests[1].handover.is_none());
|
||||
}
|
||||
}
|
||||
@@ -346,6 +346,13 @@ pub struct Guest {
|
||||
/// Its login started while a proxy said it would arrive from another
|
||||
/// server, and used that expectation up (`player-expect`).
|
||||
pub expected: bool,
|
||||
/// Leaving for another server (`player-release moved`) without the "left the
|
||||
/// game" line.
|
||||
pub quiet_leave: bool,
|
||||
/// Taken out of the world because a proxy moved it to another server: the game
|
||||
/// saves neither its player file nor its records, since the other server has the
|
||||
/// player now.
|
||||
pub released: bool,
|
||||
/// Root command names the mods took out of this player's command tree
|
||||
/// (`native_player_commands_sent`); their tree is sent without them.
|
||||
pub hidden_commands: Vec<String>,
|
||||
@@ -522,6 +529,8 @@ impl Guest {
|
||||
placed: false,
|
||||
handover: None,
|
||||
expected: j.expected,
|
||||
quiet_leave: false,
|
||||
released: false,
|
||||
hidden_commands: Vec::new(),
|
||||
flying_speed: 0.05,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
|
||||
@@ -1019,10 +1019,18 @@ impl Hub {
|
||||
}
|
||||
let max = transfers.settings.max_freeze_ticks;
|
||||
for g in &mut self.guests {
|
||||
if g.handover
|
||||
.as_ref()
|
||||
.is_some_and(|h| super::arrivals::frozen_too_long(h.since, self.ticks, max))
|
||||
{
|
||||
let Some(h) = &g.handover else {
|
||||
continue;
|
||||
};
|
||||
// A frozen player takes no damage, but the void and `/kill` still kill: its
|
||||
// record is then not what is left of it, and `player-release moved` must fail.
|
||||
if g.dead {
|
||||
g.handover = None;
|
||||
events.push(HostEvent::Warn(format!(
|
||||
"{} died while frozen for a hand-over: the hand-over is over",
|
||||
g.name,
|
||||
)));
|
||||
} else if super::arrivals::frozen_too_long(h.since, self.ticks, max) {
|
||||
g.handover = None;
|
||||
events.push(HostEvent::Warn(format!(
|
||||
"{} was frozen for a hand-over for {max} ticks and nothing finished it: let go",
|
||||
@@ -2037,11 +2045,55 @@ impl Hub {
|
||||
if o.tab_hidden.remove(&g.uuid) {
|
||||
continue;
|
||||
}
|
||||
o.sender.send(left.clone());
|
||||
if !g.quiet_leave {
|
||||
o.sender.send(left.clone());
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Takes the frozen player `uuid` out of the world because a proxy moved it to another
|
||||
/// server, for the hand-over `id`: the others see it go (its entity and its player-list
|
||||
/// entry), but nothing is saved (the other server has the player now) and, unless
|
||||
/// `announce`, the "left the game" line is not sent. The game is told it left, marked
|
||||
/// released, so it saves nothing.
|
||||
///
|
||||
/// Refused when the player is not in that hand-over: another one, or none because the
|
||||
/// freeze ran out (the player has played on since, and the record the other server has is
|
||||
/// older), or a player who died while frozen.
|
||||
pub fn release_moved(
|
||||
&mut self,
|
||||
uuid: u128,
|
||||
id: u128,
|
||||
announce: bool,
|
||||
) -> Result<(), &'static str> {
|
||||
let i = self.handover_of(uuid, id)?;
|
||||
let g = &mut self.guests[i];
|
||||
g.quiet_leave = !announce;
|
||||
g.left_reason = "moved to another server".to_owned();
|
||||
g.sender
|
||||
.disconnect(translatable("multiplayer.disconnect.generic", &[]));
|
||||
let mut gone = self.remove_guest(i, "moved to another server");
|
||||
gone.released = true;
|
||||
gone.handover = None;
|
||||
self.pending_kicks.push(HostEvent::Left(Box::new(gone)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The guest index of the player `uuid` while it is frozen for the hand-over `id`.
|
||||
pub fn handover_of(&self, uuid: u128, id: u128) -> Result<usize, &'static str> {
|
||||
let Some(i) = self.guests.iter().position(|g| g.uuid == uuid) else {
|
||||
return Err("that player is not connected to this server");
|
||||
};
|
||||
let g = &self.guests[i];
|
||||
match &g.handover {
|
||||
_ if g.dead => Err("that player died during the hand-over"),
|
||||
None => Err("that player is not in a hand-over (or its freeze ran out)"),
|
||||
Some(h) if h.id != id => Err("that is not the player's current hand-over"),
|
||||
Some(_) => Ok(i),
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServerTickRateManager.updateStepTicks` / `updateStateToClients`:
|
||||
/// send clock changes before the block events caused by their ticks.
|
||||
pub fn sync_ticking(&mut self, rate: f32, frozen: bool, steps: crate::clock::StepState) {
|
||||
|
||||
@@ -236,6 +236,23 @@ byte for byte, and does not restart the freeze: a proxy that lost the reply asks
|
||||
Without `freeze` the export is a read: `handover` is null and the player goes on. The request is refused for a
|
||||
player who is not connected, has not finished joining, is leaving, is riding, is asleep or is dead.
|
||||
|
||||
While frozen, the player takes no damage (as vanilla's `Abilities.invulnerable`: the void and `/kill` still kill)
|
||||
and picks nothing up (items, arrows, experience orbs), so its record stays what it is until the release. A player
|
||||
who dies while frozen all the same ends the hand-over, with a warning in the log.
|
||||
|
||||
`player-release <uuid> <hand-over> moved [announce]` and `player-release <uuid> <hand-over> abort` end the
|
||||
hand-over, on the source server. Both name the hand-over the export returned. `moved` takes the player out of the
|
||||
world because another server has it now: the others see its entity and its player-list entry go, nothing is written
|
||||
to its player file or its records (the other server has the player), the log and the `leave` event record the
|
||||
departure with the reason `moved to another server`, and the player's connection is closed with a generic
|
||||
disconnect. The "left the game" line is neither sent nor logged unless `announce` is added. Nothing is deleted: the
|
||||
player's file here stays as it was. `abort` lifts the freeze, and the player goes on as if nothing had happened.
|
||||
`moved` replies `{"ok": true, "released": "moved", "announced": false}` and `abort` replies
|
||||
`{"ok": true, "released": "abort"}`. Both are refused for a player who is not in a hand-over, for another hand-over
|
||||
than the player's current one, and for a player who died while frozen. Once the freeze has run out the hand-over is
|
||||
over, and a late `moved` is refused: the player has played on since, and the record the other server has is older.
|
||||
The proxy then fails the hand-over on the target (`player-drop`).
|
||||
|
||||
**The headless engine** comes after perf-ticks-2's work lands: Resources split into shared and per-game, the
|
||||
game built in `Game::new`, the integrated server on its own thread. On that base:
|
||||
|
||||
|
||||
Reference in new issue
Block a user