Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/commit-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ jobs:
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/rigid_body_recovery_diagnostics.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_gui_layout_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/savegame_v3_contract.py \
tools/tests/sdl3_input_parity.py \
Expand Down Expand Up @@ -498,6 +499,7 @@ jobs:
tools/tests/vk_display_color_mapping.py \
tools/tests/vk_display_color_mapping_shader_pin.py \
tools/tests/vk_display_color_mapping_capture.py \
tools/tests/savegame_restore_matrix.py \
tools/tests/vk_temporal_resolve_shader_pin.py \
tools/tests/vscode_fast_build.py
bash -n tools/build/meson_setup.sh
Expand Down Expand Up @@ -715,6 +717,7 @@ jobs:
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/rigid_body_recovery_diagnostics.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_gui_layout_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/savegame_v3_contract.py
python tools/tests/sdl3_input_parity.py
Expand Down Expand Up @@ -856,6 +859,7 @@ jobs:
python tools/tests/vk_display_color_mapping.py
python tools/tests/vk_display_color_mapping_shader_pin.py
python tools/tests/vk_display_color_mapping_capture.py --self-test
python tools/tests/savegame_restore_matrix.py --self-test
python tools/tests/vk_temporal_resolve_shader_pin.py
python tools/tests/vscode_fast_build.py

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/push-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ jobs:
tools/tests/renderer_vulkan_world_interaction_compatibility.py \
tools/tests/rigid_body_recovery_diagnostics.py \
tools/tests/savegame_corruption_contract.py \
tools/tests/savegame_gui_layout_contract.py \
tools/tests/savegame_pointer_width_safety.py \
tools/tests/savegame_v3_contract.py \
tools/tests/sdl3_input_parity.py \
Expand Down Expand Up @@ -498,6 +499,7 @@ jobs:
tools/tests/vk_display_color_mapping.py \
tools/tests/vk_display_color_mapping_shader_pin.py \
tools/tests/vk_display_color_mapping_capture.py \
tools/tests/savegame_restore_matrix.py \
tools/tests/vk_temporal_resolve_shader_pin.py \
tools/tests/vscode_fast_build.py
bash -n tools/build/meson_setup.sh
Expand Down Expand Up @@ -715,6 +717,7 @@ jobs:
python tools/tests/renderer_vulkan_world_interaction_compatibility.py
python tools/tests/rigid_body_recovery_diagnostics.py
python tools/tests/savegame_corruption_contract.py
python tools/tests/savegame_gui_layout_contract.py
python tools/tests/savegame_pointer_width_safety.py
python tools/tests/savegame_v3_contract.py
python tools/tests/sdl3_input_parity.py
Expand Down Expand Up @@ -856,6 +859,7 @@ jobs:
python tools/tests/vk_display_color_mapping.py
python tools/tests/vk_display_color_mapping_shader_pin.py
python tools/tests/vk_display_color_mapping_capture.py --self-test
python tools/tests/savegame_restore_matrix.py --self-test
python tools/tests/vk_temporal_resolve_shader_pin.py
python tools/tests/vscode_fast_build.py

Expand Down
15 changes: 15 additions & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ completed release material here and publish curated notes in `releases/`.

## Post-0.14.000 corrections

- [ ] Keep HUD window state when an older save's HUD is read through its earlier
definition. Copying only the GUI state dictionary lost values scripts set on
windows: a save taken in a boss fight loaded without its boss bar, because
`showBossBar` sets `BossBar::visible` and restoring the boss does not replay it.
`idUserInterface::ReadFromSaveGameWithLayout` now carries every window value the
game changed from the saved definition, with whether it still follows its
expression, as well as the state, flags and cursor. A HUD a level script hid
(as `game/mcc_1` does from its start) stays hidden after loading an older save.
- [ ] Add `savegame_gui_layout_contract.py`, which pins every serialized openQ4 GUI
and the `guis/savecompat/` revisions, and `savegame_restore_matrix.py`, an opt-in
runtime restore check whose `--self-test` runs in CI.

Candidate save release wording: Older saves taken during a boss fight now load
with the boss's health bar on screen.

- [x] Complete the stage-one baseline implementation and audit: use a shared
high-resolution presentation phase for each SP/MP draw and precise OpenXR
smooth-turn deltas; restore imported game regression coverage; reconcile
Expand Down
15 changes: 12 additions & 3 deletions docs/dev/savegame-compatibility-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,18 @@ only the current revision has:
| `guis/hud_strogg.gui` | 1562 | `guis/savecompat/hud_strogg_r1.gui` | `hit_indicator_reduced` |

A record from a payload whose layout build predates the revision, and whose blob
does not name that window, is read into a copy built from the saved definition.
Its state dictionary carries over to the current GUI, whose windows start from
their definition; the copy is then freed. The window check matters because a
does not name that window, is read through `idUserInterface::ReadFromSaveGameWithLayout`.
The engine parses the saved definition twice in private: one copy consumes the
record with every normal check, the other shows what that definition starts with.
The state dictionary, interaction flags and cursor carry over to the current GUI,
and so does every window value the game had changed from the saved definition
wherever the current GUI has a window and variable of the same name and type: a
boss bar a script made visible, and its health width, survive. Each value carries
its evaluation flag too. A script `set` clears that flag so the variable stops
following its expression, and the saved flag decides, even where the value equals
the definition's: a HUD panel a script hid stays hidden through later GUI updates,
while a variable still following its expression keeps doing so. Running
transitions and timeline progress restart. Both copies are then freed. The window check matters because a
build of uncommitted work can carry the edit under an earlier build number (the
build-1561 saves on hand do). GUI records hold no sync markers, so the next one
bounds the search. The saved definitions are copies of `38bdc6872^` and must not
Expand Down
18 changes: 17 additions & 1 deletion docs/dev/savegame-reliability.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,23 @@ comes from an incompatible version.", and loaded the 0.12.0 checkpoint into
Air Defense Bunker with the HUD restored through its earlier definition.

The harness and fixtures live under `.tmp/savecompat/` (`load_saves.py`,
`menu_check.py`, `restamp_save.py`, `make_save.py`). No save written by the
`menu_check.py`, `restamp_save.py`, `make_save.py`). The in-repo
`tools/tests/savegame_restore_matrix.py` restores a set of saves with a staged
client, optionally restamped to other build numbers, never deletes anything and
refuses inputs inside its output directory; `--self-test` runs in CI.
`tools/tests/savegame_gui_layout_contract.py` pins every serialized openQ4 GUI and
the `guis/savecompat/` revisions, so editing one fails until it is classified.

GUI-revision window state, Linux x64, 2026-10-18. A v3 save from build 1109 on
`game/mcc_1`, whose map script sends `hideHud` (`set "p_health::visible" "0"` and
likewise for armor, overriding their expressions), was restored, armor was then
changed and 300 frames run, with `hudGet` reading the live HUD before and after
(`savegame_restore_matrix.py --after-load ... --expect-log ...`). Copying values
alone left both panels visible (`= 1`) from the first update, because their
evaluation flags stayed set and `p_armor`'s saved `0` equals its definition's.
Carrying the flags keeps both at `0` throughout. The same build keeps
`Bossbar::visible = 1` after 300 frames on the `game/waste` boss-fight save, and
restores the 0.14.000 build-1574 and v0.13.2 build-878 saves. No save written by the
0.6.95, 0.8.0, 0.10.0, 0.11.0 or 0.13.0 binaries themselves was available, and
Linux and macOS old-save restores are untested.

Expand Down
27 changes: 11 additions & 16 deletions src/game/gamesys/SaveGame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2076,26 +2076,21 @@ idRestoreGame::ReadUserInterfaceRevision

A GUI record written for an earlier revision of its file (SaveGameReleases.h)
follows that revision's structure, so it is read into a copy built from that
revision. Its state carries over to the current GUI, whose windows start from
their definition.
revision. Its state carries over to the current GUI, and so does every window
value the game had changed from that revision -- a boss bar a script made
visible, for example -- where the current GUI has the same window and variable.
Animation progress is not carried over.
================
*/
void idRestoreGame::ReadUserInterfaceRevision( idUserInterface *ui, const openQ4SaveGameGuiRevision_t &revision ) {
idUserInterface *saved = uiManager->FindGui( revision.savedDefinition, true, true, true );
if ( saved == NULL ) {
Error( "idRestoreGame::ReadUserInterface: '%s' needs its earlier definition '%s'", revision.name, revision.savedDefinition );
}
if ( saved->ReadFromSaveGame( file ) == false ) {
uiManager->DeAlloc( saved );
Error( "idSaveGame::ReadUserInterface: ui failed to read properly\n" );
}
const idDict &state = saved->State();
for ( int i = 0; i < state.GetNumKeyVals(); i++ ) {
const idKeyValue *kv = state.GetKeyVal( i );
ui->SetStateString( kv->GetKey(), kv->GetValue() );
const int offset = file->Tell();
int restoredWindowValues = 0;
if ( !ui->ReadFromSaveGameWithLayout( file, revision.savedDefinition, restoredWindowValues ) ) {
Error( "idRestoreGame::ReadUserInterface: '%s' at offset %d failed to read with its earlier definition '%s'",
revision.name, offset, revision.savedDefinition );
}
uiManager->DeAlloc( saved );
common->DPrintf( "Restored '%s' from a save written for its earlier definition\n", revision.name );
common->DPrintf( "Restored '%s' from a save written for its earlier definition (%d changed window values)\n",
revision.name, restoredWindowValues );
}

// RAVEN BEGIN
Expand Down
23 changes: 23 additions & 0 deletions src/game/gamesys/SysCmds.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,28 @@ void Cmd_GetViewpos_f( const idCmdArgs &args ) {
gameLocal.Printf( "(%s) %.1f\n", origin.ToString(), angles.yaw );
}

/*
==================
Cmd_HudGet_f

Prints a window variable of the local player's own HUD, such as
p_armor::visible, after any expression evaluation it is subject to.
==================
*/
static void Cmd_HudGet_f( const idCmdArgs &args ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( args.Argc() != 2 || player == NULL || player->hud == NULL ) {
gameLocal.Printf( "usage: hudGet <window::variable> with a local player HUD\n" );
return;
}
idStr value;
if ( !player->hud->GetPresentationValue( args.Argv( 1 ), value ) ) {
gameLocal.Printf( "hudGet: unknown HUD variable '%s'\n", args.Argv( 1 ) );
return;
}
gameLocal.Printf( "hudGet %s = %s\n", args.Argv( 1 ), value.c_str() );
}

/*
==================
Cmd_Viewpos_f
Expand Down Expand Up @@ -4359,6 +4381,7 @@ void idGameLocal::InitConsoleCommands( void ) {
cmdSystem->AddCommand( "kill", Cmd_Kill_f, CMD_FL_GAME, "kills the player" );
cmdSystem->AddCommand( "where", Cmd_GetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints the current view position" );
cmdSystem->AddCommand( "getviewpos", Cmd_GetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints the current view position" );
cmdSystem->AddCommand( "hudGet", Cmd_HudGet_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints a window variable of the local player's HUD" );
cmdSystem->AddCommand( "viewpos", Cmd_Viewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints the current view origin and angles" );
cmdSystem->AddCommand( "setviewpos", Cmd_SetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "sets the current view position" );
cmdSystem->AddCommand( "benchmarkViewSweep", Cmd_BenchmarkViewSweep_f, CMD_FL_GAME|CMD_FL_CHEAT, "pans the view a given number of degrees over a given number of milliseconds" );
Expand Down
27 changes: 11 additions & 16 deletions src/mpgame/gamesys/SaveGame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2089,26 +2089,21 @@ idRestoreGame::ReadUserInterfaceRevision

A GUI record written for an earlier revision of its file (SaveGameReleases.h)
follows that revision's structure, so it is read into a copy built from that
revision. Its state carries over to the current GUI, whose windows start from
their definition.
revision. Its state carries over to the current GUI, and so does every window
value the game had changed from that revision -- a boss bar a script made
visible, for example -- where the current GUI has the same window and variable.
Animation progress is not carried over.
================
*/
void idRestoreGame::ReadUserInterfaceRevision( idUserInterface *ui, const openQ4SaveGameGuiRevision_t &revision ) {
idUserInterface *saved = uiManager->FindGui( revision.savedDefinition, true, true, true );
if ( saved == NULL ) {
Error( "idRestoreGame::ReadUserInterface: '%s' needs its earlier definition '%s'", revision.name, revision.savedDefinition );
}
if ( saved->ReadFromSaveGame( file ) == false ) {
uiManager->DeAlloc( saved );
Error( "idSaveGame::ReadUserInterface: ui failed to read properly\n" );
}
const idDict &state = saved->State();
for ( int i = 0; i < state.GetNumKeyVals(); i++ ) {
const idKeyValue *kv = state.GetKeyVal( i );
ui->SetStateString( kv->GetKey(), kv->GetValue() );
const int offset = file->Tell();
int restoredWindowValues = 0;
if ( !ui->ReadFromSaveGameWithLayout( file, revision.savedDefinition, restoredWindowValues ) ) {
Error( "idRestoreGame::ReadUserInterface: '%s' at offset %d failed to read with its earlier definition '%s'",
revision.name, offset, revision.savedDefinition );
}
uiManager->DeAlloc( saved );
common->DPrintf( "Restored '%s' from a save written for its earlier definition\n", revision.name );
common->DPrintf( "Restored '%s' from a save written for its earlier definition (%d changed window values)\n",
revision.name, restoredWindowValues );
}

// RAVEN BEGIN
Expand Down
23 changes: 23 additions & 0 deletions src/mpgame/gamesys/SysCmds.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,28 @@ static void Cmd_Kick_f( const idCmdArgs &args ) {
cmdSystem->BufferCommandText( CMD_EXEC_NOW, va( "kick %d\n", player->entityNumber ) );
}

/*
==================
Cmd_HudGet_f

Prints a window variable of the local player's own HUD, such as
p_armor::visible, after any expression evaluation it is subject to.
==================
*/
static void Cmd_HudGet_f( const idCmdArgs &args ) {
idPlayer *player = gameLocal.GetLocalPlayer();
if ( args.Argc() != 2 || player == NULL || player->hud == NULL ) {
gameLocal.Printf( "usage: hudGet <window::variable> with a local player HUD\n" );
return;
}
idStr value;
if ( !player->hud->GetPresentationValue( args.Argv( 1 ), value ) ) {
gameLocal.Printf( "hudGet: unknown HUD variable '%s'\n", args.Argv( 1 ) );
return;
}
gameLocal.Printf( "hudGet %s = %s\n", args.Argv( 1 ), value.c_str() );
}

/*
==================
Cmd_GetViewpos_f
Expand Down Expand Up @@ -4049,6 +4071,7 @@ void idGameLocal::InitConsoleCommands( void ) {
cmdSystem->AddCommand( "kill", Cmd_Kill_f, CMD_FL_GAME, "kills the player" );
cmdSystem->AddCommand( "where", Cmd_GetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints the current view position" );
cmdSystem->AddCommand( "getviewpos", Cmd_GetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints the current view position" );
cmdSystem->AddCommand( "hudGet", Cmd_HudGet_f, CMD_FL_GAME|CMD_FL_CHEAT, "prints a window variable of the local player's HUD" );
cmdSystem->AddCommand( "setviewpos", Cmd_SetViewpos_f, CMD_FL_GAME|CMD_FL_CHEAT, "sets the current view position" );
cmdSystem->AddCommand( "gotolevelshot", Cmd_GotoLevelshot_f, CMD_FL_GAME|CMD_FL_CHEAT, "moves the view to the levelshot position" );
cmdSystem->AddCommand( "teleport", Cmd_Teleport_f, CMD_FL_GAME|CMD_FL_CHEAT, "teleports the player to an entity location", idGameLocal::ArgCompletion_EntityName );
Expand Down
Loading
Loading