Add live force game mode management setting

Co-Authored-By: Codex (GPT-6) <[email protected]>
This commit is contained in:
TiggerTakkyTTTandCodex committed 2026-11-02 17:32:43 +07:00
1 parent 9c7f42d9b7
commit 138cbc1254
7 files changed
+103 -6

No files matched your search

@@ -637,6 +637,8 @@ struct ServerState {
max_players: usize,
/// The mutable `gamemode` server property (`DedicatedServer.gameMode`).
game_mode: i32,
/// The mutable `force-gamemode` server property (`DedicatedServer.forceGameMode`).
force_game_mode: bool,
}
/// The dedicated-only commands' replies vanilla sends with `broadcast`
@@ -1005,6 +1007,7 @@ impl Game<'_> {
broadcast_rcon: props.broadcast_rcon_to_ops,
op_level: props.op_permission_level,
game_mode: props.game_mode,
force_game_mode: props.force_game_mode,
report: None,
view: (
props.view_distance.clamp(2, 32) as u32,
@@ -89,6 +89,22 @@ impl Game<'_> {
self.set_default_game_mode(state.game_mode);
return json!({ "value": GAME_MODES[state.game_mode as usize] });
}
"serversettings/force_game_mode" => {
return json!({ "value": state.force_game_mode });
}
"serversettings/force_game_mode/set" => {
let Some(force) = body["value"].as_bool() else {
return json!({ "error": "force game mode must be a boolean" });
};
rpc(format!(
"Update force game mode from {} to {}",
state.force_game_mode, force
));
self.store_property("force-gamemode", force.to_string());
state.force_game_mode = force;
self.set_force_game_mode(force);
return json!({ "value": state.force_game_mode });
}
"serversettings/difficulty" => {
return json!({ "value": DIFFICULTIES[self.level.difficulty_id() as usize] });
}
+10 -3
View File
@@ -1695,6 +1695,15 @@ impl Game<'_> {
}
}
/// Apply the default mode to connected players when forcing it is enabled.
pub(super) fn set_force_game_mode(&mut self, force: bool) {
if let Some(host) = self.lan.as_mut() {
host.settings.force_game_mode = force;
host.hub.config.force_game_mode = force;
enforce_guest_game_mode(host, &self.world, self.collision);
}
}
/// New command access and game mode forcing settings of the open world.
pub(super) fn lan_apply_settings(&mut self, guest_commands: bool, force: bool) {
if let Some(server) = &self.integrated {
@@ -1706,11 +1715,9 @@ impl Game<'_> {
let effective = guest_commands && (!self.serving() || self.allow_commands());
if let Some(host) = self.lan.as_mut() {
host.settings.guest_commands = guest_commands;
host.settings.force_game_mode = force;
host.hub.config.guest_commands = effective;
host.hub.config.force_game_mode = force;
enforce_guest_game_mode(host, &self.world, self.collision);
}
self.set_force_game_mode(force);
self.resend_commands_filtered();
}
@@ -59,6 +59,15 @@ pub fn call(shared: &Shared, client: u32, short: &str, argument: &Value) -> Resu
.ok_or_else(|| invalid_params("Not a string"))?,
),
),
"serversettings/force_game_mode" => game.force_game_mode(client, None),
"serversettings/force_game_mode/set" => game.force_game_mode(
client,
Some(
argument
.as_bool()
.ok_or_else(|| invalid_params(format!("Not a boolean: {argument}")))?,
),
),
"allowlist" => Ok(lists.allowlist()),
"bans" => Ok(lists.bans()),
"ip_bans" => Ok(lists.ip_bans()),
@@ -290,6 +299,29 @@ struct Game<'a> {
}
impl Game<'_> {
/// `MinecraftServerSettingsServiceImpl.forceGameMode` and `setForceGameMode`.
fn force_game_mode(&self, client: u32, value: Option<bool>) -> Result<Value, Failure> {
let mut request = json!({ "method": "serversettings/force_game_mode" });
if let Some(value) = value {
request["method"] = json!("serversettings/force_game_mode/set");
request["value"] = json!(value);
}
let reply = self.ask(&format!("management {client} {request}"))?;
if let Some(error) = reply["error"].as_str() {
return Err((INTERNAL_ERROR, Some(error.to_owned())));
}
if let Some(value) = value {
crate::config::update(
&self.shared.dir,
"properties",
"force-gamemode",
toml::Value::Boolean(value),
)
.map_err(|e| (INTERNAL_ERROR, Some(e)))?;
}
Ok(reply["value"].clone())
}
/// `MinecraftServerSettingsServiceImpl.getGameMode` and `setGameMode`.
fn game_mode(&self, client: u32, value: Option<&str>) -> Result<Value, Failure> {
let mut request = json!({ "method": "serversettings/game_mode" });
+1 -1
View File
@@ -87,7 +87,7 @@ MinecraftServer.fillSystemReport skip:debug system report for crash and debug o
MinecraftServer.setDifficulty port crates/horizon-client/src/game/commands.rs::Game::cmd_difficulty gap: player: LAN guests are not sent ChangeDifficulty when /difficulty changes; they get it on join, respawn or dimension change
MinecraftServer.updateMobSpawningFlags inline crates/horizon-client/src/game/natural_spawning.rs::Game::spawn_natural rules are read at each spawn; no cached spawn flag
MinecraftServer.sendDifficultyUpdate inline crates/horizon-client/src/lan/hub.rs::Hub::place_new_player gap: player: sent when a guest joins, respawns or changes dimension; see setDifficulty
MinecraftServer.enforceGameTypeForPlayers inline crates/horizon-client/src/game/lan.rs::Game::lan_apply_settings gap: player: guests already online keep their mode when force changes; the default applies at join (lan_guest_joined)
MinecraftServer.enforceGameTypeForPlayers inline crates/horizon-client/src/game/lan.rs::enforce_guest_game_mode applies the default mode to online players when force game mode is enabled
MinecraftServer.executeIfPossible skip:java rejects tasks during shutdown
MinecraftServer.reloadResources port crates/horizon-client/src/game/functions.rs::Game::cmd_reload gap: minor: data packs only, via reload_datapacks
MinecraftServer.configurePackRepository port crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: picks the enabled packs and adds new ones; no feature-flag check and no safe mode
1 DelegatingOps.DelegateListBuilder.build skip:java forwards to the original ListBuilder; codec plumbing
87 MinecraftServer.setDifficulty port crates/horizon-client/src/game/commands.rs::Game::cmd_difficulty gap: player: LAN guests are not sent ChangeDifficulty when /difficulty changes; they get it on join, respawn or dimension change
88 MinecraftServer.updateMobSpawningFlags inline crates/horizon-client/src/game/natural_spawning.rs::Game::spawn_natural rules are read at each spawn; no cached spawn flag
89 MinecraftServer.sendDifficultyUpdate inline crates/horizon-client/src/lan/hub.rs::Hub::place_new_player gap: player: sent when a guest joins, respawns or changes dimension; see setDifficulty
90 MinecraftServer.enforceGameTypeForPlayers inline crates/horizon-client/src/game/lan.rs::Game::lan_apply_settings crates/horizon-client/src/game/lan.rs::enforce_guest_game_mode gap: player: guests already online keep their mode when force changes; the default applies at join (lan_guest_joined) applies the default mode to online players when force game mode is enabled
91 MinecraftServer.executeIfPossible skip:java rejects tasks during shutdown
92 MinecraftServer.reloadResources port crates/horizon-client/src/game/functions.rs::Game::cmd_reload gap: minor: data packs only, via reload_datapacks
93 MinecraftServer.configurePackRepository port crates/horizon-client/src/game/functions.rs::Game::reload_datapacks gap: minor: picks the enabled packs and adds new ones; no feature-flag check and no safe mode
+2 -1
View File
@@ -23,7 +23,8 @@ DedicatedServer.convertOldUsers skip:other one-time import of pre-1.7 .txt user
DedicatedServer.runCommand port crates/horizon-server/src/rcon.rs::run_on_game gap: minor: runs as the console (`console <line>` on the control endpoint), not as an Rcon source
DedicatedServer.setEntityBroadcastRangePercentage skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
DedicatedServer.createTextFilterForPlayer skip:service external chat filter service; Horizon has no text filter
DedicatedServer.setForceGameMode skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
DedicatedServer.setForceGameMode port crates/horizon-client/src/game/dedicated/management.rs::Game::management stores force-gamemode, updates the host and applies the default mode to online players
MinecraftServerSettingsServiceImpl.setForceGameMode port crates/horizon-server/src/management_methods.rs::Game::force_game_mode forwards the live setting to the game and persists it in server.toml
DedicatedServer.setGameMode port crates/horizon-client/src/game/lan.rs::Game::set_default_game_mode the /defaultgamemode command; gap: minor: the value is not written back to server.properties
DedicatedServer.setAcceptsTransfers skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
DedicatedServer.setPauseWhenEmptySeconds skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
1 DedicatedServer.readCodeOfConducts port crates/horizon-client/src/lan/code_of_conduct.rs::read with enable-code-of-conduct the folder's files are read at startup (game/dedicated.rs::run_dedicated_inner) and handed to a joining player, whose client shows the prompt (game/code_of_conduct.rs)
23 DedicatedServer.runCommand port crates/horizon-server/src/rcon.rs::run_on_game gap: minor: runs as the console (`console <line>` on the control endpoint), not as an Rcon source
24 DedicatedServer.setEntityBroadcastRangePercentage skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
25 DedicatedServer.createTextFilterForPlayer skip:service external chat filter service; Horizon has no text filter
26 DedicatedServer.setForceGameMode skip:server port crates/horizon-client/src/game/dedicated/management.rs::Game::management JSON-RPC management API only (ServerSettingsService); Horizon has no such API stores force-gamemode, updates the host and applies the default mode to online players
27 MinecraftServerSettingsServiceImpl.setForceGameMode port crates/horizon-server/src/management_methods.rs::Game::force_game_mode forwards the live setting to the game and persists it in server.toml
28 DedicatedServer.setGameMode port crates/horizon-client/src/game/lan.rs::Game::set_default_game_mode the /defaultgamemode command; gap: minor: the value is not written back to server.properties
29 DedicatedServer.setAcceptsTransfers skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
30 DedicatedServer.setPauseWhenEmptySeconds skip:server JSON-RPC management API only (ServerSettingsService); Horizon has no such API
+39 -1
View File
@@ -16,7 +16,7 @@ sent the same things over TLS, and every answer is compared exactly:
vanilla's data generator), `server/status`, the four lists, `players`, kicking nobody, `server/save` and
`server/system_message` with no one online, the four lists changed (`add`, `remove`, `set`,
`clear`; with no profile service, players named resolve to offline players on both), the live server
message, difficulty and default game mode read and changed (including their saved properties), and
message, difficulty, default game mode and forced game mode read and changed (including saved properties), and
`server/stop`;
- the TLS version each server agrees on.
@@ -138,6 +138,15 @@ MESSAGES = [
'{"jsonrpc":"2.0","id":52,"method":"minecraft:serversettings/game_mode"}',
'{"jsonrpc":"2.0","id":53,"method":"minecraft:serversettings/game_mode/set","params":["spectator"]}',
'{"jsonrpc":"2.0","id":54,"method":"minecraft:serversettings/game_mode"}',
'{"jsonrpc":"2.0","id":55,"method":"minecraft:serversettings/force_game_mode"}',
'{"jsonrpc":"2.0","id":56,"method":"minecraft:serversettings/force_game_mode/set","params":[true]}',
'{"jsonrpc":"2.0","id":57,"method":"minecraft:serversettings/force_game_mode"}',
'{"jsonrpc":"2.0","id":58,"method":"minecraft:serversettings/force_game_mode/set","params":[false]}',
'{"jsonrpc":"2.0","id":59,"method":"minecraft:serversettings/force_game_mode"}',
'{"jsonrpc":"2.0","id":60,"method":"minecraft:serversettings/force_game_mode/set","params":[true]}',
'{"jsonrpc":"2.0","id":61,"method":"minecraft:serversettings/force_game_mode"}',
'{"jsonrpc":"2.0","id":62,"method":"minecraft:serversettings/force_game_mode/set","params":["yes"]}',
'{"jsonrpc":"2.0","id":63,"method":"minecraft:serversettings/force_game_mode"}',
]
@@ -360,12 +369,32 @@ def main():
sides[side]['saved_game_mode'] = next(
(line.partition('=')[2] for line in properties.splitlines()
if line.startswith('gamemode=')), None)
sides[side]['saved_force_game_mode'] = next(
(line.partition('=')[2] for line in properties.splitlines()
if line.startswith('force-gamemode=')), None)
if side == 'horizon':
world = (folder / 'server.toml').read_text().partition('[world]')[2].split('\n[', 1)[0]
sides[side]['saved_config_difficulty'] = 'difficulty = "peaceful"' in world
sides[side]['saved_config_game_mode'] = 'game_mode = "spectator"' in world
sides[side]['saved_config_force_game_mode'] = 'force-gamemode = true' in (
folder / 'server.toml').read_text()
finally:
server.stop()
restarted = Server(command, folder, build / f'{side}-restart.log')
try:
restarted.wait_for(DONE, 600)
ws = Socket(management, context, [f'Authorization: Bearer {SECRET}'])
try:
ws.send('{"jsonrpc":"2.0","id":64,"method":"minecraft:serversettings/force_game_mode"}')
while True:
reply = normal(ws.receive(timeout=60))
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
break
sides[side]['restarted_force_game_mode'] = reply
finally:
ws.sock.close()
finally:
restarted.stop()
vanilla, horizon = sides['vanilla'], sides['horizon']
failures = []
for key in ('no secret', 'wrong secret', 'origin not allowed', 'subprotocol', 'tls', 'stop'):
@@ -382,7 +411,16 @@ def main():
failures.append(f"saved game mode: vanilla {vanilla['saved_game_mode']!r}, Horizon {horizon['saved_game_mode']!r}")
if not horizon['saved_config_game_mode']:
failures.append('Horizon did not save spectator game mode in server.toml')
if vanilla['saved_force_game_mode'] != horizon['saved_force_game_mode'] or vanilla['saved_force_game_mode'] != 'true':
failures.append(f"saved forced game mode: vanilla {vanilla['saved_force_game_mode']!r}, Horizon {horizon['saved_force_game_mode']!r}")
if not horizon['saved_config_force_game_mode']:
failures.append('Horizon did not save forced game mode in server.toml')
if vanilla['restarted_force_game_mode'] != horizon['restarted_force_game_mode'] or \
vanilla['restarted_force_game_mode'] != {'jsonrpc': '2.0', 'id': 64, 'result': True}:
failures.append(f"restarted forced game mode: vanilla {vanilla['restarted_force_game_mode']!r}, Horizon {horizon['restarted_force_game_mode']!r}")
(build / 'report.json').write_text(json.dumps(sides, indent=1))
if failures:
sys.exit(f'{NAME}: {len(failures)} differences (logs in {build}):\n' + '\n'.join(failures))
report = lab.LOCAL / 'datagen/out/reports/json-rpc-api-schema.json'
discovered = horizon['answers'][MESSAGES.index('{"jsonrpc":"2.0","id":12,"method":"rpc.discover"}')]
if json.loads(report.read_text()) != discovered.get('result'):