AutoSave 1.2.0: autoload, /autosaver load <name>, new-server detection, watcher reinstall

This commit is contained in:
RallyBlock committed 2026-10-26 12:29:56 -04:00
1 parent b9d0eb6afd
commit b037722432
1 file changed
+239 -32
+239 -32
View File
@@ -36,6 +36,7 @@ static int gChangeAtStart = 0;
static int gSavedCount = -1;
static bool gReady = false;
static bool gPreview = true;
static bool gAutoLoad = false;
struct Row {
ADDR obj;
@@ -591,8 +592,9 @@ static void prefsToScript()
"$Pref::Server::AutoSave::Interval = %d;"
"$Pref::Server::AutoSave::Chunk = %d;"
"$Pref::Server::AutoSave::Tick = %d;"
"$Pref::Server::AutoSave::Preview = %d;",
gEnabled ? 1 : 0, gInterval, gChunk, gTickMs, gPreview ? 1 : 0);
"$Pref::Server::AutoSave::Preview = %d;"
"$Pref::Server::AutoSave::AutoLoad = %d;",
gEnabled ? 1 : 0, gInterval, gChunk, gTickMs, gPreview ? 1 : 0, gAutoLoad ? 1 : 0);
tsf_Eval(code);
}
@@ -614,6 +616,9 @@ static void prefsFromScript()
value = varOf("$Pref::Server::AutoSave::Preview");
if (!value.empty())
gPreview = truthy(value);
value = varOf("$Pref::Server::AutoSave::AutoLoad");
if (!value.empty())
gAutoLoad = truthy(value);
clampPrefs();
prefsToScript();
}
@@ -1592,12 +1597,12 @@ static int clampInterval()
return gInterval;
}
static bool gHostSeen = false;
static int gServerId = 0;
static void reschedule()
{
char code[128];
int delay = gHostSeen ? clampInterval() * 1000 : 5000;
int delay = clampInterval() * 1000;
std::snprintf(code, sizeof(code), "$AutoSave::Schedule = schedule(%d, 0, autoSaveTick);", delay);
tsf_Eval(code);
}
@@ -1651,7 +1656,8 @@ function AutoSave_Help(%client)
AutoSave_Tell(%client, "/autosaver tick <milliseconds, 1-1000>");
AutoSave_Tell(%client, "/autosaver screenshot <on or off>");
AutoSave_Tell(%client, "/autosaver save");
AutoSave_Tell(%client, "/autosaver load");
AutoSave_Tell(%client, "/autosaver load <optional autosave name>");
AutoSave_Tell(%client, "/autosaver autoload <on or off>");
}
function AutoSave_Busy()
{
@@ -1697,19 +1703,74 @@ function AutoSave_ReloadTick()
$AutoSave::ReloadFile = "";
serverDirectSaveFileLoad(%file, 3, "", 1, 0);
}
function AutoSave_Reload(%client)
function AutoSave_AutoLoadStart()
{
if (isEventPending($AutoSave::AutoLoadSchedule))
cancel($AutoSave::AutoLoadSchedule);
$AutoSave::AutoLoadTries = 0;
AutoSave_AutoLoadWait();
}
function AutoSave_AutoLoadWait()
{
if (!$missionRunning || !isObject(mainBrickGroup))
{
if ($AutoSave::AutoLoadTries++ < 240)
$AutoSave::AutoLoadSchedule = schedule(500, 0, AutoSave_AutoLoadWait);
else
echo("AutoSave: autoload gave up, the mission did not start");
return;
}
if ($GameModeArg !$= "")
{
echo("AutoSave: autoload skipped, a game mode is running");
return;
}
if ($SaveFileArg !$= "" || AutoSave_Busy() || getBrickCount() > 0)
{
echo("AutoSave: autoload skipped, the server already has bricks");
return;
}
%file = AutoSave_NewestFile();
if (%file $= "")
{
echo("AutoSave: autoload skipped, there is no autosave");
return;
}
echo("AutoSave: autoload " @ %file);
$AutoSave::AutoLoading = 1;
serverDirectSaveFileLoad(%file, 3, "", 1, 0);
}
function AutoSave_Reload(%client, %wanted)
{
if ($Game::MissionCleaningUp || AutoSave_Busy() || $AutoSave::ReloadFile !$= "")
{
AutoSave_Tell(%client, "Bricks are still being cleared or loaded.");
return;
}
%wanted = trim(%wanted);
if (%wanted $= "")
{
%file = AutoSave_NewestFile();
if (%file $= "")
{
AutoSave_Tell(%client, "There is no autosave to load.");
return;
}
}
else
{
%file = autoSaveFind(%wanted);
if (getSubStr(%file, 0, 1) $= "*")
{
AutoSave_Tell(%client, "\c3" @ getSubStr(%file, 1, 10) @ "\c6 autosaves match \c3" @ %wanted @ "\c6. Type more of the name.");
return;
}
if (%file $= "")
{
AutoSave_Tell(%client, "No autosave named \c3" @ %wanted @ "\c6 in saves/autosave/.");
return;
}
}
%name = %client.getPlayerName();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 is loading \c3saves/autosave/" @ fileName(%file) @ "\c6.");
$AutoSave::ReloadFile = %file;
@@ -1734,7 +1795,7 @@ function AutoSave_Command(%client, %cmd, %arg)
}
if (%cmd $= "status")
{
AutoSave_Tell(%client, "Enabled \c3" @ ($AutoSave::Enabled ? "on" : "off") @ "\c6, every \c3" @ $AutoSave::Interval @ "\c6 seconds, \c3" @ $AutoSave::Chunk @ "\c6 bricks per slice, \c3" @ $AutoSave::Tick @ "\c6 ms between slices, screenshots \c3" @ ($AutoSave::Preview ? "on" : "off") @ "\c6.");
AutoSave_Tell(%client, "Enabled \c3" @ ($AutoSave::Enabled ? "on" : "off") @ "\c6, every \c3" @ $AutoSave::Interval @ "\c6 seconds, \c3" @ $AutoSave::Chunk @ "\c6 bricks per slice, \c3" @ $AutoSave::Tick @ "\c6 ms between slices, screenshots \c3" @ ($AutoSave::Preview ? "on" : "off") @ "\c6, autoload \c3" @ ($AutoSave::AutoLoad ? "on" : "off") @ "\c6.");
return;
}
if (!AutoSave_Admin(%client))
@@ -1772,7 +1833,24 @@ function AutoSave_Command(%client, %cmd, %arg)
}
if (%cmd $= "load" || %cmd $= "reload")
{
AutoSave_Reload(%client);
AutoSave_Reload(%client, %arg);
return;
}
if (%cmd $= "autoload")
{
%arg = strlwr(%arg);
if (%arg $= "on" || %arg $= "1")
%on = 1;
else if (%arg $= "off" || %arg $= "0")
%on = 0;
else
{
AutoSave_Tell(%client, "Use \c3/autosaver autoload on\c6 or \c3/autosaver autoload off\c6.");
return;
}
$AutoSave::AutoLoad = %on;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 turned autoload \c3" @ (%on ? "on" : "off") @ "\c6. " @ (%on ? "The newest autosave loads when the server starts." : ""));
return;
}
if (%cmd $= "preview" || %cmd $= "screenshot" || %cmd $= "screenshots")
@@ -1834,21 +1912,25 @@ function AutoSave_Command(%client, %cmd, %arg)
}
AutoSave_Tell(%client, "Unknown command. Type \c3/autosaver help\c6.");
}
function serverCmdautosaver(%client, %cmd, %arg)
function AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
AutoSave_Command(%client, %cmd, %arg);
return trim(%a1 SPC %a2 SPC %a3 SPC %a4 SPC %a5 SPC %a6 SPC %a7 SPC %a8 SPC %a9 SPC %a10);
}
function serverCmdAutosaver(%client, %cmd, %arg)
function serverCmdautosaver(%client, %cmd, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
AutoSave_Command(%client, %cmd, %arg);
AutoSave_Command(%client, %cmd, AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10));
}
function serverCmdAutoSaver(%client, %cmd, %arg)
function serverCmdAutosaver(%client, %cmd, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
AutoSave_Command(%client, %cmd, %arg);
AutoSave_Command(%client, %cmd, AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10));
}
function serverCmdAUTOSAVER(%client, %cmd, %arg)
function serverCmdAutoSaver(%client, %cmd, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
AutoSave_Command(%client, %cmd, %arg);
AutoSave_Command(%client, %cmd, AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10));
}
function serverCmdAUTOSAVER(%client, %cmd, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
AutoSave_Command(%client, %cmd, AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10));
}
$AutoSave::ChatInstalled = 1;
)TS";
@@ -1890,9 +1972,9 @@ package AutoSaveWatch
{
%client = findLocalClient();
if (isObject(%client))
AutoSave_Command(%client, getWord(%body, 1), getWord(%body, 2));
AutoSave_Command(%client, getWord(%body, 1), getWords(%body, 2));
else
commandToServer('autosaver', getWord(%body, 1), getWord(%body, 2));
commandToServer('autosaver', getWord(%body, 1), getWords(%body, 2));
Canvas.popDialog(newMessageHud);
return;
}
@@ -1918,6 +2000,11 @@ package AutoSaveWatch
Parent::ServerLoadSaveFile_End();
$AutoSave::Loading = 0;
$AutoSave::Change++;
if ($AutoSave::AutoLoading)
{
$AutoSave::AutoLoading = 0;
autoSaveBaseline();
}
}
function serverCmdCancelSaveFileUpload(%client)
{
@@ -1925,11 +2012,16 @@ package AutoSaveWatch
if (!isObject($Server_LoadFileObj))
$AutoSave::Loading = 0;
$AutoSave::Change++;
$AutoSave::AutoLoading = 0;
}
function destroyServer()
{
$AutoSave::Loading = 0;
$AutoSave::AutoLoading = 0;
if (isEventPending($AutoSave::AutoLoadSchedule))
cancel($AutoSave::AutoLoadSchedule);
Parent::destroyServer();
autoSaveRewatch();
}
function serverCmdAddEvent(%client, %enabled, %inputEventIdx, %delay, %targetIdx, %NTNameIdx, %outputEventIdx, %par1, %par2, %par3, %par4)
{
@@ -2120,24 +2212,42 @@ static void autoSaveTick(ADDR obj, int argc, const char *argv[])
(void)argc;
(void)argv;
notePreview();
if (!hosting()) {
gHostSeen = false;
reschedule();
return;
}
if (!gHostSeen) {
gHostSeen = true;
prefsFromScript();
BlPrintf("%s: $Pref::Server::AutoSave: %s, every %d seconds, %d bricks per slice, %d ms, preview %s", PROJECT_NAME,
gEnabled ? "on" : "off", gInterval, gChunk, gTickMs, gPreview ? "on" : "off");
reschedule();
return;
}
if (gEnabled && gPhase == 0)
if (gServerId != 0 && gEnabled && gPhase == 0 && hosting())
beginSave();
reschedule();
}
static void scheduleHostCheck()
{
tsf_Eval("if (isEventPending($AutoSave::HostSchedule)) cancel($AutoSave::HostSchedule);"
"$AutoSave::HostSchedule = schedule(5000, 0, autoSaveHostCheck);");
}
/* Each mission creates a new MissionGroup, so its id tells servers apart even
when one stops and the next starts between two save intervals. */
static void autoSaveHostCheck(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
ADDR missionGroup = hosting() ? tsf_FindObject("MissionGroup") : 0;
int id = missionGroup != 0 ? *(int *)(missionGroup + 0x20) : 0;
if (id != 0 && id != gServerId) {
gServerId = id;
gHaveBaseline = false;
prefsFromScript();
BlPrintf("%s: $Pref::Server::AutoSave: %s, every %d seconds, %d bricks per slice, %d ms, screenshots %s, autoload %s", PROJECT_NAME,
gEnabled ? "on" : "off", gInterval, gChunk, gTickMs, gPreview ? "on" : "off", gAutoLoad ? "on" : "off");
if (gAutoLoad && truthy(callFn1("isFunction", "AutoSave_AutoLoadStart")))
tsf_Eval("AutoSave_AutoLoadStart();");
tsf_Eval("if (isEventPending($AutoSave::Schedule)) cancel($AutoSave::Schedule);");
reschedule();
} else if (id == 0) {
gServerId = 0;
}
scheduleHostCheck();
}
static char gReturnText[32];
static const char *autoSaveNow(ADDR obj, int argc, const char *argv[])
@@ -2161,6 +2271,72 @@ static const char *autoSaveNow(ADDR obj, int argc, const char *argv[])
return gReturnText;
}
static char gFoundPath[MAX_PATH];
/* Returns saves/autosave/<name>.bls for an exact name (with or without .bls,
any case) or for the one file whose name contains the text. "*N" means N
files contain it, "" means none. Names cannot leave the folder. */
static const char *autoSaveFind(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
std::string name = argv[1] != 0 ? argv[1] : "";
char folder[MAX_PATH];
WIN32_FIND_DATAA found;
HANDLE handle;
std::string match;
std::string lowerName;
int prefixCount = 0;
while (!name.empty() && name[0] == ' ')
name.erase(0, 1);
while (!name.empty() && name[name.size() - 1] == ' ')
name.erase(name.size() - 1);
if (_strnicmp(name.c_str(), "saves/autosave/", 15) == 0)
name.erase(0, 15);
if (name.size() >= 4 && _stricmp(name.c_str() + name.size() - 4, ".bls") == 0)
name.erase(name.size() - 4);
if (name.empty() || name.find_first_of("/\\:*?\"<>|") != std::string::npos || name.find("..") != std::string::npos)
return "";
if (!gameFile(folder, MAX_PATH, "autosave\\*.bls"))
return "";
handle = FindFirstFileA(folder, &found);
if (handle == INVALID_HANDLE_VALUE)
return "";
do {
if (found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
continue;
std::string file = found.cFileName;
if (file.size() < 4 || _stricmp(file.c_str() + file.size() - 4, ".bls") != 0)
continue;
std::string base = file.substr(0, file.size() - 4);
if (_stricmp(base.c_str(), name.c_str()) == 0) {
match = file;
prefixCount = 1;
break;
}
if (lowerName.empty()) {
lowerName = name;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
}
std::transform(base.begin(), base.end(), base.begin(), ::tolower);
if (base.find(lowerName) != std::string::npos) {
if (prefixCount == 0)
match = file;
prefixCount++;
}
} while (FindNextFileA(handle, &found));
FindClose(handle);
if (prefixCount == 0)
return "";
if (prefixCount > 1) {
std::snprintf(gFoundPath, sizeof(gFoundPath), "*%d", prefixCount);
return gFoundPath;
}
std::snprintf(gFoundPath, sizeof(gFoundPath), "saves/autosave/%s", match.c_str());
return gFoundPath;
}
static char gNewestPath[MAX_PATH];
static const char *autoSaveNewest(ADDR obj, int argc, const char *argv[])
@@ -2213,6 +2389,7 @@ static void deferredInstall()
if (!gArmed) {
gArmed = true;
reschedule();
scheduleHostCheck();
}
if (!gChat)
installChat();
@@ -2237,6 +2414,30 @@ static VOID CALLBACK installTimerProc(HWND hwnd, UINT message, UINT_PTR id, DWOR
}
}
/* destroyServer deactivates every package activated after the client
add-ons, AutoSaveWatch included, so it is installed again afterwards. */
static void autoSaveRewatch(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
gWatching = false;
tsf_Eval("$AutoSave::WatchInstalled = 0;");
if (gInstallTimer == 0)
gInstallTimer = SetTimer(0, 0, 500, installTimerProc);
}
/* A build that was just loaded from a file matches that file. */
static void autoSaveBaseline(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
gSavedChange = gChange;
gSavedCount = brickCount();
gHaveBaseline = true;
}
bool init()
{
if (!tsh_InitInternal())
@@ -2252,14 +2453,19 @@ bool init()
tsf_AddVar("AutoSave::Change", &gChange);
tsf_AddVar("AutoSave::Loading", &gLoading);
tsf_AddVar("AutoSave::Preview", &gPreview);
tsf_AddVar("AutoSave::AutoLoad", &gAutoLoad);
installPreview();
tsf_AddConsoleFunc(0, 0, "autoSaveTick", autoSaveTick, "autoSaveTick() - timed brick save", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveChunk", autoSaveChunk, "autoSaveChunk() - continue a brick save", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveApply", autoSaveApply, "autoSaveApply() - clamp autosave prefs, write config/server/prefs.cs and reschedule", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSave", autoSaveNow, "autoSave() - write a dated save in saves/autosave/", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveNewest", autoSaveNewest, "autoSaveNewest() - newest file in saves/autosave/", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveFind", autoSaveFind, "autoSaveFind(name) - saves/autosave/ file by name or unique start", 2, 2);
tsf_AddConsoleFunc(0, 0, "autoSavePreviewPoll", autoSavePreviewPoll, "autoSavePreviewPoll() - report a finished save screenshot", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveTraceStep", autoSaveTraceStep, "autoSaveTraceStep() - trace part of a save screenshot", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveHostCheck", autoSaveHostCheck, "autoSaveHostCheck() - notice a new server", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveRewatch", autoSaveRewatch, "autoSaveRewatch() - install the change watcher again", 1, 1);
tsf_AddConsoleFunc(0, 0, "autoSaveBaseline", autoSaveBaseline, "autoSaveBaseline() - treat the current build as saved", 1, 1);
gInstallTimer = SetTimer(0, 0, 500, installTimerProc);
if (gInstallTimer == 0)
BlPrintf("%s: could not start the install timer", PROJECT_NAME);
@@ -2279,6 +2485,7 @@ bool deinit()
tsf_Eval("if (isEventPending($AutoSave::ChunkSchedule)) cancel($AutoSave::ChunkSchedule);");
tsf_Eval("if (isEventPending($AutoSave::PreviewSchedule)) cancel($AutoSave::PreviewSchedule);");
tsf_Eval("if (isEventPending($AutoSave::TraceSchedule)) cancel($AutoSave::TraceSchedule);");
tsf_Eval("if (isEventPending($AutoSave::HostSchedule)) cancel($AutoSave::HostSchedule);");
if (gWatching)
tsf_Eval("deactivatePackage(AutoSaveWatch);");
clearSave();