Use world-state host for server weather actions

Stop hardcoding player 1 for server-initiated weather/time and instead use the current world-state host. Plugin code now defers weather commands until GetWorldStateHostPlayerId() is available, uses IsWorldStateHost() to gate execution, logs local/host IDs, and requeues when host assignment is missing. Relay logic now attempts ForceHostWorldStateBroadcast and logs if relay fails. Updated protocol docs, dev server UI tooltips, presets comment, changelog, and dev log to reflect world-state host terminology and behavior. This ensures weather/time control survives host reassignment and avoids dropped commands on reconnect/restart.
This commit is contained in:
2026-06-25 10:48:18 +12:00
parent c4466718a3
commit 77751c69f7
7 changed files with 70 additions and 21 deletions
+5 -3
View File
@@ -14,12 +14,13 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- **Weather / Time** tab in `server/dev_server_app.py` with preset buttons for `fw` weather form IDs and `set gamehour to HHmm` time values.
- `server/world_state_presets.py` with shared weather and time preset definitions.
- `protocol/server-world-state.md` documenting the new packet type.
- Event-driven world time resync via optional `timeSync` field on `worldState` packets; host (client 1) pushes time after menus, loading screens, save/load, wait/sleep, and other time freezes.
- Event-driven world time resync via optional `timeSync` field on `worldState` packets; the current world-state host pushes time after menus, loading screens, save/load, wait/sleep, and other time freezes.
- `timeSync` tracking in `server/fake_client.py` host world-state snapshot output.
### Fixed
- Server GUI weather now runs `fw <8-digit-form-id>` on player 1 only (not editor names), scheduled on the game thread.
- Weather `serverWorldState` packets are broadcast to all clients so player 1's game instance always receives them.
- Server GUI weather now runs `fw <8-digit-form-id>` on the current `worldStateHostPlayerId` client (not hardcoded to player 1), scheduled on the game thread.
- Weather `serverWorldState` handling now defers until world-state host assignment is known, then skips only non-host clients.
- Weather `serverWorldState` packets are broadcast to all clients so the designated host receives them even after host reassignment.
- Server restart now resets player/host assignment so the first client is `playerId` 1 again, preventing GUI weather from being ignored after restart.
- Plugin now defers server weather commands until player assignment is available, preventing initial GUI clicks from being dropped right after connect.
- Replaced invalid default GUI weather IDs with valid Fallout 4 weather form IDs so `fw <id>` no longer fails with "Invalid weather ...".
@@ -30,6 +31,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- Weather presets store the exact `fw` console argument (8-digit hex); edit `server/world_state_presets.py` only if a preset ID fails in your console.
- Updated protocol weather examples/default preset table to match the new valid Fallout 4 preset IDs.
- World time no longer re-applies on every `worldState` heartbeat; weather heartbeats continue at ~1 Hz while time stays in sync during normal gameplay.
- Updated Weather/Time UI and protocol wording to use world-state host terminology instead of hardcoded player-1 wording.
- Reformatted `changelog.md` to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+33
View File
@@ -9,6 +9,39 @@ failed experiments, successful tests, and next steps.
---
## 2026-06-25 - Weather Host Reassignment Uses `worldStateHostPlayerId`
### Summary
Fixed server weather control after host/client reassignment by removing the hardcoded `playerId == 1` weather gate and using current world-state host ownership instead.
### Files Changed
- `plugin/src/F4TWorldStateSync.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- `serverWorldState` weather apply now defers until both local player assignment and `worldStateHostPlayerId` are available.
- Weather apply now runs only when the local client is the current world-state host (`IsWorldStateHost()`), not when `playerId == 1`.
- Host weather execution logs now include local and host player IDs, and host relay uses the same host authority check path.
### Testing
- Not re-tested in-game in this session.
- Recommended manual checks:
- Start server and connect two clients.
- Confirm the initial host can change weather/time from server GUI.
- Disconnect the original host; verify server logs host reassignment.
- On remaining client (now host), change weather/time again.
- Verify outgoing host world-state fields are sent and server relays them.
- Verify remote proxy/world-state result matches expected weather/time in game.
### Known Issues
- None currently known.
### Next Steps
- Rebuild plugin and run a two-client disconnect/reassign session while monitoring `CommonwealthOnline.log` for host reassignment and weather relay logs.
---
## 2026-06-24 - Convert `HHmm` Time to Game-Hour Float
### Summary
+21 -8
View File
@@ -486,9 +486,21 @@ namespace
return;
}
if (*assignedPlayerId != 1U) {
const auto hostPlayerId = F4T::WorldStateSync::GetWorldStateHostPlayerId();
if (!hostPlayerId) {
RequeueServerWorldState(a_state);
if (ShouldLog("server_world_state_weather_defer_host")) {
LogInfo("Deferred server weather command until world-state host assignment is available.");
}
return;
}
if (!F4T::WorldStateSync::IsWorldStateHost()) {
if (ShouldLog("server_world_state_weather_skip")) {
LogInfo("Ignored server weather command on non-player-1 client.");
LogInfo(std::format(
"Ignored server weather command on non-host client (local={}, host={}).",
*assignedPlayerId,
*hostPlayerId));
}
return;
}
@@ -505,7 +517,10 @@ namespace
RE::Console::ExecuteCommand(command.c_str());
if (ShouldLog("server_world_state_weather_apply")) {
LogInfo(std::format("Player 1 executed server weather command: {}", command));
LogInfo(std::format(
"World-state host (player {}) executed server weather command: {}",
*assignedPlayerId,
command));
}
std::optional<std::uint32_t> forcedWeatherFormId;
@@ -515,11 +530,9 @@ namespace
forcedWeatherFormId = parsed;
}
if (F4T::WorldStateSync::IsWorldStateHost()) {
ForceHostWorldStateBroadcast(forcedWeatherFormId, false);
} else if (ShouldLog("server_world_state_weather_no_relay")) {
LogWarning(
"Player 1 applied server weather but is not world-state host; other clients may not sync.");
if (!ForceHostWorldStateBroadcast(forcedWeatherFormId, false) &&
ShouldLog("server_world_state_weather_no_relay")) {
LogWarning("Could not relay host world-state after server weather command.");
}
}
}
+5 -5
View File
@@ -5,7 +5,7 @@ Server-authoritative weather and time control from the dev server GUI.
## Authority
- **Time** (`timeHHmm`): sent to **all** clients; each converts `HHmm` to Fallout game-hour float (for example `2200 -> 22.0`, `1830 -> 18.5`) before applying.
- **Weather** (`weatherConsoleArg`): sent to **all** clients, but only **player 1** runs `fw <8-digit-id>`. Player 1 should also be the world-state host so the usual `worldState` relay updates other clients.
- **Weather** (`weatherConsoleArg`): sent to **all** clients, but only the current **world-state host** runs `fw <8-digit-id>`. The host then relays usual `worldState` updates to other clients.
## Packet Format
@@ -19,7 +19,7 @@ Time-only (all clients):
}
```
Weather (all clients receive; only player 1 acts):
Weather (all clients receive; only world-state host acts):
```json
{
@@ -36,16 +36,16 @@ Weather (all clients receive; only player 1 acts):
|-------|------|----------|-------------|
| `type` | string | yes | Must be `"serverWorldState"` |
| `timeHHmm` | string | no | 24-hour clock value (HHmm) converted client-side to game-hour float for `set gamehour to <float>` |
| `weatherConsoleArg` | string | no | Exact argument for `fw` on player 1 (8-digit lowercase hex) |
| `weatherConsoleArg` | string | no | Exact argument for `fw` on the current world-state host (8-digit lowercase hex) |
| `weatherFormId` | string | no | Relay form ID used in host `worldState` broadcast |
| `serverTime` | number (double) | no | Relay timestamp |
## Client Apply Rules
- **Time:** all clients convert `timeHHmm` to game-hour float and apply `set gamehour to <float>` on the game thread.
- **Weather:** only the client with assigned `playerId == 1` runs `fw <weatherConsoleArg>`.
- **Weather:** only the current world-state host runs `fw <weatherConsoleArg>`.
- Weather commands are scheduled on the F4SE game thread from the network receive path.
- After player 1 runs `fw`, the world-state host immediately relays `worldState` with `weatherFormId`.
- After the host runs `fw`, it immediately relays `worldState` with `weatherFormId`.
Edit `server/world_state_presets.py` so each preset uses the exact `fw` ID that works in your console (for example `0000015e`).
+3 -3
View File
@@ -187,7 +187,7 @@ class DevServerWindow(QMainWindow):
weather_layout = QGridLayout(weather_group)
for index, (label, fw_console_arg) in enumerate(WEATHER_PRESETS):
button = QPushButton(label)
button.setToolTip(f"Runs `fw {fw_console_arg}` on player 1.")
button.setToolTip(f"Runs `fw {fw_console_arg}` on the current world-state host.")
button.clicked.connect(
lambda _checked=False, console_arg=fw_console_arg: self._set_server_weather(console_arg)
)
@@ -205,8 +205,8 @@ class DevServerWindow(QMainWindow):
layout.addWidget(time_group)
hint = QLabel(
"Time is sent to all clients. Weather runs `fw <8-digit-id>` on player 1 only, "
"then player 1 relays weather to other clients. Edit preset IDs in "
"Time is sent to all clients. Weather runs `fw <8-digit-id>` on the current world-state host only, "
"then the host relays weather to other clients. Edit preset IDs in "
"server/world_state_presets.py to match what works in your console."
)
hint.setWordWrap(True)
+1 -1
View File
@@ -207,7 +207,7 @@ class FalloutTogetherServer:
self._started_at = time.time()
# Each server run is a fresh session for player/host assignment.
# Without this reset, restarts can leave stale host IDs and player IDs
# > 1, which breaks weather commands gated to player 1.
# > 1, which breaks weather authority handoff to the active host.
self._next_player_id = 1
self._world_state_host_player_id = None
+2 -1
View File
@@ -3,7 +3,8 @@
from __future__ import annotations
# Each preset is (label, fw_console_arg).
# `fw_console_arg` is passed directly to the in-game `fw` command on player 1
# `fw_console_arg` is passed directly to the in-game `fw` command on the
# current world-state host client
# (8-digit hex form ID, not an editor name like CommonwealthRain).
WEATHER_PRESETS: list[tuple[str, str]] = [
("Clear", "0002b52a"),