Skip to content

Commit 9ed2524

Browse files
sharpianclaude
andcommitted
fix: strip unmodeled item components + full view-distance on join + autocomplete perms
Three tracked follow-ups (Codex reviews + wiring audit). - items: into_validated now STRIPS unmodeled in-range components instead of keeping them as a trusted Opaque passthrough, so a hostile creative slot's malformed item_name/use_remainder bytes can never be re-emitted as trusted clientbound data. Component representation fields made private (OpaqueComponent newtype + checked ctors/accessors). (trust boundary) - chunk streaming: a standing, non-moving player now fills to the FULL advertised view distance - the load diff runs every tick from the current center (center-out, bounded 16 cols/tick) instead of only on a position packet. No more small island surrounded by void. - commands: the Brigadier graph is permission-FILTERED (predicate, not just level), dead-end parent literals are pruned with rebuilt child indices, and tab-complete ranges use char units (non-ASCII safe). (autocomplete invariants) Gate green: fmt + clippy -D warnings + xtask generate --check + 999 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8f56ee5 commit 9ed2524

10 files changed

Lines changed: 588 additions & 172 deletions

File tree

‎server/app/src/command.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ mod tests {
141141
let tree = build_command_tree();
142142

143143
// An operator sees both commands and the typed `mode` argument.
144-
let op = tree.to_brigadier(4);
144+
let op = tree.to_brigadier(4, &|_| true);
145145
let op_names: Vec<&str> = op.nodes().iter().filter_map(|n| n.name()).collect();
146146
assert!(op_names.contains(&SPAWN_COMMAND));
147147
assert!(op_names.contains(&GAMEMODE_COMMAND));
@@ -157,13 +157,16 @@ mod tests {
157157
));
158158

159159
// A level-0 player sees only `/spawn`; the gated `/gamemode` subtree is gone.
160-
let member = tree.to_brigadier(0);
160+
let member = tree.to_brigadier(0, &|_| true);
161161
let member_names: Vec<&str> = member.nodes().iter().filter_map(|n| n.name()).collect();
162162
assert!(member_names.contains(&SPAWN_COMMAND));
163163
assert!(!member_names.contains(&GAMEMODE_COMMAND));
164164
assert!(!member_names.contains(&"mode"));
165165

166166
// The encoded body the join kit sends therefore differs by level.
167-
assert!(tree.encode_commands_body(4).len() > tree.encode_commands_body(0).len());
167+
assert!(
168+
tree.encode_commands_body(4, &|_| true).len()
169+
> tree.encode_commands_body(0, &|_| true).len()
170+
);
168171
}
169172
}

‎server/app/src/config.rs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ const DEFAULT_SPAWN_PROTECT_RADIUS: i32 = 0;
5050
/// without a wall-clock wait.
5151
const DEFAULT_KEEP_ALIVE_INTERVAL_MS: u64 = 10_000;
5252

53+
/// Default chunk-stream pump interval, in milliseconds.
54+
///
55+
/// How often a standing player's view is advanced toward the full advertised view
56+
/// distance without waiting for a movement packet. One server tick (50 ms at the
57+
/// 20 TPS default) keeps the backlog draining promptly while the per-pump load cap
58+
/// bounds the burst.
59+
const DEFAULT_CHUNK_STREAM_INTERVAL_MS: u64 = 50;
60+
5361
/// Default permission level granted to a non-operator player.
5462
///
5563
/// Zero is the vanilla "ordinary player" tier: it satisfies no operator gate, so
@@ -94,6 +102,12 @@ pub struct AppConfig {
94102
pub spawn_protect_bypass: Vec<String>,
95103
/// Interval between clientbound play-phase Keep Alive pings.
96104
pub keep_alive_interval: Duration,
105+
/// How often a standing player's view is pumped toward the full advertised
106+
/// view distance. The chunk stream otherwise only advances on a movement
107+
/// packet, so without this a non-moving joiner would never receive chunks past
108+
/// the initial spawn batch. Each pump is bounded by the per-update load cap, so
109+
/// a short interval drains the backlog promptly without flooding the socket.
110+
pub chunk_stream_interval: Duration,
97111
/// Names of players granted operator status (permission level 4), letting
98112
/// them run operator-gated commands such as `/gamemode`. Everyone else acts
99113
/// at [`default_permission_level`](Self::default_permission_level).
@@ -154,6 +168,7 @@ impl Default for AppConfig {
154168
spawn_protect_radius: DEFAULT_SPAWN_PROTECT_RADIUS,
155169
spawn_protect_bypass: Vec::new(),
156170
keep_alive_interval: Duration::from_millis(DEFAULT_KEEP_ALIVE_INTERVAL_MS),
171+
chunk_stream_interval: Duration::from_millis(DEFAULT_CHUNK_STREAM_INTERVAL_MS),
157172
ops: Vec::new(),
158173
default_permission_level: DEFAULT_PERMISSION_LEVEL,
159174
// None = in-memory, keeping `AppConfig::default()` deterministic and
@@ -197,6 +212,8 @@ struct RawConfig {
197212
spawn_protect_bypass: Option<Vec<String>>,
198213
/// Override for [`AppConfig::keep_alive_interval`], expressed in milliseconds.
199214
keep_alive_interval_ms: Option<u64>,
215+
/// Override for [`AppConfig::chunk_stream_interval`], expressed in milliseconds.
216+
chunk_stream_interval_ms: Option<u64>,
200217
/// Override for [`AppConfig::ops`].
201218
ops: Option<Vec<String>>,
202219
/// Override for [`AppConfig::default_permission_level`].
@@ -256,6 +273,9 @@ impl RawConfig {
256273
keep_alive_interval: self
257274
.keep_alive_interval_ms
258275
.map_or(defaults.keep_alive_interval, Duration::from_millis),
276+
chunk_stream_interval: self
277+
.chunk_stream_interval_ms
278+
.map_or(defaults.chunk_stream_interval, Duration::from_millis),
259279
ops: self.ops.unwrap_or(defaults.ops),
260280
default_permission_level: self
261281
.default_permission_level
@@ -291,6 +311,7 @@ mod tests {
291311
spawn_protect_radius = 12
292312
spawn_protect_bypass = ["Admin", "Mod"]
293313
keep_alive_interval_ms = 250
314+
chunk_stream_interval_ms = 33
294315
ops = ["Admin"]
295316
default_permission_level = 1
296317
world_dir = "/srv/world"
@@ -309,11 +330,20 @@ mod tests {
309330
assert_eq!(parsed.spawn_protect_radius, 12);
310331
assert_eq!(parsed.spawn_protect_bypass, vec!["Admin", "Mod"]);
311332
assert_eq!(parsed.keep_alive_interval, Duration::from_millis(250));
333+
assert_eq!(parsed.chunk_stream_interval, Duration::from_millis(33));
312334
assert_eq!(parsed.ops, vec!["Admin"]);
313335
assert_eq!(parsed.default_permission_level, 1);
314336
assert_eq!(parsed.world_dir, Some(PathBuf::from("/srv/world")));
315337
}
316338

339+
#[test]
340+
fn chunk_stream_interval_defaults_to_one_tick() {
341+
let parsed = AppConfig::from_toml_str("").expect("empty config is valid");
342+
// Defaults to 50 ms (one tick at 20 TPS), so a standing joiner's view is
343+
// pumped toward full view distance promptly.
344+
assert_eq!(parsed.chunk_stream_interval, Duration::from_millis(50));
345+
}
346+
317347
#[test]
318348
fn world_dir_defaults_to_in_memory() {
319349
let parsed = AppConfig::from_toml_str("").expect("empty config is valid");

0 commit comments

Comments
 (0)