Use an extension to save replays asynchronously (#1253)

This commit is contained in:
rtldg 2025-07-17 22:42:22 +00:00 committed by GitHub
parent 70308f3d6a
commit 16ccd0cc7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 199 additions and 18 deletions

View File

@ -36,6 +36,9 @@ jobs:
wget https://github.com/srcwr/eventqueuefixfix/releases/download/v1.0.1/eventqueuefixfix-v1.0.1-def5b0e-windows-x32.zip wget https://github.com/srcwr/eventqueuefixfix/releases/download/v1.0.1/eventqueuefixfix-v1.0.1-def5b0e-windows-x32.zip
unzip eventqueuefixfix-v1.0.1-def5b0e-windows-x32.zip "addons/sourcemod/extensions/*" unzip eventqueuefixfix-v1.0.1-def5b0e-windows-x32.zip "addons/sourcemod/extensions/*"
rm "addons/sourcemod/extensions/eventqueuefixfix.pdb" rm "addons/sourcemod/extensions/eventqueuefixfix.pdb"
wget https://github.com/srcwr/srcwrfloppy/releases/download/v2.0.0/srcwrfloppy-v2.0.0.zip
unzip -qO UTF-8 srcwrfloppy-v2.0.0.zip "addons/sourcemod/extensions/*"
rm "addons/sourcemod/extensions/srcwr💾.pdb"
- name: Run compiler - name: Run compiler
shell: bash shell: bash

View File

@ -26,6 +26,9 @@ Includes a records system, map zones (start/end marks etc), bonuses, HUD with us
* Allows for timescaling boosters and is used to fix some exploits. (Use this instead of `boosterfix`) * Allows for timescaling boosters and is used to fix some exploits. (Use this instead of `boosterfix`)
* (included in bhoptimer release zips) * (included in bhoptimer release zips)
* Along with using [eventqueuefixfix](https://github.com/srcwr/eventqueuefixfix) at the same time to fix eventqueuefix on Windows after the 2025-02-18 update. * Along with using [eventqueuefixfix](https://github.com/srcwr/eventqueuefixfix) at the same time to fix eventqueuefix on Windows after the 2025-02-18 update.
* [srcwr💾](https://github.com/srcwr/srcwrfloppy)
* Saves replays asynchronously (read: doesn't lag the server when saving a replay).
* (included in bhoptimer release zips)
* [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556) * [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556)
* Used to grab `{serverip}` in advertisements. * Used to grab `{serverip}` in advertisements.
* [DynamicChannels](https://github.com/Vauff/DynamicChannels) * [DynamicChannels](https://github.com/Vauff/DynamicChannels)

View File

@ -365,6 +365,35 @@ stock void WriteReplayHeader(File fFile, int style, int track, float time, int s
fFile.WriteInt32(view_as<int>(fZoneOffset[1])); fFile.WriteInt32(view_as<int>(fZoneOffset[1]));
} }
stock void cell2buf(char[] buf, int& pos, int cell)
{
buf[pos++] = cell & 0xFF;
buf[pos++] = (cell >> 8) & 0xFF;
buf[pos++] = (cell >> 16) & 0xFF;
buf[pos++] = (cell >> 24) & 0xFF;
}
stock int WriteReplayHeaderToBuffer(char[] buf, int style, int track, float time, int steamid, int preframes, int postframes, float fZoneOffset[2], int totalframes, float tickrate, const char[] sMap)
{
int pos = FormatEx(buf, 512, "%d:%s\n%s", REPLAY_FORMAT_SUBVERSION, REPLAY_FORMAT_FINAL, sMap);
pos += 1; // skip past NUL
buf[pos++] = style & 0xFF;
buf[pos++] = track & 0xFF;
cell2buf(buf, pos, preframes);
cell2buf(buf, pos, totalframes - preframes - postframes);
cell2buf(buf, pos, view_as<int>(time));
cell2buf(buf, pos, steamid);
cell2buf(buf, pos, postframes);
cell2buf(buf, pos, view_as<int>(tickrate));
cell2buf(buf, pos, view_as<int>(fZoneOffset[0]));
cell2buf(buf, pos, view_as<int>(fZoneOffset[1]));
return pos;
}
// file_a is usually used as the wr replay file. // file_a is usually used as the wr replay file.
// file_b is usually used as the duplicate/backup replay file. // file_b is usually used as the duplicate/backup replay file.
stock void WriteReplayFrames(ArrayList playerrecording, int iSize, File file_a, File file_b) stock void WriteReplayFrames(ArrayList playerrecording, int iSize, File file_a, File file_b)

View File

@ -48,8 +48,9 @@ forward Action Shavit_ShouldSaveReplayCopy(int client, int style, float time, in
/** /**
* Called when either a WR replay or a copy of a replay has been saved. * Called when either a WR replay or a copy of a replay has been saved.
* NOTE: Can be called with a delay after a run is finished due to asynchronous replay saving through extensions.
* *
* @param client Client index. * @param client Client index. Can be 0 if the replay was saved asynchronously & the client disconnected super duper quick...
* @param style Style the record was done on. * @param style Style the record was done on.
* @param time Record time. * @param time Record time.
* @param jumps Jumps amount. * @param jumps Jumps amount.

View File

@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2025 rtldg <rtldg@protonmail.com>
#if defined _floppy_included
#endinput
#endif
#define _floppy_included
#pragma semicolon 1
typeset ReplaySavedCallback {
function void(bool saved, any value, char[] sPath);
}
// Don't modify the `playerrecording` ArrayList until the ReplaySavedCallback is called... OR ELSE!!!!
native void SRCWRFloppy_AsyncSaveReplay(
ReplaySavedCallback callback // what to call when saved
, any value // what to pass along to the callback
, char[] wrpath
, char[] copypath
, char[] header
, int headersize
, ArrayList playerrecording
, int totalframes
);
public Extension __ext_srcwrfloppy =
{
name = "srcwr💾",
file = "srcwr💾.ext",
#if defined AUTOLOAD_EXTENSIONS
autoload = 1,
#else
autoload = 0,
#endif
#if defined REQUIRE_EXTENSIONS
required = 1,
#else
required = 0,
#endif
};
#if !defined REQUIRE_EXTENSIONS
public void __ext_floppy_SetNTVOptional()
{
MarkNativeAsOptional("SRCWRFloppy_AsyncSaveReplay");
}
#endif

View File

@ -34,6 +34,10 @@
#include <shavit/replay-file> #include <shavit/replay-file>
#include <shavit/replay-stocks.sp> #include <shavit/replay-stocks.sp>
#undef REQUIRE_EXTENSIONS
#include <srcwr/floppy>
public Plugin myinfo = public Plugin myinfo =
{ {
name = "[shavit] Replay Recorder", name = "[shavit] Replay Recorder",
@ -94,6 +98,7 @@ float gF_HijackedAngles[MAXPLAYERS+1][2];
bool gB_HijackFramesKeepOnStart[MAXPLAYERS+1]; bool gB_HijackFramesKeepOnStart[MAXPLAYERS+1];
bool gB_ReplayPlayback = false; bool gB_ReplayPlayback = false;
bool gB_Floppy = false;
//#include <TickRateControl> //#include <TickRateControl>
forward void TickRate_OnTickRateChanged(float fOld, float fNew); forward void TickRate_OnTickRateChanged(float fOld, float fNew);
@ -149,6 +154,7 @@ public void OnPluginStart()
gF_Tickrate = (1.0 / GetTickInterval()); gF_Tickrate = (1.0 / GetTickInterval());
gB_ReplayPlayback = LibraryExists("shavit-replay-playback"); gB_ReplayPlayback = LibraryExists("shavit-replay-playback");
gB_Floppy = LibraryExists("srcwr💾");
if (gB_Late) if (gB_Late)
{ {
@ -170,6 +176,10 @@ public void OnLibraryAdded(const char[] name)
{ {
gB_ReplayPlayback = true; gB_ReplayPlayback = true;
} }
else if (StrEqual(name, "srcwr💾"))
{
gB_Floppy = true;
}
} }
public void OnLibraryRemoved(const char[] name) public void OnLibraryRemoved(const char[] name)
@ -178,6 +188,10 @@ public void OnLibraryRemoved(const char[] name)
{ {
gB_ReplayPlayback = false; gB_ReplayPlayback = false;
} }
else if (StrEqual(name, "srcwr💾"))
{
gB_Floppy = false;
}
} }
public void OnMapStart() public void OnMapStart()
@ -378,13 +392,94 @@ void DoReplaySaverCallbacks(int iSteamID, int client, int style, float time, int
int postframes = gI_PlayerFrames[client] - gI_PlayerFinishFrame[client]; int postframes = gI_PlayerFrames[client] - gI_PlayerFinishFrame[client];
char sPath[PLATFORM_MAX_PATH]; ArrayList playerrecording = view_as<ArrayList>(CloneHandle(gA_PlayerFrames[client]));
bool saved = SaveReplay(style, track, time, iSteamID, gI_PlayerPrerunFrames[client], gA_PlayerFrames[client], gI_PlayerFrames[client], postframes, timestamp, fZoneOffset, makeCopy, makeReplay, sPath, sizeof(sPath));
DataPack dp = new DataPack();
dp.WriteCell(GetClientSerial(client));
dp.WriteCell(style);
dp.WriteCell(time);
dp.WriteCell(jumps);
dp.WriteCell(strafes);
dp.WriteCell(sync);
dp.WriteCell(track);
dp.WriteCell(oldtime);
dp.WriteCell(perfs);
dp.WriteCell(avgvel);
dp.WriteCell(maxvel);
dp.WriteCell(timestamp);
dp.WriteCell(isBestReplay);
dp.WriteCell(isTooLong);
dp.WriteCell(makeCopy);
dp.WriteCell(playerrecording);
dp.WriteCell(gI_PlayerPrerunFrames[client]);
dp.WriteCell(postframes);
dp.WriteString(sName);
if (gB_Floppy)
{
char buf[512];
int headersize = WriteReplayHeaderToBuffer(buf, style, track, time, iSteamID, gI_PlayerPrerunFrames[client], postframes, fZoneOffset, gI_PlayerFrames[client], gF_Tickrate, gS_Map);
char wrpath[PLATFORM_MAX_PATH], copypath[PLATFORM_MAX_PATH];
if (makeReplay)
FormatEx(wrpath, sizeof(wrpath),
track>0?"%s/%d/%s%s_%d.replay" : "%s/%d/%s%s.replay",
gS_ReplayFolder, style, gS_Map, track
);
if (makeCopy)
FormatEx(copypath, sizeof(copypath), "%s/copy/%d_%d_%s.replay", gS_ReplayFolder, timestamp, iSteamID, gS_Map);
SRCWRFloppy_AsyncSaveReplay(
FloppyAsynchronouslySavedMyReplayWhichWasNiceOfThem
, dp
, wrpath
, copypath
, buf
, headersize
, playerrecording
, gI_PlayerFrames[client]
);
}
else
{
char sPath[PLATFORM_MAX_PATH];
bool saved = SaveReplay(style, track, time, iSteamID, gI_PlayerPrerunFrames[client], playerrecording, gI_PlayerFrames[client], postframes, timestamp, fZoneOffset, makeCopy, makeReplay, sPath, sizeof(sPath));
FloppyAsynchronouslySavedMyReplayWhichWasNiceOfThem(saved, dp, sPath)
}
ClearFrames(client);
}
void FloppyAsynchronouslySavedMyReplayWhichWasNiceOfThem(bool saved, any value, char[] sPath)
{
DataPack dp = value;
dp.Reset();
int client = GetClientFromSerial(dp.ReadCell());
int style = dp.ReadCell();
float time = dp.ReadCell();
int jumps = dp.ReadCell();
int strafes = dp.ReadCell();
float sync = dp.ReadCell();
int track = dp.ReadCell();
float oldtime = dp.ReadCell();
float perfs = dp.ReadCell();
float avgvel = dp.ReadCell();
float maxvel = dp.ReadCell();
int timestamp = dp.ReadCell();
bool isBestReplay = dp.ReadCell();
bool isTooLong = dp.ReadCell();
bool makeCopy = dp.ReadCell();
ArrayList playerrecording = dp.ReadCell();
int preframes = dp.ReadCell();
int postframes = dp.ReadCell();
char sName[MAX_NAME_LENGTH];
dp.ReadString(sName, sizeof(sName));
if (!saved) if (!saved)
{ {
LogError("SaveReplay() failed. Skipping OnReplaySaved") LogError("Failed to save replay... Skipping OnReplaySaved");
ClearFrames(client); delete playerrecording; // importante!
return; return;
} }
@ -405,13 +500,13 @@ void DoReplaySaverCallbacks(int iSteamID, int client, int style, float time, int
Call_PushCell(isTooLong); Call_PushCell(isTooLong);
Call_PushCell(makeCopy); Call_PushCell(makeCopy);
Call_PushString(sPath); Call_PushString(sPath);
Call_PushCell(gA_PlayerFrames[client]); Call_PushCell(playerrecording);
Call_PushCell(gI_PlayerPrerunFrames[client]); Call_PushCell(preframes);
Call_PushCell(postframes); Call_PushCell(postframes);
Call_PushString(sName); Call_PushString(sName);
Call_Finish(); Call_Finish();
ClearFrames(client); delete playerrecording;
} }
public void Shavit_OnFinish(int client, int style, float time, int jumps, int strafes, float sync, int track, float oldtime, float perfs, float avgvel, float maxvel, int timestamp) public void Shavit_OnFinish(int client, int style, float time, int jumps, int strafes, float sync, int track, float oldtime, float perfs, float avgvel, float maxvel, int timestamp)
@ -469,16 +564,6 @@ bool SaveReplay(int style, int track, float time, int steamid, int preframes, Ar
File fWR = null; File fWR = null;
File fCopy = null; File fCopy = null;
if (saveWR)
{
FormatEx(sPath, sPathLen, "%s/%d/%s%s.replay", gS_ReplayFolder, style, gS_Map, (track > 0)? sTrack:"");
if (!(fWR = OpenFile(sPath, "wb+")))
{
LogError("Failed to open WR replay file for writing. ('%s')", sPath);
}
}
if (saveCopy) if (saveCopy)
{ {
FormatEx(sPath, sPathLen, "%s/copy/%d_%d_%s.replay", gS_ReplayFolder, timestamp, steamid, gS_Map); FormatEx(sPath, sPathLen, "%s/copy/%d_%d_%s.replay", gS_ReplayFolder, timestamp, steamid, gS_Map);
@ -489,6 +574,16 @@ bool SaveReplay(int style, int track, float time, int steamid, int preframes, Ar
} }
} }
if (saveWR)
{
FormatEx(sPath, sPathLen, "%s/%d/%s%s.replay", gS_ReplayFolder, style, gS_Map, (track > 0)? sTrack:"");
if (!(fWR = OpenFile(sPath, "wb+")))
{
LogError("Failed to open WR replay file for writing. ('%s')", sPath);
}
}
if (!fWR && !fCopy) if (!fWR && !fCopy)
{ {
// I want to try and salvage the replay file so let's write it out to a random // I want to try and salvage the replay file so let's write it out to a random