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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,9 +459,16 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldLog("server_world_state_time_apply")) {
|
if (ShouldLog("server_world_state_time_apply")) {
|
||||||
|
if (parsedGameHour) {
|
||||||
|
LogInfo(std::format(
|
||||||
|
"Applied server time command: {} (from HHmm={}).",
|
||||||
|
command,
|
||||||
|
*a_state.timeHHmm));
|
||||||
|
} else {
|
||||||
LogInfo(std::format("Applied server time command: {}", command));
|
LogInfo(std::format("Applied server time command: {}", command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const bool hasWeatherRequest =
|
const bool hasWeatherRequest =
|
||||||
(a_state.weatherConsoleArg && !a_state.weatherConsoleArg->empty()) ||
|
(a_state.weatherConsoleArg && !a_state.weatherConsoleArg->empty()) ||
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user