From 00ca365e7aba2afe94d3785babc84b985908b211 Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 17 Jun 2026 10:28:28 +0200 Subject: [PATCH 1/2] Fix #847: Reset faceanimtime during fast load to prevent stuck pain face When taking damage, cl.faceanimtime is set to cl.time + 0.2 to show the pain face animation. During a fast load, cl.time is reset to the save time, but cl.faceanimtime was not reset. This caused the pain face to remain visible if the save was made during the 0.2s animation window. Fix: Reset cl.faceanimtime to 0 during fast load, consistent with how other client state (dlights, beams, particles) is reset. --- Quake/host_cmd.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Quake/host_cmd.c b/Quake/host_cmd.c index 159a6add6..6cfd70dbc 100644 --- a/Quake/host_cmd.c +++ b/Quake/host_cmd.c @@ -1644,6 +1644,7 @@ static void Host_Loadgame_f (void) memset (cl_dlights, 0, sizeof (cl_dlights)); memset (cl_temp_entities, 0, sizeof (cl_temp_entities)); memset (cl_beams, 0, sizeof (cl_beams)); + cl.faceanimtime = 0.0; V_ResetBlend (); Fog_ResetFade (); R_ClearParticles (); From 6038f27124a10ff58fdc3f926e6e6344167226a6 Mon Sep 17 00:00:00 2001 From: Adrian Date: Sun, 28 Jun 2026 15:52:51 +0200 Subject: [PATCH 2/2] Fix #847: Clear player entity damage fields during fastload to prevent stuck pain face The faceanimtime reset alone was insufficient because the server re-sends a stale svc_damage on the next frame (from dmg_take/dmg_save restored by the savefile). V_ParseDamage then overwrites faceanimtime using the old cl.time (before svc_time resets it), causing the pain face to get stuck until cl.time catches up -- potentially thousands of seconds. Fix by zeroing dmg_take/dmg_save on the player entity (edict 1) in the fastload cleanup block, preventing the stale damage message from being sent. Also adds a test_faceanim console command for programmatic verification. Usage: load a map, type 'test_faceanim run', observe PASS/FAIL. --- Misc/test_faceanim.cfg | 20 +++++++++++ Quake/host_cmd.c | 71 ++++++++++++++++++++++++++++++++++++++ test_fastload_faceanim.bat | 11 ++++++ 3 files changed, 102 insertions(+) create mode 100644 Misc/test_faceanim.cfg create mode 100644 test_fastload_faceanim.bat diff --git a/Misc/test_faceanim.cfg b/Misc/test_faceanim.cfg new file mode 100644 index 000000000..0703defd6 --- /dev/null +++ b/Misc/test_faceanim.cfg @@ -0,0 +1,20 @@ +// Test for issue #847: fastload face animation stuck +// Automates: set damage, save, fastload, verify faceanimtime + +echo "=== Test: fastload face animation (issue #847) ===" + +// Step 1: Start a map +map start + +// Step 2: Set damage on player and save +test_faceanim run + +// Step 3: Wait for save to complete, then fastload +wait +wait +fastload _test_faceanim + +// Step 4: Wait for load, then check result +wait +wait +test_faceanim check diff --git a/Quake/host_cmd.c b/Quake/host_cmd.c index 6cfd70dbc..168cddab4 100644 --- a/Quake/host_cmd.c +++ b/Quake/host_cmd.c @@ -1645,6 +1645,14 @@ static void Host_Loadgame_f (void) memset (cl_temp_entities, 0, sizeof (cl_temp_entities)); memset (cl_beams, 0, sizeof (cl_beams)); cl.faceanimtime = 0.0; + // Clear pending damage on the player entity so the server doesn't + // re-send a stale svc_damage that would overwrite faceanimtime + // with a value based on the old cl.time (before svc_time resets it). + { + edict_t *pent = EDICT_NUM (1); + pent->v.dmg_take = 0; + pent->v.dmg_save = 0; + } V_ResetBlend (); Fog_ResetFade (); R_ClearParticles (); @@ -2612,6 +2620,67 @@ DEMO LOOP CONTROL =============================================================================== */ +/* +================== +Test_FaceAnim_f + +Debug command for issue #847: tests that faceanimtime is properly +reset during fastload and doesn't get stuck by a stale svc_damage. +================== +*/ +static void Test_FaceAnim_f (void) +{ + edict_t *pent; + + if (Cmd_Argc () < 2) + { + Con_Printf ("test_faceanim usage:\n"); + Con_Printf (" test_faceanim run - set damage, save game, fastload, check\n"); + return; + } + + if (!sv.active || svs.maxclients != 1 || cls.signon != SIGNONS) + { + if (cls.state == ca_dedicated) + return; + Cbuf_InsertText ("wait\ntest_faceanim run\n"); + return; + } + + pent = svs.clients->edict; + + if (!strcmp (Cmd_Argv (1), "run")) + { + Con_Printf ("test_faceanim: setting dmg_take=50 on player...\n"); + pent->v.dmg_take = 50; + pent->v.dmg_save = 0; + Con_Printf ("test_faceanim: scheduling save -> wait -> fastload -> wait -> check\n"); + // Cbuf_InsertText prepends, so insert in REVERSE execution order. + // Desired execution: save -> wait10 -> fastload -> wait10 -> result + Cbuf_InsertText ("test_faceanim result\n"); + Cbuf_InsertText ("wait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\n"); + Cbuf_InsertText ("fastload _test_faceanim\n"); + Cbuf_InsertText ("wait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\nwait\n"); + Cbuf_InsertText ("save _test_faceanim\n"); + return; + } + + if (!strcmp (Cmd_Argv (1), "result")) + { + Con_Printf ("test_faceanim: cl.time = %f, cl.faceanimtime = %f\n", cl.time, cl.faceanimtime); + Con_Printf ("test_faceanim: dmg_take = %f, dmg_save = %f\n", pent->v.dmg_take, pent->v.dmg_save); + + if (cl.faceanimtime <= cl.time) + Con_Printf ("test_faceanim: PASS - face is not stuck (faceanimtime <= cl.time)\n"); + else + Con_Printf ("test_faceanim: FAIL - face is STUCK (faceanimtime = %f > cl.time = %f, delta = %f)\n", + cl.faceanimtime, cl.time, cl.faceanimtime - cl.time); + return; + } + + Con_Printf ("test_faceanim: unknown subcommand '%s'\n", Cmd_Argv (1)); +} + /* ================== Host_Startdemos_f @@ -2745,6 +2814,8 @@ void Host_InitCommands (void) Cmd_AddCommand ("save", Host_Savegame_f); Cmd_AddCommand ("give", Host_Give_f); + Cmd_AddCommand ("test_faceanim", Test_FaceAnim_f); + Cmd_AddCommand ("startdemos", Host_Startdemos_f); Cmd_AddCommand ("demos", Host_Demos_f); Cmd_AddCommand ("stopdemo", Host_Stopdemo_f); diff --git a/test_fastload_faceanim.bat b/test_fastload_faceanim.bat new file mode 100644 index 000000000..a051279e5 --- /dev/null +++ b/test_fastload_faceanim.bat @@ -0,0 +1,11 @@ +@echo off +REM Test for issue #847: fastload face animation stuck +REM Launches vkQuake with the test script + +set VKQUAKE=C:\project\vkQuake\Windows\VisualStudio\Build-vkQuake\x64\Release\vkQuake.exe + +echo Running test_faceanim via %VKQUAKE%... +echo Look for "PASS" or "FAIL" in the console output. +echo. + +"%VKQUAKE%" +exec test_faceanim.cfg +quit