Convert HHmm to Fallout game-hour float

Add parsing and application of HHmm time strings as Fallout game-hour floats so GUI time presets apply correctly. Introduce ParseHHmmToGameHour in plugin/src/F4TWorldStateSync.cpp and use it when building the console command (formats as set gamehour to <float> with fallback to the raw HHmm string if parsing fails). Improve logging to indicate when a parsed HHmm was used. Update protocol/server-world-state.md, changelog.md, and docs/dev-log.md to document the behavior and rationale.
This commit is contained in:
2026-06-24 12:34:18 +12:00
parent 1493e68349
commit c4466718a3
4 changed files with 87 additions and 5 deletions
+1
View File
@@ -24,6 +24,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- Plugin now defers server weather commands until player assignment is available, preventing initial GUI clicks from being dropped right after connect. - 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 ...". - Replaced invalid default GUI weather IDs with valid Fallout 4 weather form IDs so `fw <id>` no longer fails with "Invalid weather ...".
- Corrected `Clear` preset ID typo from `0002852a` to valid `0002b52a` (`CommonwealthClear`). - Corrected `Clear` preset ID typo from `0002852a` to valid `0002b52a` (`CommonwealthClear`).
- Time apply now converts `timeHHmm` to Fallout game-hour float before executing `set gamehour`, fixing 2200-style values applying incorrectly.
### Changed ### Changed
- 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. - 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.
+30
View File
@@ -9,6 +9,36 @@ failed experiments, successful tests, and next steps.
--- ---
## 2026-06-24 - Convert `HHmm` Time to Game-Hour Float
### Summary
Fixed server GUI time presets applying the wrong time in Fallout 4 by converting `HHmm` values (such as `2200`) to game-hour float values before running `set gamehour`.
### Files Changed
- `plugin/src/F4TWorldStateSync.cpp`
- `protocol/server-world-state.md`
- `changelog.md`
### Details
- `serverWorldState.timeHHmm` is now parsed as `HHmm` on the client.
- Parsed times are applied as float hours:
- `2200` -> `22.0`
- `1830` -> `18.5`
- If parsing fails, plugin falls back to the original raw command for compatibility.
### Testing
- Not re-tested in-game in this session.
- User reported `Night (2200)` previously set daytime while console showed `set gamehour to 2200`.
- Recommended: test `0600`, `1200`, `1900`, and `2200` from GUI and verify resulting in-game time.
### Known Issues
- None currently known.
### Next Steps
- Rebuild plugin and verify each time preset maps correctly in-game.
---
## 2026-06-24 - Replace Invalid GUI Weather IDs ## 2026-06-24 - Replace Invalid GUI Weather IDs
### Summary ### Summary
+53 -2
View File
@@ -195,6 +195,47 @@ namespace
return value; return value;
} }
std::optional<float> ParseHHmmToGameHour(std::string_view a_hhmm)
{
if (a_hhmm.empty() || a_hhmm.size() > 4) {
return std::nullopt;
}
std::string digits;
digits.reserve(4);
for (const auto ch : a_hhmm) {
if (ch < '0' || ch > '9') {
return std::nullopt;
}
digits.push_back(ch);
}
while (digits.size() < 4) {
digits.insert(digits.begin(), '0');
}
int hours = 0;
int minutes = 0;
{
const auto* begin = digits.data();
const auto* split = begin + 2;
const auto* end = begin + 4;
const auto hourResult = std::from_chars(begin, split, hours);
const auto minuteResult = std::from_chars(split, end, minutes);
if (hourResult.ec != std::errc{} || hourResult.ptr != split ||
minuteResult.ec != std::errc{} || minuteResult.ptr != end) {
return std::nullopt;
}
}
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return std::nullopt;
}
return static_cast<float>(hours) + static_cast<float>(minutes) / 60.0F;
}
bool ForceHostWorldStateBroadcast(std::optional<std::uint32_t> a_forcedWeatherFormId, bool a_sendTime) bool ForceHostWorldStateBroadcast(std::optional<std::uint32_t> a_forcedWeatherFormId, bool a_sendTime)
{ {
if (!F4T::WorldStateSync::IsWorldStateHost() || !F4T::Networking::IsConnectedToServer()) { if (!F4T::WorldStateSync::IsWorldStateHost() || !F4T::Networking::IsConnectedToServer()) {
@@ -394,7 +435,10 @@ namespace
void ApplyServerWorldState(const F4T::WorldStateSync::PendingServerWorldState& a_state) void ApplyServerWorldState(const F4T::WorldStateSync::PendingServerWorldState& a_state)
{ {
if (a_state.timeHHmm && !a_state.timeHHmm->empty()) { if (a_state.timeHHmm && !a_state.timeHHmm->empty()) {
const auto command = std::format("set gamehour to {}", *a_state.timeHHmm); const auto parsedGameHour = ParseHHmmToGameHour(*a_state.timeHHmm);
const auto command = parsedGameHour
? std::format("set gamehour to {:.4f}", *parsedGameHour)
: std::format("set gamehour to {}", *a_state.timeHHmm);
RE::Console::ExecuteCommand(command.c_str()); RE::Console::ExecuteCommand(command.c_str());
float gameHour = 0.0F; float gameHour = 0.0F;
@@ -415,7 +459,14 @@ namespace
} }
if (ShouldLog("server_world_state_time_apply")) { if (ShouldLog("server_world_state_time_apply")) {
LogInfo(std::format("Applied server time command: {}", command)); if (parsedGameHour) {
LogInfo(std::format(
"Applied server time command: {} (from HHmm={}).",
command,
*a_state.timeHHmm));
} else {
LogInfo(std::format("Applied server time command: {}", command));
}
} }
} }
+3 -3
View File
@@ -4,7 +4,7 @@ Server-authoritative weather and time control from the dev server GUI.
## Authority ## Authority
- **Time** (`timeHHmm`): sent to **all** clients; each runs `set gamehour to HHmm`. - **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 **player 1** runs `fw <8-digit-id>`. Player 1 should also be the world-state host so the usual `worldState` relay updates other clients.
## Packet Format ## Packet Format
@@ -35,14 +35,14 @@ Weather (all clients receive; only player 1 acts):
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| `type` | string | yes | Must be `"serverWorldState"` | | `type` | string | yes | Must be `"serverWorldState"` |
| `timeHHmm` | string | no | 24-hour clock value for `set gamehour to HHmm` | | `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 player 1 (8-digit lowercase hex) |
| `weatherFormId` | string | no | Relay form ID used in host `worldState` broadcast | | `weatherFormId` | string | no | Relay form ID used in host `worldState` broadcast |
| `serverTime` | number (double) | no | Relay timestamp | | `serverTime` | number (double) | no | Relay timestamp |
## Client Apply Rules ## Client Apply Rules
- **Time:** all clients apply `set gamehour to HHmm` on the game thread. - **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 client with assigned `playerId == 1` runs `fw <weatherConsoleArg>`.
- Weather commands are scheduled on the F4SE game thread from the network receive path. - 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 player 1 runs `fw`, the world-state host immediately relays `worldState` with `weatherFormId`.