Repository navigation
Conversation
GCC 16 no longer provides std::uint64_t through the retained UI's other headers, so Motion.h failed to compile on current Linux toolchains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mber 0.14.000 rewrote guis/hud.gui and guis/hud_strogg.gui in the same commit that moved the payload to v4. A saved GUI's window tree is positional and the payload does not record which definition wrote it, so the v3 decoder parsed v3 HUD window trees with the v4 definitions. Every v0.10-v0.13.2 save desynced in the player HUD after map teardown (idWinStr::ReadFromSaveGame: invalid string length). The exact v3 HUD definitions now ship under guis/savegame/v3/. For v3 payloads both game libraries read those window trees into a private instance of the v3 definition, with every normal check, then move its state, interaction flags and cursor to the live HUD. Build numbers are commit counts: a source archive stamps 0 and a shallow clone 1. The engine build floor and the liquid-field thresholds now apply to v3 only, so such builds restore their own v4 saves. savegame_gui_layout_contract.py pins every serialized openQ4 GUI by payload version. savegame_restore_matrix.py is an opt-in runtime restore check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
themuffinator
left a comment
There was a problem hiding this comment.
The version-specific GUI decoder and format-4 build-number change address real compatibility problems. I ran savegame_v3_contract.py, savegame_pointer_width_safety.py, savegame_corruption_contract.py, savegame_gui_layout_contract.py and validation_hardening.py on 842acc2; all five passed on Windows.
Two issues need correction before merging: the runtime harness can delete an input save when an output directory contains it, and the GUI migration discards persistent HUD window state, including boss-bar visibility. The input-deletion case was reproduced with an isolated disposable fixture and no game launch. The HUD issue is based on tracing the restore, GUI definition and gameplay update paths; I have not run an in-game restore.
CI is also incomplete: four checks passed, twelve were cancelled and one was skipped. Windows x64 exceeded its 60-minute limit while running retained-UI tests; Linux x64 GCC/libdecor exceeded 90 minutes. These timeouts need investigation and completed validation before merge.
| for ( int i = 0; i < savedState.GetNumKeyVals(); i++ ) { | ||
| const idKeyValue *kv = savedState.GetKeyVal( i ); | ||
| ui->SetStateString( kv->GetKey(), kv->GetValue() ); | ||
| } | ||
| ui->SetInteractive( saved->IsInteractive() ); | ||
| ui->SetUniqued( saved->IsUniqued() ); | ||
| ui->SetCursor( saved->CursorX(), saved->CursorY() ); |
There was a problem hiding this comment.
[P2] Preserve persistent HUD window state during the v3 migration
Copying State() does not copy window-local variables restored into saved->desktop. For example, Bossbar::visible defaults to 0 in both live HUD definitions and is set to 1 by showBossBar when idPlayer::StartBossBattle runs. Restoring bossEnemy does not call StartBossBattle again; UpdateHudStats only updates boss_health / emits updateBossBar, which does not restore that visibility. Consequently a v3 save taken during a boss fight loses the boss bar after migration (and the retained HUD also derives its join state from the legacy window). This is persistent state loss beyond the documented animation restart. Transfer compatible persistent presentation variables or explicitly reconstruct that HUD state after restore, in both SP and MP, and cover a save taken during a boss fight.
There was a problem hiding this comment.
Agreed, and fixed in 7185ae2. Copying only State() lost window-local values such as Bossbar::visible.
How it works now. idUserInterface::ReadFromSaveGameWithLayout, appended after the existing vtable slots, parses the saved v3 definition twice in private:
- one copy consumes the v3 window tree with every normal check;
- the other shows what that definition starts with.
Besides the dictionary, flags and cursor, every window value the game had changed moves to the live GUI, wherever a window and variable of the same name and type exist. Variables bound to the GUI state already arrive with the dictionary. Transitions and timeline progress still restart. SP and MP both use it, and savegame_gui_layout_contract.py now requires that.
Boss-fight check. I made a v3 save with the v3-only build 1109 during game/waste's boss fight (trigger target_bossbattle_bar) and restored it on this branch. With developer 1 the log shows:
gui 'guis/hud_strogg.gui': restored saved Bossbar::visible '1' (layout 'guis/savegame/v3/hud_strogg.gui' starts with '0')
gui 'guis/hud_strogg.gui': restored saved p_boss_health_bar::rect '266 40 148.9 50' (layout ... starts with '266 40 0 50')
The post-restore screenshot shows the boss bar, and it matches a boss fight started fresh on this build.
A separate bug this exposed on main. After merging main, every v3 and v4 save, including 0.14.000's own, failed at the player's animator:
idAnimator::Restore: saved joint count 0 does not match model joint count 65 for model 'model_player_marine' on entity 'player1'
The cause is f15a41f, which added a bool cacheValid to idExtrapolate/idInterpolate. jointMod_t (which embeds idInterpolateAccelDecelLinear<idAngles>) and running GUI transitions (<idVec4>) are saved as native images, so each grew by 4 bytes. Compiled sizes for idExtrapolate, the angle interpolator and the vec4 interpolator: 0.14.000 is 64/104/128, main is 68/108/132.
dcddd68 marks the cache stale with a NaN currentTime instead. That restores the original layout and keeps the negative-time fix: InterpolationTest.cpp passes unchanged with GCC and Clang at -O0 and -O2. static_asserts beside both raw writes now require these types to hold only their declared members, and they fail against f15a41f's headers.
…ild-compat # Conflicts: # docs/dev/release-completion.md
main added guis/arena_pause.gui, a multiplayer pause menu no save writes, and 2126a23 edited guis/weapons/machinegun_scope.gui within format 4. That edit only drops matscaley from two windows. It sets the fixed idWindow::matScaley member, which every window writes whether or not the .gui names it, so the saved window tree is unchanged and v3 and v4 saves still decode with the new file. The contract can now record such layout-neutral revisions, each mapped to the pinned layout it keeps and the evidence for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness removed <output-root>/<save name> before staging each save, so a save kept inside that directory was deleted before it was read. It now deletes nothing. Each invocation writes into a new run directory it creates, and before any write it refuses linked saves or companions and any input inside the output root. Companion .txt/.tga files are hashed with the save, and a missing input counts as modified. --self-test checks these guards on disposable files without a client and runs in CI and local validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f15a41f added a bool cache flag to idExtrapolate and idInterpolate. idAnimator saves each jointMod_t, which embeds idInterpolateAccelDecelLinear<idAngles>, and a running GUI transition saves its idInterpolateAccelDecelLinear<idVec4>, both as native images. Each grew by four bytes, so that build could not restore any v3 or v4 save holding a joint modifier, which includes every player: idAnimator::Restore: saved joint count 0 does not match model joint count 65 A NaN cached time now marks the cache stale. NaN never equals a sample time, so negative times, including -1, stay valid and the native interpolation suite passes unchanged (GCC and Clang, -O0 and -O2). Sizes match 0.14.000 again. static_asserts beside both raw writes require these types to hold only their declared members on every ABI; they fail against f15a41f's headers. The joint-count error now names the model and entity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copying only the state dictionary lost window-local values that scripts set. Bossbar::visible is 0 in both HUD definitions and showBossBar sets it to 1, but restoring bossEnemy does not replay StartBossBattle, so a v3 save taken during a boss fight loaded without its boss bar. idUserInterface::ReadFromSaveGameWithLayout, appended after the existing vtable slots, parses the saved definition twice in private: one copy consumes the v3 window tree with every normal check, the other shows what that definition starts with. Besides the dictionary, flags and cursor, every window value the game changed moves to the live GUI where a window and variable of the same name and type exist. GUI-state-bound variables already arrive with the dictionary. Transitions and timeline progress still restart. SP and MP route v3 HUDs through it; the layout contract requires that. A v3 build-1109 save taken in game/waste's boss fight restores Bossbar::visible and the health width, and its screenshot matches a fresh boss fight on this build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
savegame_restore_matrix.py --screenshot captures the restored scene after the settle frames. The savegame policy, reliability notes and release list describe the HUD window-value migration, the interpolator layout regression and its guards, and the new restore results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ration main's 6e13a0f and 49a2710 fix the three restore failures this branch addressed, with a different design: HUD revisions under guis/savecompat/ chosen by layout build and a marker window, an interpolator mirror for payloads before build 1582, and release builds mapped to their tagged commit counts. Their versions of the shared files are taken as they are. This branch's v3 HUD copies, version-keyed HUD table, v4 build-number changes and interpolator layout restore are dropped: the last would misread every save main has written since 1582. Kept, because main lacks them: - idUserInterface::ReadFromSaveGameWithLayout. main's revision reader copied only the state dictionary, so a v3 save taken in game/waste's boss fight restored without its boss bar. It now goes through the engine migration, which carries Bossbar::visible and 38 other changed window values. - savegame_gui_layout_contract.py, rewritten for main's revision table: it pins every serialized GUI and each guis/savecompat/ copy (header aside, the v0.13.2 blobs), checks each marker window exists only in the current definition, and requires the engine migration. - savegame_restore_matrix.py and its --self-test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
themuffinator
left a comment
There was a problem hiding this comment.
Re-reviewed the current revision against the consolidated roadmap (Q4-05/Q4-07/UI-09). The destructive output-directory cleanup has been removed; the five available path/retention self-tests pass on Windows here (the two symlink cases were skipped because symlink creation was unavailable). I am resolving that original input-deletion thread.
The new migration carries window values, but still loses their saved evaluation state; the inline finding below needs correction and a behavioral regression that advances GUI evaluation after restore. Keep the reported Linux boss-fight evidence, but a copied value or a single initial capture does not establish persistence through subsequent updates. No in-game restore was run in this review.
CI run 39138909016 stops at the unrelated missing Mock::Authored accessor in ui_number_clipboard.py; #190 addresses that failure. Complete validation against current main after the test-harness fixes. Also keep the self-test's temporary files under the repository's .tmp directory rather than the system temporary directory.
| } | ||
| common->DPrintf( "gui '%s': restored saved %s::%s '%s' (layout '%s' starts with '%s')\n", | ||
| source.c_str(), windowName, savedVars[ i ].name, savedVar->c_str(), layoutPath, initialVar->c_str() ); | ||
| liveVar->Set( savedVar->c_str() ); |
There was a problem hiding this comment.
[P2] Preserve the saved evaluation flag as well as the variable value
Script_Set calls SetEval(false), and that flag is serialized by the idWinVar readers. This transfer only calls Set(), which leaves the live variable's newly parsed evaluation flag intact. idRegister::GetFromRegs then remains allowed to overwrite a scripted value with its expression on a subsequent GUI update. The value-only equality filter above also skips an explicit override whose saved value equals the initial value even though its evaluation ownership changed. Transfer compatible evaluation state (including that equal-value case) without freezing values that should still follow expressions, and exercise restore followed by changing GUI state and several evaluation/redraw ticks.
There was a problem hiding this comment.
Fixed in 035b340. The migration now carries a value whenever its value or its eval flag differs from the saved definition's, and sets the live flag to the saved one (liveVar->SetEval( savedVar->GetEval() )). So:
- a scripted override stays frozen, including the equal-value case;
- a variable that was still following its expression keeps doing so.
Behavioural regression. game/mcc_1's main() sends hideHud at once, which sets p_health::visible/p_armor::visible to 0 against their expressions ("gui::mp" == 0) and ("gui::player_armor" > 0 && …). I made a v3 save there with build 1109 after give armor, then restored it and:
- read the live HUD with a new cheat command,
hudGet <window::variable>(SP and MP); - changed GUI state with
give armor; - ran 300 frames;
- read it again.
| right after restore | after armor change + 300 frames | |
|---|---|---|
| value-only transfer | p_health::visible = 1, p_armor::visible = 1 |
1, 1 |
| this commit | 0, 0 |
0, 0 |
The developer 1 log shows the equal-value case being carried: p_armor::visible '0' eval 0 (layout … starts with '0' eval 1). savegame_restore_matrix.py now takes --after-load commands and --expect-log patterns, and that's how this runs as an assertion:
--after-load 'give armor' --after-load 'wait 300' --after-load 'echo AFTER_UPDATES' \
--after-load 'hudGet p_health::visible' --after-load 'hudGet p_armor::visible' \
--expect-log 'AFTER_UPDATES\s*\nhudGet p_health::visible = 0\s*\nhudGet p_armor::visible = 0'
The same build keeps Bossbar::visible = 1 after 300 frames on the game/waste boss-fight save, and still restores the 0.14.000 (build 1574) and v0.13.2 (build 878) saves. savegame_gui_layout_contract.py now requires the flag transfer and fails if it's removed. The harness self-test keeps its scratch files under the repository's .tmp/.
One gotcha I hit while building fixtures: a save made after using the console script command can't be restored, because the program records a pseudo-file named console. So the fixture uses give armor instead.
…ild-compat # Conflicts: # docs/dev/release-completion.md
A script `set` stores its value and clears the variable's eval flag, so
idRegister::GetFromRegs stops rewriting it from the window's expression; the
flag is saved with the value. The revision migration copied only values, and
only where they differed from the definition, so the live variable kept its
freshly parsed eval and the next GUI update recomputed it. game/mcc_1's script
hides the HUD at once (set "p_health::visible" "0" against ("gui::mp" == 0));
restoring a v3 save from there showed both panels again on the first update,
p_armor because its saved 0 equals its definition's.
The migration now carries a value whenever its value or eval flag differs
from the saved definition's, and sets the live flag to the saved one. A
variable that was still following its expression keeps doing so.
hudGet <window::variable> (SP and MP, cheat) prints the local player's HUD
value after evaluation, and savegame_restore_matrix.py gains --after-load
commands and --expect-log patterns. With them the v3 mcc_1 save is restored,
armor changed and 300 frames run: value-only transfer reads 1 for both panels,
this reads 0 throughout. The boss-fight save keeps Bossbar::visible = 1 after
300 frames, and the 0.14.000 and v0.13.2 saves still restore.
savegame_gui_layout_contract.py requires the flag transfer. The harness
self-test keeps its scratch files under the repository's .tmp/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI status for
Passing on this PR: Validation Script Smoke (which includes the save-compatibility contracts and the restore harness self-test), Linux x64 GCC and Clang no-libdecor, Linux ARM64 GCC libdecor and Clang no-libdecor, Linux x64 ASan+UBSan, and the ARM64 cross build. The timeouts look like runner time rather than this change. sync-window-ops passed on #189 with the same base, and #191 roughly halves the Python check time, which should keep most of these jobs under the limit. The review fix for the expression-override ( |
Status after merging
mainmain's 6e13a0f and 49a2710 fixed the three restore failures this PR started with, using a different design:guis/savecompat/;I merged
main, took its versions of every shared file, and dropped this branch's competing fixes. Its interpolator layout restore in particular would misread every savemainhas written since build 1582. What remains is whatmaindoesn't have.1. Keep HUD window state when reading an earlier GUI revision
ReadUserInterfaceRevisioncopied only the state dictionary. Window-local values that scripts set were lost:showBossBarsetsBossBar::visible, and restoring the boss doesn't replayStartBossBattle, so an older save taken in a boss fight loaded without its boss bar. This is the gap raised in review.idUserInterface::ReadFromSaveGameWithLayoutis appended after the existing vtable slots, so older modules keep their slot layout. It parses the saved definition twice in private:The state dictionary, flags and cursor move to the live GUI, along with every window value the game changed, wherever a window and variable of the same name and type exist. Running transitions restart. SP and MP both call it.
Same v3 save, taken by build 1109 during
game/waste's boss fight (trigger target_bossbattle_bar), restored on Linux x64:mainrestored saved Bossbar::visible '1' (layout 'guis/savecompat/hud_strogg_r1.gui' starts with '0'), plusp_boss_health_bar::rectand 37 more2.
savegame_gui_layout_contract.pyRewritten for
main's revision table:arena_pause.guiis new and never serialized);2126a232'smatscaleyremoval recorded as layout-neutral;OPENQ4_SAVEGAME_GUI_REVISIONSmust match, and eachguis/savecompat/copy must equal what38bdc687^shipped once its header is stripped;On
mainit fails only on that last check. A simulated HUD edit fails it until the edit is classified.3.
savegame_restore_matrix.pyAn opt-in runtime restore check. It never deletes anything, and before writing it refuses linked inputs and inputs inside its output root (the earlier review comment).
--screenshotcaptures the restored scene, and--self-testruns in CI.On this branch it restores my v0.13.2 build-878 save, the 0.14.000 build-1574 save and the boss-fight save, and leaves the inputs unchanged.
mainrestores the same three.Also
#include <cstdint>insrc/ui/retained/Motion.hfor GCC 16.Tests
savegame_corruption_contract,savegame_pointer_width_safety,savegame_v3_contract,savegame_gui_layout_contract,gui_transition_operand_contract,ui_manager_lifecycleandvalidation_hardeningpass, as doessavegame_restore_matrix.py --self-test(7/7).mainitself atui_number_clipboard.py. I'm opening a separate PR for that.Not verified
Windows, macOS and ARM64 restores; gameplay state beyond the HUD.
🤖 Generated with Claude Code