diff --git a/src/main.cpp b/src/main.cpp index 58c2a4f..fcd94fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 "); AutoSave_Tell(%client, "/autosaver screenshot "); AutoSave_Tell(%client, "/autosaver save"); - AutoSave_Tell(%client, "/autosaver load"); + AutoSave_Tell(%client, "/autosaver load "); + AutoSave_Tell(%client, "/autosaver autoload "); } function AutoSave_Busy() { @@ -1697,18 +1703,73 @@ 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; } - %file = AutoSave_NewestFile(); - if (%file $= "") + %wanted = trim(%wanted); + if (%wanted $= "") { - AutoSave_Tell(%client, "There is no autosave to load."); - return; + %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."); @@ -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/.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();