Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c18aa81f88 | ||
|
|
e08059d324 | ||
|
|
8a52259499 | ||
|
|
283410617e | ||
|
|
7eb9916fea | ||
|
|
50d5fbee05 | ||
|
|
071ab25fa7 | ||
|
|
1b67c96c8c | ||
|
|
b037722432 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
project(AutoSave VERSION 1.1.0)
|
||||
project(AutoSave VERSION 1.2.2)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "-static -static-libgcc -static-libstdc++ -Wall -Werror")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O2 -DTSFUNCS_DEBUG=false")
|
||||
|
||||
@@ -40,6 +40,8 @@ The first save after starting always runs. After that, a save is skipped if noth
|
||||
|
||||
While bricks are loading, the per-brick change checks are skipped, and a finished or cancelled load counts as one change.
|
||||
|
||||
The game turns off packages added after the client add-ons when a server shuts down, which includes AutoSave's change watcher. AutoSave installs it again afterwards, and the first save on each new server always runs.
|
||||
|
||||
A save is also skipped while bricks are loading, while bricks are being cleared, and while the mission is being cleaned up. If a load or a clear starts in the middle of a save, the save stops, its temp file is removed, and the next interval tries again.
|
||||
|
||||
## Large builds
|
||||
@@ -52,47 +54,62 @@ Up to 75000 bricks, bricks are sorted by height like the stock save. Over 75000,
|
||||
|
||||
## Chat commands
|
||||
|
||||
Anyone can use:
|
||||
`/as` is short for `/autosaver` and works with every command below, for example `/as save` or `/as load 08-55-15`.
|
||||
|
||||
Only staff can use `/autosaver` at all, including `help` and `status`. By default that is admins and super admins. With `$Pref::Server::AutoSave::SuperAdminOnly = 1` it is super admins only. The host is always a super admin. Anyone else is told who can use it, and nothing runs.
|
||||
|
||||
- `/autosaver help`
|
||||
- `/autosaver status`
|
||||
|
||||
Admins can use:
|
||||
|
||||
- `/autosaver access admins` or `/autosaver access superadmins`: who can use `/autosaver`. Only a super admin can change this.
|
||||
- `/autosaver on` and `/autosaver off`
|
||||
- `/autosaver save` (or `now`): save right away
|
||||
- `/autosaver load` (or `reload`): clear all bricks, then load the newest file in `saves/autosave/`
|
||||
- `/autosaver load <name>`: clear all bricks, then load that autosave instead, for example `/autosaver load 2026-10-26 08-55-15`
|
||||
- `/autosaver interval <seconds>`: 30 to 86400, default 300
|
||||
- `/autosaver chunk <bricks>`: bricks per slice, 1 to 20000, default 4000
|
||||
- `/autosaver tick <milliseconds>`: pause between slices, 1 to 1000, default 1 (never less than 12 ms in practice)
|
||||
- `/autosaver screenshot on` or `off` (also `screenshots` or `preview`): picture for each save, default on
|
||||
- `/autosaver autoload on` or `off`: load the newest autosave when a server starts, default off
|
||||
|
||||
Out-of-range values are clamped. Changes are announced to everyone and saved right away.
|
||||
|
||||
`load` uses the file with the newest modified time on disk, including saves made since the game started. It loads with saved ownership and exact colors. The command is case-insensitive.
|
||||
`load` with no name uses the file with the newest modified time on disk, including saves made since the game started. With a name, `.bls` is optional, case does not matter, and any part of the name works if only one autosave matches it (`/autosaver load 08-55-15`). If several match, nothing is cleared and you are told how many. Names cannot point outside `saves/autosave/`. Either way it loads with saved ownership and exact colors. The command is case-insensitive.
|
||||
|
||||
## Autoload
|
||||
|
||||
With autoload on, the newest file in `saves/autosave/` is loaded when a server starts, with saved ownership and exact colors, the same as `/autosaver load`. It waits until the mission is running. It is skipped, with a console line saying why, when:
|
||||
|
||||
- a game mode is running (game modes bring their own build);
|
||||
- a save was picked when the server was set up, or bricks are already loaded or loading;
|
||||
- there is no autosave.
|
||||
|
||||
The loaded build counts as already saved, so the next interval does not write the same build again.
|
||||
|
||||
## Preferences
|
||||
|
||||
Settings are server prefs, stored in `config/server/prefs.cs` with the game's other server prefs:
|
||||
|
||||
```
|
||||
$Pref::Server::AutoSave::AutoLoad = 0;
|
||||
$Pref::Server::AutoSave::Chunk = 4000;
|
||||
$Pref::Server::AutoSave::Enabled = 1;
|
||||
$Pref::Server::AutoSave::Interval = 300;
|
||||
$Pref::Server::AutoSave::Preview = 1;
|
||||
$Pref::Server::AutoSave::SuperAdminOnly = 0;
|
||||
$Pref::Server::AutoSave::Tick = 1;
|
||||
```
|
||||
|
||||
They are read when you start hosting. Until then the timer only checks every 5 seconds whether a server is up. If a pref is missing, the default is used and added to the file the next time the game writes its prefs.
|
||||
They are read each time a server starts. AutoSave checks every 5 seconds for a new server, which it tells apart by the mission's `MissionGroup`, so stopping one server and starting another is noticed even within one save interval. The save timer restarts with each new server. If a pref is missing, the default is used and added to the file the next time the game writes its prefs.
|
||||
|
||||
Every `/autosaver on`, `off`, `interval`, `chunk`, `tick` and `screenshot` writes `prefs.cs` right away, using the same export the game runs when custom game settings change. AutoSave only writes the file while a server is running, after the game has loaded it, so other prefs are kept. You can also edit the lines while the game is closed. Values out of range are clamped the same way as in chat.
|
||||
Every `/autosaver on`, `off`, `interval`, `chunk`, `tick`, `screenshot`, `autoload` and `access` writes `prefs.cs` right away, using the same export the game runs when custom game settings change. AutoSave only writes the file while a server is running, after the game has loaded it, so other prefs are kept. You can also edit the lines while the game is closed. Values out of range are clamped the same way as in chat.
|
||||
|
||||
## Console
|
||||
|
||||
- `autoSave()` starts a save now
|
||||
- `autoSaveNewest()` returns the newest autosave path
|
||||
- `autoSaveFind(name)` returns the autosave path for a name, `*N` if N files match, or nothing
|
||||
- `autoSaveApply()` clamps the settings, copies them to `$Pref::Server::AutoSave::*`, writes `config/server/prefs.cs` while hosting, and restarts the timer
|
||||
- `$AutoSave::Enabled`, `$AutoSave::Interval`, `$AutoSave::Chunk`, `$AutoSave::Tick`, `$AutoSave::Preview` are the live values
|
||||
- `$AutoSave::Enabled`, `$AutoSave::Interval`, `$AutoSave::Chunk`, `$AutoSave::Tick`, `$AutoSave::Preview`, `$AutoSave::AutoLoad`, `$AutoSave::SuperAdminOnly` are the live values
|
||||
|
||||
A value set on `$AutoSave::*` in the console is not saved, and a new interval does not take effect, until `autoSaveApply()` is called.
|
||||
|
||||
@@ -109,6 +126,4 @@ cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MIN
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
The result is `build/AutoSave.dll`. CMake is set so the filename has no `lib` prefix. The link line is tsfuncs, MinHook, `psapi`, `gdiplus` (PNG to JPEG for the preview) and `shlwapi`, with static libgcc and libstdc++.
|
||||
|
||||
Source is `src/main.cpp` and `CMakeLists.txt`. The current DLL is attached to the latest release.
|
||||
The result is `build/AutoSave.dll`. CMa
|
||||
+302
-44
@@ -36,6 +36,8 @@ static int gChangeAtStart = 0;
|
||||
static int gSavedCount = -1;
|
||||
static bool gReady = false;
|
||||
static bool gPreview = true;
|
||||
static bool gAutoLoad = false;
|
||||
static bool gSuperAdminOnly = false;
|
||||
|
||||
struct Row {
|
||||
ADDR obj;
|
||||
@@ -591,8 +593,10 @@ 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;"
|
||||
"$Pref::Server::AutoSave::SuperAdminOnly = %d;",
|
||||
gEnabled ? 1 : 0, gInterval, gChunk, gTickMs, gPreview ? 1 : 0, gAutoLoad ? 1 : 0, gSuperAdminOnly ? 1 : 0);
|
||||
tsf_Eval(code);
|
||||
}
|
||||
|
||||
@@ -614,6 +618,12 @@ 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);
|
||||
value = varOf("$Pref::Server::AutoSave::SuperAdminOnly");
|
||||
if (!value.empty())
|
||||
gSuperAdminOnly = truthy(value);
|
||||
clampPrefs();
|
||||
prefsToScript();
|
||||
}
|
||||
@@ -1592,12 +1602,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);
|
||||
}
|
||||
@@ -1629,7 +1639,11 @@ static void autoSaveApply(ADDR obj, int argc, const char *argv[])
|
||||
static const char kChatScript[] = R"TS(
|
||||
function AutoSave_Admin(%client)
|
||||
{
|
||||
return isObject(%client) && (%client.isAdmin || %client.isSuperAdmin);
|
||||
if (!isObject(%client))
|
||||
return 0;
|
||||
if ($AutoSave::SuperAdminOnly)
|
||||
return %client.isSuperAdmin;
|
||||
return %client.isAdmin || %client.isSuperAdmin;
|
||||
}
|
||||
function AutoSave_Tell(%client, %line)
|
||||
{
|
||||
@@ -1642,7 +1656,7 @@ function AutoSave_Tell(%client, %line)
|
||||
}
|
||||
function AutoSave_Help(%client)
|
||||
{
|
||||
AutoSave_Tell(%client, "/autosaver help");
|
||||
AutoSave_Tell(%client, "/autosaver help \c6(or \c3/as\c6 for short)");
|
||||
AutoSave_Tell(%client, "/autosaver status");
|
||||
AutoSave_Tell(%client, "/autosaver on");
|
||||
AutoSave_Tell(%client, "/autosaver off");
|
||||
@@ -1651,7 +1665,9 @@ 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>");
|
||||
AutoSave_Tell(%client, "/autosaver access <admins or superadmins>");
|
||||
}
|
||||
function AutoSave_Busy()
|
||||
{
|
||||
@@ -1697,18 +1713,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.");
|
||||
@@ -1727,6 +1798,11 @@ function AutoSave_Command(%client, %cmd, %arg)
|
||||
{
|
||||
echo("AutoSave: /autosaver" SPC %cmd SPC %arg);
|
||||
%cmd = strlwr(%cmd);
|
||||
if (!AutoSave_Admin(%client))
|
||||
{
|
||||
AutoSave_Tell(%client, $AutoSave::SuperAdminOnly ? "Only super admins can use /autosaver." : "Only admins can use /autosaver.");
|
||||
return;
|
||||
}
|
||||
if (%cmd $= "" || %cmd $= "help")
|
||||
{
|
||||
AutoSave_Help(%client);
|
||||
@@ -1734,15 +1810,32 @@ 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.");
|
||||
return;
|
||||
}
|
||||
if (!AutoSave_Admin(%client))
|
||||
{
|
||||
AutoSave_Tell(%client, "Only an admin can change autosave.");
|
||||
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, access \c3" @ ($AutoSave::SuperAdminOnly ? "super admins" : "admins") @ "\c6.");
|
||||
return;
|
||||
}
|
||||
%name = %client.getPlayerName();
|
||||
if (%cmd $= "access")
|
||||
{
|
||||
if (!%client.isSuperAdmin)
|
||||
{
|
||||
AutoSave_Tell(%client, "Only a super admin can change who can use /autosaver.");
|
||||
return;
|
||||
}
|
||||
%arg = strlwr(%arg);
|
||||
if (%arg $= "admin" || %arg $= "admins")
|
||||
%super = 0;
|
||||
else if (%arg $= "superadmin" || %arg $= "superadmins" || %arg $= "super")
|
||||
%super = 1;
|
||||
else
|
||||
{
|
||||
AutoSave_Tell(%client, "Use \c3/autosaver access admins\c6 or \c3/autosaver access superadmins\c6.");
|
||||
return;
|
||||
}
|
||||
$AutoSave::SuperAdminOnly = %super;
|
||||
autoSaveApply();
|
||||
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 set /autosaver to \c3" @ (%super ? "super admins only" : "admins and super admins") @ "\c6.");
|
||||
return;
|
||||
}
|
||||
if (%cmd $= "on")
|
||||
{
|
||||
$AutoSave::Enabled = 1;
|
||||
@@ -1772,7 +1865,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 +1944,41 @@ 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));
|
||||
}
|
||||
function serverCmdas(%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));
|
||||
}
|
||||
function serverCmdAs(%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));
|
||||
}
|
||||
function serverCmdAS(%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));
|
||||
}
|
||||
function serverCmdaS(%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";
|
||||
@@ -1886,13 +2016,14 @@ package AutoSaveWatch
|
||||
if (getSubStr(%text, 0, 1) $= "/")
|
||||
{
|
||||
%body = trim(getSubStr(%text, 1, 256));
|
||||
if (strlwr(getWord(%body, 0)) $= "autosaver")
|
||||
%word = strlwr(getWord(%body, 0));
|
||||
if (%word $= "autosaver" || %word $= "as")
|
||||
{
|
||||
%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 +2049,11 @@ package AutoSaveWatch
|
||||
Parent::ServerLoadSaveFile_End();
|
||||
$AutoSave::Loading = 0;
|
||||
$AutoSave::Change++;
|
||||
if ($AutoSave::AutoLoading)
|
||||
{
|
||||
$AutoSave::AutoLoading = 0;
|
||||
autoSaveBaseline();
|
||||
}
|
||||
}
|
||||
function serverCmdCancelSaveFileUpload(%client)
|
||||
{
|
||||
@@ -1925,11 +2061,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 +2261,43 @@ 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, %s", PROJECT_NAME,
|
||||
gEnabled ? "on" : "off", gInterval, gChunk, gTickMs, gPreview ? "on" : "off", gAutoLoad ? "on" : "off",
|
||||
gSuperAdminOnly ? "super admins only" : "admins");
|
||||
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 +2321,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 +2439,7 @@ static void deferredInstall()
|
||||
if (!gArmed) {
|
||||
gArmed = true;
|
||||
reschedule();
|
||||
scheduleHostCheck();
|
||||
}
|
||||
if (!gChat)
|
||||
installChat();
|
||||
@@ -2237,6 +2464,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 +2503,20 @@ bool init()
|
||||
tsf_AddVar("AutoSave::Change", &gChange);
|
||||
tsf_AddVar("AutoSave::Loading", &gLoading);
|
||||
tsf_AddVar("AutoSave::Preview", &gPreview);
|
||||
tsf_AddVar("AutoSave::AutoLoad", &gAutoLoad);
|
||||
tsf_AddVar("AutoSave::SuperAdminOnly", &gSuperAdminOnly);
|
||||
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 +2536,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();
|
||||
|
||||
Reference in new issue
Block a user