Skip to content

Async replay frame loading #1

@mariokeks

Description

@mariokeks

It would be nice to have a native for loading replay frames asynchronously to avoid server lag, similar to how SRCWRFloppy_AsyncSaveReplay handles writing.

The idea is to let the plugin handle the header parsing via the existing ReadReplayHeader() stock and pass the file offset to the extension, so the extension knows where to start reading and can load the frames on the background thread without blocking the game thread.

This would be useful for plugins like savestate or any plugin that needs to load arbitrary replay files on demand, and would pair nicely with a new Shavit_StartReplayFromFileAsync native in shavit-replay-playback.

Limitations:

  • Only REPLAY_FORMAT_FINAL is supported. The old text-based format, REPLAY_FORMAT_V2 and btimes replays cannot be handled by the extension since they require format-aware parsing. The caller is responsible for checking header.sReplayFormat after ReadReplayHeader() and falling back to ReadReplayFrames() for unsupported formats.

floppy.inc addition

typeset ReplayFramesLoadedCallback {
    function void(bool loaded, any value, ArrayList frames);
}

/**
 * Asynchronously loads replay frames from a file.
 *
 * IMPORTANT - this native only supports the REPLAY_FORMAT_FINAL binary format.
 * You MUST check the replay format after ReadReplayHeader() and handle unsupported
 * formats yourself before calling this native. Passing an unsupported format will
 * result in corrupt frame data being returned.
 *
 * Example:
 *
 *   replay_header_t header;
 *   File fFile = ReadReplayHeader(path, header, style, track);
 *   if (fFile == null) return false;
 *
 *   if (!StrEqual(header.sReplayFormat, REPLAY_FORMAT_FINAL))
 *   {
 *       // handle unsupported formats yourself, e.g.:
 *       // ReadReplayFrames(fFile, header, cache);  <- synchronous fallback
 *       // or just:
 *       // delete fFile; return false;
 *       delete fFile;
 *       return false;
 *   }
 *
 *   int fileOffset = fFile.Position;
 *   delete fFile;
 *
 *   int totalCells = GetReplayCellSize(header.iReplayVersion);
 *   int totalFrames = header.iPreFrames + header.iFrameCount + header.iPostFrames;
 *
 *   SRCWRFloppy_AsyncLoadReplayFrames(callback, value, path, fileOffset, totalCells, totalFrames);
 *
 * @param callback      Function to call when loading is complete.
 * @param value         Extra data to pass to the callback, e.g. a DataPack with request context.
 * @param path          Path to the replay file.
 * @param fileOffset    Byte offset where frame data starts (File.Position after
 *                      ReadReplayHeader() has been called and the handle closed).
 * @param totalCells    Number of cells per frame, use GetReplayCellSize().
 * @param totalFrames   Total frames to read (iPreFrames + iFrameCount + iPostFrames),
 *                      already corrected by ReadReplayHeader().
 */
native void SRCWRFloppy_AsyncLoadReplayFrames(
      ReplayFramesLoadedCallback callback
    , any value
    , const char[] path
    , int fileOffset
    , int totalCells
    , int totalFrames
);

replay-file.inc addition

// Returns the number of cells per frame for a given replay version.
// Should also replace the hardcoded checks inside ReadReplayFrames().
stock int GetReplayCellSize(int version)
{
    if (version >= 0x06)
        return 10;
    else if (version > 0x01)
        return 8;
    else
        return 6;
}

Example usage — loading a replay file by path

#include <shavit/replay-file>
#include <srcwr/floppy>

Action Command_LoadReplay(int client, int args)
{
    if (args != 1)
    {
        ReplyToCommand(client, "[SM] Usage: sm_loadreplay <path>");
        return Plugin_Handled;
    }

    char sPath[PLATFORM_MAX_PATH];
    GetCmdArg(1, sPath, sizeof(sPath));

    replay_header_t header;
    File fFile = ReadReplayHeader(sPath, header);

    if (fFile == null)
    {
        ReplyToCommand(client, "[SM] Cannot read replay header.");
        return Plugin_Handled;
    }

    if (!StrEqual(header.sReplayFormat, REPLAY_FORMAT_FINAL))
    {
        // unsupported format for async path, fall back to synchronous
        frame_cache_t cache;
        if (ReadReplayFrames(fFile, header, cache))
            Shavit_StartReplayFromFrameCache(header.iStyle, header.iTrack, 0.0, client, -1, Replay_Dynamic, false, cache);
        delete fFile;
        return Plugin_Handled;
    }

    int fileOffset = fFile.Position;
    delete fFile;

    int totalCells = GetReplayCellSize(header.iReplayVersion);
    int totalFrames = header.iPreFrames + header.iFrameCount + header.iPostFrames;

    DataPack pack = new DataPack();
    pack.WriteCell(GetClientSerial(client));
    pack.WriteCellArray(header, sizeof(header));

    SRCWRFloppy_AsyncLoadReplayFrames(OnFramesLoaded, pack, sPath, fileOffset, totalCells, totalFrames);

    return Plugin_Handled;
}

void OnFramesLoaded(bool loaded, any value, ArrayList frames)
{
    DataPack pack = view_as<DataPack>(value);
    pack.Reset();

    int client = GetClientFromSerial(pack.ReadCell());
    replay_header_t header;
    pack.ReadCellArray(header, sizeof(header));
    delete pack;

    if (!client || !IsClientInGame(client))
    {
        delete frames;
        return;
    }

    if (!loaded)
    {
        ReplyToCommand(client, "[SM] Could not load replay frames.");
        delete frames;
        return;
    }

    frame_cache_t cache;
    cache.iFrameCount    = header.iFrameCount;
    cache.iPreFrames     = header.iPreFrames;
    cache.iPostFrames    = header.iPostFrames;
    cache.fTime          = header.fTime;
    cache.iReplayVersion = header.iReplayVersion;
    cache.fTickrate      = header.fTickrate;
    cache.iSteamID       = header.iSteamID;
    cache.aFrames        = frames;
    cache.bNewFormat     = true;
    FormatEx(cache.sReplayName, sizeof(cache.sReplayName), "[U:1:%u]", header.iSteamID);

    Shavit_StartReplayFromFrameCache(header.iStyle, header.iTrack, -1.0, client, -1, Replay_Dynamic, false, cache);
}

Note: The code examples are AI-assisted and may have flaws as I am unaware about extension development.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions