Files
AutoSave/src/main.cpp
T

2511 lines
74 KiB
C++

#include <windows.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
#include "TSFuncs.hpp"
/* Writes saves/autosave/YYYY-MM-DD HH-MM-SS.bls in the same layout as
saveBricks(), without the GUI list. The picture comes from the server
preview raytracer instead of a camera screenshot. Each slice stops
after a few milliseconds so the server can tick. Builds over 75000 bricks
skip the ground-distance sort. Brick fields still come from the engine
methods. SimSet's object array is the one Set::getObject uses in this
build (count at +0x34, pointers at +0x3c, id at object +0x20), and each
save checks that against getCount/getObject before trusting it. */
static bool gEnabled = true;
static int gInterval = 300;
static int gChunk = 4000;
static int gTickMs = 1;
static bool gArmed = false;
static bool gChat = false;
static bool gChatNoted = false;
static bool gWatching = false;
static bool gHaveBaseline = false;
static int gInstalling = 0;
static int gChange = 0;
static int gLoading = 0;
static int gSavedChange = 0;
static int gChangeAtStart = 0;
static int gSavedCount = -1;
static bool gReady = false;
static bool gPreview = true;
static bool gAutoLoad = false;
struct Row {
ADDR obj;
float height;
};
static bool truthy(const std::string &value)
{
return !value.empty() && value != "0" && value != "false";
}
static std::string callObj(ADDR obj, const char *name)
{
/* Con::execute takes the object, not a pointer to the pointer. */
const char *result = tsf_BlCon__executefSimObj((ADDR *)obj, 1, name);
return result != 0 ? result : "";
}
static std::string callObj1(ADDR obj, const char *name, const char *arg)
{
const char *result = tsf_BlCon__executefSimObj((ADDR *)obj, 2, name, arg);
return result != 0 ? result : "";
}
static std::string callFn1(const char *name, const char *arg)
{
const char *result = tsf_BlCon__executef(2, name, arg);
return result != 0 ? result : "";
}
static std::string callFn2(const char *name, const char *a, const char *b)
{
const char *result = tsf_BlCon__executef(3, name, a, b);
return result != 0 ? result : "";
}
static int brickCount()
{
const char *result = tsf_BlCon__executef(1, "getBrickCount");
return result != 0 ? std::atoi(result) : -1;
}
static bool unchanged()
{
if (!gWatching || !gHaveBaseline)
return false;
return gChange == gSavedChange && brickCount() == gSavedCount;
}
static void markSaved()
{
int count;
if (gChange != gChangeAtStart) {
gHaveBaseline = true;
return;
}
count = brickCount();
gSavedChange = gChange;
gSavedCount = count;
gHaveBaseline = true;
}
static std::string dataField(ADDR obj, const char *slot, const char *index)
{
const char *result = tsf_GetDataField(obj, slot, index);
return result != 0 ? result : "";
}
/* A field name resolved through the string table once. Static fields return
a shared buffer, so copy the result before the next lookup. */
struct Field {
const char *name;
const char *entry;
};
static const char *fieldOf(ADDR obj, Field &field, const char *index = 0)
{
if (field.entry == 0)
field.entry = tsf_BlStringTable__insert(field.name, false);
const char *result = tsf_BlSimObject__getDataField(obj, field.entry, index);
return result != 0 ? result : "";
}
static bool emptyId(const char *id)
{
return id[0] == '\0' || (id[0] == '0' && id[1] == '\0') || (id[0] == '-' && id[1] == '1' && id[2] == '\0');
}
static Field kUiName = {"uiName", 0};
static Field kHasPrint = {"hasPrint", 0};
static Field kAngleID = {"angleID", 0};
static Field kIsBasePlate = {"isBasePlate", 0};
static Field kColorID = {"colorID", 0};
static Field kPrintID = {"printID", 0};
static Field kColorFxID = {"colorFxID", 0};
static Field kShapeFxID = {"shapeFxID", 0};
static Field kPosition = {"position", 0};
static Field kBlid = {"bl_id", 0};
static Field kNumEvents = {"numEvents", 0};
static Field kEventInputIdx = {"eventInputIdx", 0};
static Field kEventTargetIdx = {"eventTargetIdx", 0};
static Field kEventOutputIdx = {"eventOutputIdx", 0};
static Field kEventNT = {"eventNT", 0};
static Field kEventEnabled = {"eventEnabled", 0};
static Field kEventDelay = {"eventDelay", 0};
static Field kEventParameter = {"eventOutputParameter", 0};
static Field kEmitterDirection = {"emitterDirection", 0};
static Field kEmitter = {"emitter", 0};
static Field kLight = {"light", 0};
static Field kEnable = {"Enable", 0};
static Field kItem = {"Item", 0};
static Field kItemPosition = {"itemPosition", 0};
static Field kItemDirection = {"itemDirection", 0};
static Field kItemRespawnTime = {"itemRespawnTime", 0};
static Field kAudioEmitter = {"AudioEmitter", 0};
static Field kVehicleSpawnMarker = {"VehicleSpawnMarker", 0};
struct DatablockInfo {
std::string uiName;
bool hasPrint;
};
/* Cleared at the start of every save. */
static std::unordered_map<ADDR, DatablockInfo> gDatablocks;
static std::unordered_map<std::string, std::string> gPrintTokens;
/* Brick fields are read directly unless the first bricks of a save disagree
with the engine methods; then the methods are used until restart. */
static bool gFieldInts = true;
static bool gFieldPos = true;
static int gVerifyLeft = 0;
static const DatablockInfo &datablockInfo(ADDR datablock)
{
std::unordered_map<ADDR, DatablockInfo>::iterator it = gDatablocks.find(datablock);
if (it != gDatablocks.end())
return it->second;
DatablockInfo info;
info.uiName = fieldOf(datablock, kUiName);
info.hasPrint = truthy(fieldOf(datablock, kHasPrint));
return gDatablocks.emplace(datablock, info).first->second;
}
static std::string uiNameOf(const char *id)
{
if (emptyId(id))
return "";
ADDR datablock = tsf_FindObject((unsigned int)std::atoi(id));
return datablock != 0 ? datablockInfo(datablock).uiName : "";
}
static std::string varOf(const char *name)
{
const char *result = tsf_GetVar(name);
return result != 0 ? result : "";
}
static std::string word(const std::string &text, int index)
{
int seen = 0;
size_t i = 0;
while (i < text.size()) {
while (i < text.size() && text[i] == ' ')
i++;
if (i >= text.size())
break;
size_t start = i;
while (i < text.size() && text[i] != ' ')
i++;
if (seen == index)
return text.substr(start, i - start);
seen++;
}
return "";
}
static std::string fieldAt(const std::string &text, int index)
{
int seen = 0;
size_t i = 0;
while (true) {
size_t start = i;
while (i < text.size() && text[i] != '\t')
i++;
if (seen == index)
return text.substr(start, i - start);
if (i >= text.size())
return "";
i++;
seen++;
}
}
static std::string table(const char *base, const std::string &row, const std::string &col)
{
std::string name = std::string("$") + base + row + "_" + col;
return varOf(name.c_str());
}
static void writeLine(FILE *file, const std::string &text)
{
std::fwrite(text.data(), 1, text.size(), file);
std::fputc('\r', file);
std::fputc('\n', file);
}
static int nativeCount(ADDR set)
{
return *(int *)(set + 0x34);
}
static ADDR nativeObject(ADDR set, int index)
{
ADDR *list = *(ADDR **)(set + 0x3c);
if (list == 0)
return 0;
return list[index];
}
static bool layoutMatches(ADDR set)
{
char index[16];
if (set == 0)
return false;
int native = nativeCount(set);
int script = std::atoi(callObj(set, "getCount").c_str());
if (native != script || native < 0 || native > 2000000)
return false;
if (native == 0)
return true;
ADDR obj = nativeObject(set, 0);
if (obj == 0)
return false;
std::snprintf(index, sizeof(index), "%d", 0);
int id = *(int *)(obj + 0x20);
return std::atoi(callObj1(set, "getObject", index).c_str()) == id;
}
static ADDR scriptObject(ADDR set, int index)
{
char text[16];
std::snprintf(text, sizeof(text), "%d", index);
std::string id = callObj1(set, "getObject", text);
if (id.empty() || id == "-1")
return 0;
return tsf_FindObject((unsigned int)std::atoi(id.c_str()));
}
static bool plantedBrick(ADDR obj)
{
if (obj == 0)
return false;
if (!truthy(callObj(obj, "isPlanted")) || truthy(callObj(obj, "isDead")))
return false;
return true;
}
static int classify(ADDR obj)
{
if (obj == 0)
return 0;
if (callObj(obj, "getClassName") == "fxDTSBrick")
return plantedBrick(obj) ? 1 : 0;
return 2;
}
static void remember(ADDR obj, std::vector<Row> &rows, bool &anyValid)
{
float height = (float)std::atof(callObj(obj, "getDistanceFromGround").c_str());
if (height >= 0.0f && height <= 999999.0f)
anyValid = true;
Row row;
row.obj = obj;
row.height = height;
rows.push_back(row);
}
static std::string printTokenFor(const std::string &filename)
{
std::string path = callFn1("filePath", filename.c_str());
std::string dir;
size_t underA;
size_t underB;
if (path.empty() || filename == "base/data/shapes/bricks/brickTop.png")
return "/";
if (path.size() <= 8)
return "/";
dir = path.substr(8);
underA = dir.find('_');
underB = underA == std::string::npos ? std::string::npos : dir.find('_', underA + 1);
if (underA == std::string::npos || underB == std::string::npos || underB <= underA + 1)
return "/";
return dir.substr(underA + 1, underB - underA - 1) + "/" + callFn1("fileBase", filename.c_str());
}
static const std::string &printToken(const std::string &printId)
{
std::unordered_map<std::string, std::string>::iterator it = gPrintTokens.find(printId);
if (it != gPrintTokens.end())
return it->second;
std::string token = printTokenFor(callFn1("getPrintTexture", printId.c_str()));
return gPrintTokens.emplace(printId, token).first->second;
}
static void writeEvents(FILE *file, ADDR brick)
{
int count = std::atoi(fieldOf(brick, kNumEvents));
if (count <= 0 || count > 256)
return;
std::string className = callObj(brick, "getClassName");
for (int i = 0; i < count; ++i) {
char index[16];
std::snprintf(index, sizeof(index), "%d", i);
std::string inputIdx = fieldOf(brick, kEventInputIdx, index);
std::string targetIdx = fieldOf(brick, kEventTargetIdx, index);
std::string outputIdx = fieldOf(brick, kEventOutputIdx, index);
std::string targetName = "-1";
std::string targetClass = "FxDTSBrick";
std::string nt = fieldOf(brick, kEventNT, index);
if (targetIdx != "-1") {
std::string list = table("InputEvent_TargetList", className, inputIdx);
std::string entry = fieldAt(list, std::atoi(targetIdx.c_str()));
targetName = word(entry, 0);
targetClass = word(entry, 1);
nt = "";
if (targetName.empty())
targetName = "-1";
}
std::string enabled = fieldOf(brick, kEventEnabled, index);
std::string delay = fieldOf(brick, kEventDelay, index);
std::string line = std::string("+-EVENT\t") + index + "\t" + enabled
+ "\t" + table("InputEvent_Name", className, inputIdx)
+ "\t" + delay
+ "\t" + targetName + "\t" + nt + "\t"
+ table("OutputEvent_Name", targetClass, outputIdx);
std::string params = table("OutputEvent_parameterList", targetClass, outputIdx);
for (int j = 0; j < 4; ++j) {
char slot[32];
std::string kind = word(fieldAt(params, j), 0);
std::snprintf(slot, sizeof(slot), "%d_%d", i, j + 1);
std::string value = fieldOf(brick, kEventParameter, slot);
if (kind == "dataBlock") {
ADDR named = 0;
if (!value.empty() && value != "0" && value != "-1")
named = tsf_FindObject((unsigned int)std::atoi(value.c_str()));
line += "\t";
line += named != 0 ? callObj(named, "getName") : "-1";
} else {
line += "\t" + value;
}
}
writeLine(file, line);
}
}
static bool writeBrick(FILE *file, ADDR brick, bool ownership, bool extended, bool local)
{
std::string id = callObj(brick, "getDataBlock");
ADDR datablock = 0;
if (!id.empty() && id != "0" && id != "-1")
datablock = tsf_FindObject((unsigned int)std::atoi(id.c_str()));
if (datablock == 0)
return false;
const DatablockInfo &info = datablockInfo(datablock);
bool verify = gVerifyLeft > 0;
if (verify)
gVerifyLeft--;
std::string pos;
if (gFieldPos) {
pos = fieldOf(brick, kPosition);
if (verify) {
std::string transform = callObj(brick, "getTransform");
std::string check = word(transform, 0) + " " + word(transform, 1) + " " + word(transform, 2);
if (check != pos) {
gFieldPos = false;
BlPrintf("%s: brick position field differs from getTransform, using getTransform", PROJECT_NAME);
pos = check;
}
}
} else {
std::string transform = callObj(brick, "getTransform");
pos = word(transform, 0) + " " + word(transform, 1) + " " + word(transform, 2);
}
std::string angle;
std::string base;
std::string color;
std::string colorFx;
std::string shapeFx;
std::string printId;
if (gFieldInts) {
angle = fieldOf(brick, kAngleID);
base = fieldOf(brick, kIsBasePlate);
color = fieldOf(brick, kColorID);
colorFx = fieldOf(brick, kColorFxID);
shapeFx = fieldOf(brick, kShapeFxID);
if (info.hasPrint)
printId = fieldOf(brick, kPrintID);
if (verify && (angle != callObj(brick, "getAngleID") || base != callObj(brick, "isBasePlate")
|| color != callObj(brick, "getColorID") || colorFx != callObj(brick, "getColorFxID")
|| shapeFx != callObj(brick, "getShapeFxID")
|| (info.hasPrint && printId != callObj(brick, "getPrintID")))) {
gFieldInts = false;
BlPrintf("%s: brick fields differ from their methods, using the methods", PROJECT_NAME);
}
}
if (!gFieldInts) {
angle = callObj(brick, "getAngleID");
base = callObj(brick, "isBasePlate");
color = callObj(brick, "getColorID");
colorFx = callObj(brick, "getColorFxID");
shapeFx = callObj(brick, "getShapeFxID");
if (info.hasPrint)
printId = callObj(brick, "getPrintID");
}
std::string line;
line.reserve(160);
line += info.uiName;
line += "\" ";
line += pos;
line += ' ';
line += angle;
line += ' ';
line += base;
line += ' ';
line += color;
line += ' ';
if (info.hasPrint)
line += printToken(printId);
line += ' ';
line += colorFx;
line += ' ';
line += shapeFx;
line += ' ';
line += callObj(brick, "isRayCasting");
line += ' ';
line += callObj(brick, "isColliding");
line += ' ';
line += callObj(brick, "isRendering");
writeLine(file, line);
if (ownership && local && truthy(base)) {
std::string groupId = callObj(brick, "getGroup");
ADDR group = 0;
if (!emptyId(groupId.c_str()))
group = tsf_FindObject((unsigned int)std::atoi(groupId.c_str()));
if (group != 0)
writeLine(file, std::string("+-OWNER ") + fieldOf(group, kBlid));
}
if (!extended)
return true;
std::string name = callObj(brick, "getName");
if (!name.empty())
writeLine(file, "+-NTOBJECTNAME " + name);
writeEvents(file, brick);
std::string emitterDir = fieldOf(brick, kEmitterDirection);
const char *emitterId = fieldOf(brick, kEmitter);
ADDR emitter = emptyId(emitterId) ? 0 : tsf_FindObject((unsigned int)std::atoi(emitterId));
if (emitter != 0) {
std::string ui = uiNameOf(callObj(emitter, "getEmitterDataBlock").c_str());
writeLine(file, "+-EMITTER " + ui + "\" " + emitterDir);
} else if (!emitterDir.empty() && std::atof(emitterDir.c_str()) != 0.0) {
writeLine(file, "+-EMITTER NONE\" " + emitterDir);
}
const char *lightId = fieldOf(brick, kLight);
ADDR light = emptyId(lightId) ? 0 : tsf_FindObject((unsigned int)std::atoi(lightId));
if (light != 0) {
std::string ui = uiNameOf(callObj(light, "getDataBlock").c_str());
writeLine(file, "+-LIGHT " + ui + "\" " + fieldOf(light, kEnable));
}
const char *itemId = fieldOf(brick, kItem);
ADDR item = emptyId(itemId) ? 0 : tsf_FindObject((unsigned int)std::atoi(itemId));
std::string itemPos = fieldOf(brick, kItemPosition);
std::string itemDir = fieldOf(brick, kItemDirection);
std::string itemTime = fieldOf(brick, kItemRespawnTime);
if (item != 0) {
std::string ui = uiNameOf(callObj(item, "getDataBlock").c_str());
writeLine(file, "+-ITEM " + ui + "\" " + itemPos + " " + itemDir + " " + itemTime);
} else if ((!itemDir.empty() && std::atof(itemDir.c_str()) != 2.0)
|| std::atof(itemPos.c_str()) != 0.0
|| (std::atof(itemTime.c_str()) != 0.0 && std::atof(itemTime.c_str()) != 4000.0)) {
writeLine(file, "+-ITEM NONE\" " + itemPos + " " + itemDir + " " + itemTime);
}
const char *audioId = fieldOf(brick, kAudioEmitter);
ADDR audio = emptyId(audioId) ? 0 : tsf_FindObject((unsigned int)std::atoi(audioId));
if (audio != 0) {
std::string ui = uiNameOf(callObj(audio, "getProfileId").c_str());
writeLine(file, "+-AUDIOEMITTER " + ui + "\" ");
}
const char *vehicleId = fieldOf(brick, kVehicleSpawnMarker);
ADDR vehicle = emptyId(vehicleId) ? 0 : tsf_FindObject((unsigned int)std::atoi(vehicleId));
if (vehicle != 0)
writeLine(file, "+-VEHICLE " + callObj(vehicle, "getUiName") + "\" " + callObj(vehicle, "getReColorVehicle"));
return true;
}
static bool gameDir(char *path)
{
int n = (int)GetModuleFileNameA(0, path, MAX_PATH);
if (n <= 0 || n >= MAX_PATH)
return false;
for (int i = n - 1; i >= 0; --i) {
if (path[i] == '\\' || path[i] == '/') {
path[i] = '\0';
break;
}
}
return true;
}
static bool gameFile(char *out, int outLen, const char *leaf)
{
char path[MAX_PATH];
if (!gameDir(path))
return false;
if (std::snprintf(out, outLen, "%s\\saves\\%s", path, leaf) >= outLen)
return false;
return true;
}
static void clampPrefs()
{
if (gInterval < 30)
gInterval = 30;
if (gInterval > 86400)
gInterval = 86400;
if (gChunk < 1)
gChunk = 1;
if (gChunk > 20000)
gChunk = 20000;
if (gTickMs < 1)
gTickMs = 1;
if (gTickMs > 1000)
gTickMs = 1000;
}
/* Settings live in $Pref::Server::AutoSave::* so the game writes them to
config/server/prefs.cs with the rest of the server prefs. They are read
when hosting starts, because prefs.cs has been executed by then. */
static void prefsToScript()
{
char code[512];
std::snprintf(code, sizeof(code),
"$Pref::Server::AutoSave::Enabled = %d;"
"$Pref::Server::AutoSave::Interval = %d;"
"$Pref::Server::AutoSave::Chunk = %d;"
"$Pref::Server::AutoSave::Tick = %d;"
"$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);
}
static void prefsFromScript()
{
std::string value;
value = varOf("$Pref::Server::AutoSave::Enabled");
if (!value.empty())
gEnabled = truthy(value);
value = varOf("$Pref::Server::AutoSave::Interval");
if (!value.empty())
gInterval = std::atoi(value.c_str());
value = varOf("$Pref::Server::AutoSave::Chunk");
if (!value.empty())
gChunk = std::atoi(value.c_str());
value = varOf("$Pref::Server::AutoSave::Tick");
if (!value.empty())
gTickMs = std::atoi(value.c_str());
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();
}
/* Same export the game runs when custom game settings change mid-game.
Only called while hosting, after prefs.cs has been loaded. */
static void exportPrefs()
{
tsf_Eval(
"export(\"$Pref::Server::*\", \"config/server/prefs.cs\", 0);"
"export(\"$Pref::Net::PacketRateToClient\", \"config/server/prefs.cs\", 1);"
"export(\"$Pref::Net::PacketRateToServer\", \"config/server/prefs.cs\", 1);"
"export(\"$Pref::Net::PacketSize\", \"config/server/prefs.cs\", 1);"
"export(\"$Pref::Net::LagThreshold\", \"config/server/prefs.cs\", 1);");
}
static bool hosting()
{
ADDR connection = tsf_FindObject("ServerConnection");
if (connection != 0)
return truthy(callObj(connection, "isLocal"));
if (truthy(varOf("$Server::Dedicated")))
return true;
std::string type = varOf("$Server::ServerType");
return type == "SinglePlayer" || type == "MultiPlayer";
}
static bool schedulePending(ADDR obj)
{
std::string id;
if (obj == 0)
return false;
id = dataField(obj, "chainDeleteSchedule", 0);
if (id.empty() || id == "0")
return false;
return truthy(callFn1("isEventPending", id.c_str()));
}
static bool chainDeleting()
{
ADDR root;
bool native;
int count;
int i;
root = tsf_FindObject("mainBrickGroup");
if (root == 0)
return false;
if (schedulePending(root))
return true;
native = layoutMatches(root);
count = native ? nativeCount(root) : std::atoi(callObj(root, "getCount").c_str());
if (count < 0 || count > 100000)
return false;
for (i = 0; i < count; ++i) {
ADDR child = native ? nativeObject(root, i) : scriptObject(root, i);
if (child == 0)
continue;
if (schedulePending(child))
return true;
if (!layoutMatches(child))
continue;
if (nativeCount(child) <= 0)
continue;
if (schedulePending(nativeObject(child, 0)))
return true;
}
return false;
}
static bool saveBusy()
{
if (truthy(varOf("$Game::MissionCleaningUp")))
return true;
if (chainDeleting())
return true;
if (gLoading)
return true;
std::string id = varOf("$LoadSaveFile_Tick_Schedule");
if (id.empty() || id == "0")
return false;
return truthy(callFn1("isEventPending", id.c_str()));
}
struct Group {
ADDR obj;
bool native;
bool descend;
};
static const int kSortLimit = 75000;
static const int kMaxBricks = 8000000;
static const unsigned int kSliceMs = 8;
static const unsigned int kSliceGapMs = 12;
static std::vector<Group> gGroups;
static std::vector<Row> gRows;
static int gGroupPos = 0;
static int gBrickPos = 0;
static bool gAnyValid = false;
static bool gOwnership = false;
static bool gExtended = false;
static bool gLocal = false;
static bool gDirect = false;
static int gTotal = 0;
static size_t gWritePos = 0;
static int gWritten = 0;
static unsigned int gStarted = 0;
static unsigned int gReported = 0;
static unsigned int gNextAllowed = 0;
static int gPumping = 0;
static long gLinecountAt = -1;
static FILE *gFile = 0;
static int gPhase = 0;
static char gTempPath[MAX_PATH];
static char gFinalPath[MAX_PATH];
static char gSaveLabel[64];
static int clampChunk()
{
if (gChunk < 1)
return 1;
if (gChunk > 20000)
return 20000;
return gChunk;
}
static int clampTick()
{
if (gTickMs < 1)
return 1;
if (gTickMs > 1000)
return 1000;
return gTickMs;
}
static int groupCount(const Group &group)
{
int count = group.native ? nativeCount(group.obj) : std::atoi(callObj(group.obj, "getCount").c_str());
if (count < 0 || count > kMaxBricks)
return 0;
return count;
}
static ADDR groupObject(const Group &group, int index)
{
if (group.native)
return nativeObject(group.obj, index);
return scriptObject(group.obj, index);
}
static bool sliceExpired(unsigned long long start)
{
static LARGE_INTEGER freq;
static int ready = 0;
LARGE_INTEGER now;
if (!ready) {
QueryPerformanceFrequency(&freq);
ready = freq.QuadPart > 0;
}
QueryPerformanceCounter(&now);
if (!ready)
return false;
return (unsigned long long)(now.QuadPart - (LONGLONG)start) * 1000ull >= (unsigned long long)freq.QuadPart * kSliceMs;
}
static unsigned long long sliceStamp()
{
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return (unsigned long long)now.QuadPart;
}
static void clearSave()
{
if (gFile != 0) {
std::fclose(gFile);
gFile = 0;
DeleteFileA(gTempPath);
}
gGroups.clear();
gRows.clear();
gPhase = 0;
gDirect = false;
gLinecountAt = -1;
gNextAllowed = 0;
}
static void scheduleChunk()
{
char code[96];
unsigned int now = GetTickCount();
int delay = clampTick();
if (now < gNextAllowed && gNextAllowed - now > (unsigned int)delay)
delay = (int)(gNextAllowed - now);
if (delay < (int)kSliceGapMs)
delay = (int)kSliceGapMs;
if (delay > 1000)
delay = 1000;
std::snprintf(code, sizeof(code), "$AutoSave::ChunkSchedule = schedule(%d, 0, autoSaveChunk);", delay);
tsf_Eval(code);
}
static void noteProgress()
{
unsigned int now = GetTickCount();
if (now - gReported < 2000)
return;
gReported = now;
if (gDirect)
BlPrintf("%s: %d / %d bricks", PROJECT_NAME, gWritten, gTotal);
else if (gPhase == 1)
BlPrintf("%s: gathered %u / %d bricks", PROJECT_NAME, (unsigned)gRows.size(), gTotal);
else
BlPrintf("%s: %u / %u bricks", PROJECT_NAME, (unsigned)gWritePos, (unsigned)gRows.size());
}
static bool openSaveFile()
{
char dir[MAX_PATH];
if (!gameFile(dir, MAX_PATH, ""))
return false;
size_t len = std::strlen(dir);
if (len > 0 && (dir[len - 1] == '\\' || dir[len - 1] == '/'))
dir[len - 1] = '\0';
CreateDirectoryA(dir, 0);
char folder[MAX_PATH];
if (std::snprintf(folder, MAX_PATH, "%s\\autosave", dir) >= MAX_PATH)
return false;
CreateDirectoryA(folder, 0);
SYSTEMTIME st;
GetLocalTime(&st);
char desc[64];
std::snprintf(gSaveLabel, sizeof(gSaveLabel), "%04u-%02u-%02u %02u-%02u-%02u", (unsigned)st.wYear, (unsigned)st.wMonth, (unsigned)st.wDay, (unsigned)st.wHour, (unsigned)st.wMinute, (unsigned)st.wSecond);
std::snprintf(desc, sizeof(desc), "%04u-%02u-%02u %02u:%02u:%02u", (unsigned)st.wYear, (unsigned)st.wMonth, (unsigned)st.wDay, (unsigned)st.wHour, (unsigned)st.wMinute, (unsigned)st.wSecond);
int n = 1;
for (;;) {
char leaf[160];
if (n == 1)
std::snprintf(leaf, sizeof(leaf), "autosave\\%s.bls", gSaveLabel);
else
std::snprintf(leaf, sizeof(leaf), "autosave\\%s (%d).bls", gSaveLabel, n);
if (!gameFile(gFinalPath, MAX_PATH, leaf))
return false;
if (GetFileAttributesA(gFinalPath) == INVALID_FILE_ATTRIBUTES)
break;
n++;
if (n > 50)
return false;
}
if (!gameFile(gTempPath, MAX_PATH, "autosave\\writing.bls.tmp"))
return false;
gFile = std::fopen(gTempPath, "wb");
if (gFile == 0)
return false;
setvbuf(gFile, 0, _IOFBF, 1 << 20);
writeLine(gFile, "This is a Blockland save file. You probably shouldn't modify it cause you'll screw it up.");
writeLine(gFile, "1");
writeLine(gFile, desc);
for (int i = 0; i < 64; ++i) {
char index[16];
std::snprintf(index, sizeof(index), "%d", i);
writeLine(gFile, callFn1("getColorIDTable", index));
}
char header[32];
if (gDirect) {
std::snprintf(header, sizeof(header), "Linecount %10u", 0u);
gLinecountAt = std::ftell(gFile);
if (gLinecountAt < 0 || std::strlen(header) != 20) {
std::fclose(gFile);
gFile = 0;
DeleteFileA(gTempPath);
return false;
}
std::fwrite(header, 1, 20, gFile);
std::fputc('\r', gFile);
std::fputc('\n', gFile);
} else {
gLinecountAt = -1;
std::snprintf(header, sizeof(header), "Linecount %u", (unsigned)gRows.size());
writeLine(gFile, header);
}
return true;
}
static void patchLinecount()
{
char header[32];
long back;
if (gFile == 0 || gLinecountAt < 0)
return;
std::snprintf(header, sizeof(header), "Linecount %10u", (unsigned)gWritten);
if (std::strlen(header) != 20)
return;
std::fflush(gFile);
back = std::ftell(gFile);
if (std::fseek(gFile, gLinecountAt, SEEK_SET) != 0)
return;
std::fwrite(header, 1, 20, gFile);
std::fflush(gFile);
if (back >= 0)
std::fseek(gFile, back, SEEK_SET);
}
static void serverChat(const char *text)
{
char code[768];
if (std::snprintf(code, sizeof(code), "if (isFunction(\"messageAll\")) messageAll('', \"%s\");", text) >= (int)sizeof(code))
return;
tsf_Eval(code);
}
/* The server preview raytracer (startRaytracer/stopRaytracer) traces a
768x768 overhead image a few pixels per tick. When it finishes it encodes
a PNG into a buffer at raytracer+0x88 (size +0x8c), restarts itself, and
hands the buffer to an upload thread for the master server. AutoSave keeps
a copy of that image as the save's .jpg. When the game never asked for the
preview, the restart is stopped and nothing is uploaded. */
typedef void (*ConsoleVoidFn)(ADDR, int, const char **);
typedef void (__thiscall *TraceBeginFn)(ADDR);
typedef void (*TraceUploadFn)(void *);
typedef void (__fastcall *EngineFreeFn)(void *);
struct GdipStartupInput {
UINT32 GdiplusVersion;
void *DebugEventCallback;
BOOL SuppressBackgroundThread;
BOOL SuppressExternalCodecs;
};
struct GdipEncoderParameter {
GUID Guid;
ULONG NumberOfValues;
ULONG Type;
void *Value;
};
struct GdipEncoderParameters {
UINT Count;
GdipEncoderParameter Parameter[1];
};
extern "C" {
int WINAPI GdiplusStartup(ULONG_PTR *, const GdipStartupInput *, void *);
int WINAPI GdipCreateBitmapFromStream(IStream *, void **);
int WINAPI GdipSaveImageToFile(void *, const WCHAR *, const CLSID *, const GdipEncoderParameters *);
int WINAPI GdipDisposeImage(void *);
IStream *WINAPI SHCreateMemStream(const BYTE *, UINT);
}
static ADDR gTracer = 0;
static ConsoleVoidFn gTraceStartOriginal = 0;
static ConsoleVoidFn gTraceStopOriginal = 0;
static TraceBeginFn gTraceBeginOriginal = 0;
static TraceUploadFn gTraceUploadOriginal = 0;
static EngineFreeFn gEngineFree = 0;
static bool gTraceHooked = false;
static int gTraceStarting = 0;
static volatile LONG gShotWanted = 0;
static volatile LONG gGameWantsTrace = 0;
static volatile LONG gShotResult = 0;
static CRITICAL_SECTION gShotLock;
static char gShotPath[MAX_PATH];
static char gShotDone[MAX_PATH];
static ULONG_PTR gGdipToken = 0;
static bool gGdipReady = false;
static bool writePreview(const BYTE *png, int size, const char *path)
{
static const CLSID jpegClsid = {0x557cf401, 0x1a04, 0x11d3, {0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e}};
static const GUID qualityGuid = {0x1d5be4b5, 0xfa4a, 0x452d, {0x9c, 0xdd, 0x5d, 0xb3, 0x51, 0x05, 0xe7, 0xeb}};
char temp[MAX_PATH];
WCHAR wide[MAX_PATH];
IStream *stream;
void *image = 0;
ULONG quality = 90;
GdipEncoderParameters params;
bool ok;
if (png == 0 || size <= 0)
return false;
if (!gGdipReady) {
GdipStartupInput input = {1, 0, FALSE, FALSE};
if (GdiplusStartup(&gGdipToken, &input, 0) != 0)
return false;
gGdipReady = true;
}
if (std::snprintf(temp, sizeof(temp), "%s.tmp", path) >= (int)sizeof(temp))
return false;
if (MultiByteToWideChar(CP_ACP, 0, temp, -1, wide, MAX_PATH) == 0)
return false;
stream = SHCreateMemStream(png, (UINT)size);
if (stream == 0)
return false;
ok = GdipCreateBitmapFromStream(stream, &image) == 0 && image != 0;
if (ok) {
params.Count = 1;
params.Parameter[0].Guid = qualityGuid;
params.Parameter[0].NumberOfValues = 1;
params.Parameter[0].Type = 4;
params.Parameter[0].Value = &quality;
ok = GdipSaveImageToFile(image, wide, &jpegClsid, &params) == 0;
}
if (image != 0)
GdipDisposeImage(image);
stream->Release();
if (ok && !MoveFileExA(temp, path, MOVEFILE_REPLACE_EXISTING))
ok = false;
if (!ok)
DeleteFileA(temp);
return ok;
}
/* Main thread only. */
static bool gShotActive = false;
static DWORD gShotStarted = 0;
static DWORD gShotLogged = 0;
static int gSavedWork = -1;
/* The engine clamps the work per tick to 1..128 and starts at 64. AutoSave's
own pictures use 128 and put the old value back afterwards. */
static void raiseWork()
{
int *work = (int *)(gTracer + 0x10);
if (gSavedWork >= 0 || *work >= 128)
return;
gSavedWork = *work;
*work = 128;
}
static void restoreWork()
{
if (gSavedWork < 0)
return;
*(int *)(gTracer + 0x10) = gSavedWork;
gSavedWork = -1;
}
static void traceStartHook(ADDR obj, int argc, const char **argv)
{
InterlockedExchange(&gGameWantsTrace, 1);
restoreWork();
gTraceStarting++;
gTraceStartOriginal(obj, argc, argv);
gTraceStarting--;
}
static void traceStopHook(ADDR obj, int argc, const char **argv)
{
InterlockedExchange(&gGameWantsTrace, 0);
InterlockedExchange(&gShotWanted, 0);
restoreWork();
if (gShotActive) {
gShotActive = false;
BlPrintf("%s: screenshot stopped by the game", PROJECT_NAME);
}
gTraceStopOriginal(obj, argc, argv);
}
static void __thiscall traceBeginHook(ADDR tracer)
{
gTraceBeginOriginal(tracer);
/* Called outside startRaytracer only when an image just finished. */
if (gTraceStarting == 0 && gGameWantsTrace == 0) {
*(BYTE *)(tracer + 0x84) = 0;
*(int *)(tracer + 0x0c) = 0;
restoreWork();
}
}
static void traceUploadHook(void *arg)
{
BYTE *png = *(BYTE **)(gTracer + 0x88);
int size = *(int *)(gTracer + 0x8c);
if (InterlockedExchange(&gShotWanted, 0)) {
char path[MAX_PATH];
EnterCriticalSection(&gShotLock);
std::memcpy(path, gShotPath, sizeof(path));
LeaveCriticalSection(&gShotLock);
bool ok = writePreview(png, size, path);
EnterCriticalSection(&gShotLock);
std::memcpy(gShotDone, path, sizeof(gShotDone));
LeaveCriticalSection(&gShotLock);
InterlockedExchange(&gShotResult, ok ? 1 : 2);
}
if (gGameWantsTrace) {
gTraceUploadOriginal(arg);
return;
}
if (png != 0)
gEngineFree(png);
}
static void serverChat(const char *text);
static bool notePreview()
{
LONG result = InterlockedExchange(&gShotResult, 0);
char path[MAX_PATH];
char text[384];
if (result == 0)
return false;
EnterCriticalSection(&gShotLock);
std::memcpy(path, gShotDone, sizeof(path));
LeaveCriticalSection(&gShotLock);
const char *slash = std::strrchr(path, '\\');
const char *base = slash != 0 ? slash + 1 : path;
if (result == 1) {
BlPrintf("%s: screenshot %u s -> saves/autosave/%s", PROJECT_NAME, (unsigned)((GetTickCount() - gShotStarted) / 1000u), base);
std::snprintf(text, sizeof(text), "\\c6[\\c3AutoSave\\c6] \\c6Screenshot saved to \\c3saves/autosave/%s\\c6.", base);
serverChat(text);
} else {
BlPrintf("%s: could not write the screenshot saves/autosave/%s", PROJECT_NAME, base);
}
return true;
}
/* The engine traces at most 128 pixels per tick. AutoSave also traces
pixels itself in short slices, through the same per-pixel function, and
leaves the last pixel to the engine so its normal finish still runs. */
typedef void (__fastcall *TracePixelFn)(int, int);
static TracePixelFn gTracePixel = 0;
static const unsigned int kTraceSliceMs = 6;
static const unsigned int kTraceGapMs = 12;
static void scheduleTraceStep()
{
char code[160];
std::snprintf(code, sizeof(code),
"if (isEventPending($AutoSave::TraceSchedule)) cancel($AutoSave::TraceSchedule);"
"$AutoSave::TraceSchedule = schedule(%u, 0, autoSaveTraceStep);", kTraceGapMs);
tsf_Eval(code);
}
static void autoSaveTraceStep(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
if (!gShotActive || gShotWanted == 0 || gTracePixel == 0 || *(BYTE *)(gTracer + 0x84) == 0)
return;
if (gPhase == 0) {
ADDR bitmap = *(ADDR *)(gTracer + 0x04);
int width = bitmap != 0 ? *(int *)(bitmap + 0x14) : 0;
int height = bitmap != 0 ? *(int *)(bitmap + 0x18) : 0;
int total = width * height;
int index = *(int *)(gTracer + 0x08);
if (width > 0 && height > 0 && index >= 0 && index < total - 1) {
LARGE_INTEGER freq;
LARGE_INTEGER start;
LARGE_INTEGER now;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
long long budget = (long long)freq.QuadPart * kTraceSliceMs / 1000;
while (index < total - 1) {
gTracePixel(index % width, index / width);
index++;
*(int *)(gTracer + 0x08) = index;
if ((index & 15) == 0) {
QueryPerformanceCounter(&now);
if (now.QuadPart - start.QuadPart >= budget)
break;
}
}
}
}
scheduleTraceStep();
}
static void schedulePreviewPoll()
{
tsf_Eval("if (isEventPending($AutoSave::PreviewSchedule)) cancel($AutoSave::PreviewSchedule);"
"$AutoSave::PreviewSchedule = schedule(2000, 0, autoSavePreviewPoll);");
}
static void autoSavePreviewPoll(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
if (notePreview()) {
gShotActive = false;
return;
}
if (!gShotActive)
return;
DWORD now = GetTickCount();
if (now - gShotStarted > 20u * 60u * 1000u) {
gShotActive = false;
InterlockedExchange(&gShotWanted, 0);
BlPrintf("%s: screenshot gave up after 20 minutes", PROJECT_NAME);
return;
}
if (now - gShotLogged >= 30000u && gShotWanted != 0) {
ADDR bitmap = *(ADDR *)(gTracer + 0x04);
int done = *(int *)(gTracer + 0x08);
int total = bitmap != 0 ? *(int *)(bitmap + 0x14) * *(int *)(bitmap + 0x18) : 0;
gShotLogged = now;
if (total > 0)
BlPrintf("%s: screenshot %d%%", PROJECT_NAME, (int)((long long)done * 100 / total));
}
schedulePreviewPoll();
}
static void requestPreview()
{
char path[MAX_PATH];
size_t len;
if (!gPreview || !gTraceHooked)
return;
notePreview();
std::snprintf(path, sizeof(path), "%s", gFinalPath);
len = std::strlen(path);
if (len < 4 || _stricmp(path + len - 4, ".bls") != 0)
return;
std::memcpy(path + len - 4, ".jpg", 4);
EnterCriticalSection(&gShotLock);
std::memcpy(gShotPath, path, sizeof(gShotPath));
LeaveCriticalSection(&gShotLock);
InterlockedExchange(&gShotWanted, 1);
gShotActive = true;
gShotStarted = GetTickCount();
gShotLogged = gShotStarted;
if (*(BYTE *)(gTracer + 0x84) == 0) {
const char *args[1] = {"startRaytracer"};
gTraceStarting++;
gTraceStartOriginal(0, 1, args);
gTraceStarting--;
raiseWork();
BlPrintf("%s: tracing a screenshot", PROJECT_NAME);
} else {
BlPrintf("%s: screenshot will use the server preview that is already tracing", PROJECT_NAME);
}
schedulePreviewPoll();
scheduleTraceStep();
}
static void cancelPreview()
{
if (!gTraceHooked)
return;
InterlockedExchange(&gShotWanted, 0);
gShotActive = false;
if (gGameWantsTrace == 0 && *(BYTE *)(gTracer + 0x84) != 0) {
*(BYTE *)(gTracer + 0x84) = 0;
*(int *)(gTracer + 0x0c) = 0;
}
restoreWork();
}
static void installPreview()
{
ADDR start = tsh_ScanFunctionHex((char *)"68 ? ? ? ? C7 05 ? ? ? ? 00 00 80 BF C7 05 ? ? ? ? 00 00 80 BF C7 05 ? ? ? ? 00 00 80 BF C7 05 ? ? ? ? 00 00 80 BF C7 05 ? ? ? ? 00 00 80 BF C7 05 ? ? ? ? 00 00 00 00");
ADDR stop = tsh_ScanFunctionHex((char *)"C6 05 ? ? ? ? 00 C3 CC CC CC CC CC CC CC CC 8B 44 24 08 0F AF 44 24 0C C3");
ADDR begin = tsh_ScanFunctionHex((char *)"55 8B EC 83 E4 F8 83 EC 30 0F 57 D2 56 8B F1 57 80 BE 85 00 00 00 00");
ADDR upload = tsh_ScanFunctionHex((char *)"81 EC 4C 01 00 00 A1 ? ? ? ? 33 C4 89 84 24 48 01 00 00 83 3D ? ? ? ? 00");
ADDR engineFree = tsh_ScanFunctionHex((char *)"51 53 55 56 8B 35 ? ? ? ? 57 8B F9 F7 D6 23 F7 3B F7");
ADDR pixel = tsh_ScanFunctionHex((char *)"55 8B EC 83 E4 F8 83 EC 54 53 56 8B 35 ? ? ? ? 8B DA 57 BF 01 00 00 00 89 4C 24 20");
InitializeCriticalSection(&gShotLock);
if (!start || !stop || !begin || !upload || !engineFree) {
BlPrintf("%s: server preview was not found, saves have no picture", PROJECT_NAME);
return;
}
gTracer = *(ADDR *)(upload + 0x16);
if (*(ADDR *)(stop + 2) != gTracer + 0x84) {
BlPrintf("%s: server preview layout differed, saves have no picture", PROJECT_NAME);
return;
}
gEngineFree = (EngineFreeFn)engineFree;
if (pixel != 0 && *(ADDR *)(pixel + 0x0d) == gTracer + 0x04)
gTracePixel = (TracePixelFn)pixel;
else
BlPrintf("%s: screenshots trace at the engine's rate only", PROJECT_NAME);
if (MH_CreateHook((void *)start, (void *)&traceStartHook, (void **)&gTraceStartOriginal) != MH_OK
|| MH_CreateHook((void *)stop, (void *)&traceStopHook, (void **)&gTraceStopOriginal) != MH_OK
|| MH_CreateHook((void *)begin, (void *)&traceBeginHook, (void **)&gTraceBeginOriginal) != MH_OK
|| MH_CreateHook((void *)upload, (void *)&traceUploadHook, (void **)&gTraceUploadOriginal) != MH_OK) {
BlPrintf("%s: server preview hook failed, saves have no picture", PROJECT_NAME);
return;
}
if (MH_EnableHook((void *)start) != MH_OK || MH_EnableHook((void *)stop) != MH_OK
|| MH_EnableHook((void *)begin) != MH_OK || MH_EnableHook((void *)upload) != MH_OK) {
BlPrintf("%s: server preview hook failed, saves have no picture", PROJECT_NAME);
return;
}
gTraceHooked = true;
}
static void finishSave()
{
unsigned int elapsed;
patchLinecount();
std::fflush(gFile);
std::fclose(gFile);
gFile = 0;
gLinecountAt = -1;
const char *slash = std::strrchr(gFinalPath, '\\');
const char *base = slash != 0 ? slash + 1 : gFinalPath;
if (!MoveFileExA(gTempPath, gFinalPath, MOVEFILE_REPLACE_EXISTING)) {
BlPrintf("%s: could not write saves/autosave/%s", PROJECT_NAME, base);
DeleteFileA(gTempPath);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Could not write the save.");
return;
}
elapsed = GetTickCount() - gStarted;
BlPrintf("%s: %d bricks in %u.%03us -> saves/autosave/%s", PROJECT_NAME, gWritten, elapsed / 1000u, elapsed % 1000u, base);
{
char text[512];
std::snprintf(text, sizeof(text), "\\c6[\\c3AutoSave\\c6] \\c6Saved \\c3%d\\c6 bricks to \\c3saves/autosave/%s\\c6.", gWritten, base);
serverChat(text);
}
requestPreview();
markSaved();
gGroups.clear();
gRows.clear();
gPhase = 0;
}
static void pumpSave()
{
int budget;
unsigned long long stamp;
bool openedThisSlice;
if (gPhase == 0 || gPumping)
return;
gPumping = 1;
if (gLoading) {
BlPrintf("%s: bricks are loading, save stopped", PROJECT_NAME);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Save stopped while bricks are loading.");
gPumping = 0;
return;
}
if (chainDeleting()) {
BlPrintf("%s: bricks are clearing, save stopped", PROJECT_NAME);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Save stopped while bricks are clearing.");
gPumping = 0;
return;
}
if (!hosting() || saveBusy()) {
BlPrintf("%s: save cancelled", PROJECT_NAME);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Save cancelled.");
gPumping = 0;
return;
}
if (GetTickCount() < gNextAllowed) {
scheduleChunk();
gPumping = 0;
return;
}
budget = clampChunk();
stamp = sliceStamp();
openedThisSlice = false;
if (gDirect && gFile == 0) {
if (!openSaveFile()) {
BlPrintf("%s: could not write saves/autosave/writing.bls.tmp", PROJECT_NAME);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Could not write the save.");
gPumping = 0;
return;
}
}
if (gPhase == 1) {
while (budget > 0 && !sliceExpired(stamp) && gGroupPos < (int)gGroups.size()) {
Group group = gGroups[gGroupPos];
int count = groupCount(group);
ADDR obj;
int kind;
if (gBrickPos >= count) {
gGroupPos++;
gBrickPos = 0;
continue;
}
obj = groupObject(group, gBrickPos);
gBrickPos++;
budget--;
kind = group.descend ? classify(obj) : (plantedBrick(obj) ? 1 : 0);
if (kind == 1) {
if (gDirect) {
if (writeBrick(gFile, obj, gOwnership, gExtended, gLocal))
gWritten++;
} else
remember(obj, gRows, gAnyValid);
} else if (kind == 2 && group.descend) {
bool nativeChild = layoutMatches(obj);
int nested = 0;
if (nativeChild)
nested = nativeCount(obj);
else {
std::string cls = callObj(obj, "getClassName");
if (cls == "SimGroup" || cls == "SimSet" || cls == "BrickGroup")
nested = std::atoi(callObj(obj, "getCount").c_str());
}
if (nested > 0 && nested <= kMaxBricks) {
Group child;
child.obj = obj;
child.native = nativeChild;
child.descend = false;
gGroups.push_back(child);
}
}
if (gBrickPos >= count) {
gGroupPos++;
gBrickPos = 0;
}
}
if (gGroupPos >= (int)gGroups.size()) {
if (gDirect) {
if (gWritten == 0) {
BlPrintf("%s: no bricks to save", PROJECT_NAME);
clearSave();
markSaved();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6No bricks to save.");
gPumping = 0;
return;
}
finishSave();
} else if (gRows.empty()) {
BlPrintf("%s: no bricks to save", PROJECT_NAME);
clearSave();
markSaved();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6No bricks to save.");
gPumping = 0;
return;
} else {
if (gAnyValid) {
std::stable_sort(gRows.begin(), gRows.end(), [](const Row &a, const Row &b) {
return a.height < b.height;
});
}
if (!openSaveFile()) {
BlPrintf("%s: could not write saves/autosave/writing.bls.tmp", PROJECT_NAME);
clearSave();
serverChat("\\c6[\\c3AutoSave\\c6] \\c6Could not write the save.");
gPumping = 0;
return;
}
gPhase = 2;
gWritePos = 0;
gWritten = 0;
openedThisSlice = true;
}
}
}
if (gPhase == 2 && !openedThisSlice && budget > 0 && !sliceExpired(stamp)) {
while (budget > 0 && !sliceExpired(stamp) && gWritePos < gRows.size()) {
if (writeBrick(gFile, gRows[gWritePos].obj, gOwnership, gExtended, gLocal))
gWritten++;
gWritePos++;
budget--;
}
std::fflush(gFile);
if (gWritePos >= gRows.size())
finishSave();
} else if (gDirect && gFile != 0) {
std::fflush(gFile);
}
if (gPhase != 0) {
noteProgress();
gNextAllowed = GetTickCount() + kSliceGapMs;
scheduleChunk();
}
gPumping = 0;
}
static int beginSave()
{
ADDR root;
ADDR connection;
bool native;
int groups;
int total;
if (gPhase != 0 || !hosting())
return -1;
if (gLoading) {
BlPrintf("%s: bricks are loading, skipped", PROJECT_NAME);
return -5;
}
if (chainDeleting()) {
BlPrintf("%s: bricks are clearing, skipped", PROJECT_NAME);
return -4;
}
if (saveBusy())
return -1;
if (unchanged()) {
BlPrintf("%s: no changes, skipped", PROJECT_NAME);
return -3;
}
gChangeAtStart = gChange;
root = tsf_FindObject("mainBrickGroup");
if (root == 0)
return -1;
gGroups.clear();
gRows.clear();
gAnyValid = false;
gGroupPos = 0;
gBrickPos = 0;
gWritePos = 0;
gWritten = 0;
gFile = 0;
gLinecountAt = -1;
gDirect = false;
gStarted = GetTickCount();
gReported = gStarted;
gDatablocks.clear();
gPrintTokens.clear();
gVerifyLeft = 64;
gNextAllowed = 0;
gLocal = false;
connection = tsf_FindObject("ServerConnection");
if (connection != 0)
gLocal = truthy(callObj(connection, "isLocal"));
native = layoutMatches(root);
if (!native)
BlPrintf("%s: brick group layout differed, walking it through the console", PROJECT_NAME);
groups = native ? nativeCount(root) : std::atoi(callObj(root, "getCount").c_str());
if (groups < 0 || groups > kMaxBricks)
return 0;
total = 0;
if (groups > kSortLimit)
total = groups;
else {
for (int g = 0; g < groups; ++g) {
ADDR child = native ? nativeObject(root, g) : scriptObject(root, g);
if (child == 0)
continue;
if (callObj(child, "getClassName") == "fxDTSBrick")
total++;
else if (layoutMatches(child)) {
int nested = nativeCount(child);
if (nested > 0 && nested <= kMaxBricks)
total += nested;
} else
total++;
}
}
gTotal = total;
gDirect = total > kSortLimit;
if (!gDirect && gLocal && truthy(callFn1("isFunction", "saveBricks_ProcessWrenchExtras")))
tsf_Eval("saveBricks_ProcessWrenchExtras();");
Group rootEntry;
rootEntry.obj = root;
rootEntry.native = native;
rootEntry.descend = true;
gGroups.push_back(rootEntry);
gOwnership = truthy(varOf("$pref::SaveOwnership"));
gExtended = truthy(varOf("$pref::SaveExtendedBrickInfo"));
gPhase = 1;
if (gDirect)
BlPrintf("%s: %d bricks, ground sort off", PROJECT_NAME, gTotal);
else
BlPrintf("%s: saving %d bricks", PROJECT_NAME, gTotal);
pumpSave();
if (gPhase == 0)
return gWritten;
{
char text[192];
std::snprintf(text, sizeof(text), "\\c6[\\c3AutoSave\\c6] \\c6Saving \\c3%d\\c6 bricks...", gTotal);
serverChat(text);
}
return -2;
}
static int clampInterval()
{
if (gInterval < 30)
return 30;
if (gInterval > 86400)
return 86400;
return gInterval;
}
static int gServerId = 0;
static void reschedule()
{
char code[128];
int delay = clampInterval() * 1000;
std::snprintf(code, sizeof(code), "$AutoSave::Schedule = schedule(%d, 0, autoSaveTick);", delay);
tsf_Eval(code);
}
static void autoSaveChunk(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
pumpSave();
}
static void autoSaveApply(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
clampPrefs();
if (!gPreview)
cancelPreview();
prefsToScript();
if (hosting())
exportPrefs();
tsf_Eval("if (isEventPending($AutoSave::Schedule)) cancel($AutoSave::Schedule);");
if (gArmed)
reschedule();
}
static const char kChatScript[] = R"TS(
function AutoSave_Admin(%client)
{
return isObject(%client) && (%client.isAdmin || %client.isSuperAdmin);
}
function AutoSave_Tell(%client, %line)
{
if (!isObject(%client))
{
echo("AutoSave: " @ %line);
return;
}
messageClient(%client, '', "\c6[\c3AutoSave\c6] \c6" @ %line);
}
function AutoSave_Help(%client)
{
AutoSave_Tell(%client, "/autosaver help");
AutoSave_Tell(%client, "/autosaver status");
AutoSave_Tell(%client, "/autosaver on");
AutoSave_Tell(%client, "/autosaver off");
AutoSave_Tell(%client, "/autosaver interval <seconds, 30-86400>");
AutoSave_Tell(%client, "/autosaver chunk <bricks, 1-20000>");
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 <optional autosave name>");
AutoSave_Tell(%client, "/autosaver autoload <on or off>");
}
function AutoSave_Busy()
{
if ($AutoSave::Loading)
return 1;
if (isObject($Server_LoadFileObj))
return 1;
if (isEventPending($LoadSaveFile_Tick_Schedule))
return 1;
if (!isObject(mainBrickGroup))
return 0;
if (isEventPending(mainBrickGroup.chainDeleteSchedule))
return 1;
%count = mainBrickGroup.getCount();
for (%i = 0; %i < %count; %i++)
{
%group = mainBrickGroup.getObject(%i);
if (isEventPending(%group.chainDeleteSchedule))
return 1;
if (isObject(%group) && %group.getCount() > 0)
{
%first = %group.getObject(0);
if (isObject(%first) && isEventPending(%first.chainDeleteSchedule))
return 1;
}
}
return 0;
}
function AutoSave_NewestFile()
{
return autoSaveNewest();
}
function AutoSave_ReloadTick()
{
if ($AutoSave::ReloadFile $= "")
return;
if (AutoSave_Busy())
{
$AutoSave::ReloadSchedule = schedule(100, 0, AutoSave_ReloadTick);
return;
}
%file = $AutoSave::ReloadFile;
$AutoSave::ReloadFile = "";
serverDirectSaveFileLoad(%file, 3, "", 1, 0);
}
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;
if (getBrickCount() > 0)
serverCmdClearAllBricks(%client);
if (getBrickCount() > 0 && !AutoSave_Busy())
{
$AutoSave::ReloadFile = "";
AutoSave_Tell(%client, "Could not clear the bricks.");
return;
}
AutoSave_ReloadTick();
}
function AutoSave_Command(%client, %cmd, %arg)
{
echo("AutoSave: /autosaver" SPC %cmd SPC %arg);
%cmd = strlwr(%cmd);
if (%cmd $= "" || %cmd $= "help")
{
AutoSave_Help(%client);
return;
}
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, autoload \c3" @ ($AutoSave::AutoLoad ? "on" : "off") @ "\c6.");
return;
}
if (!AutoSave_Admin(%client))
{
AutoSave_Tell(%client, "Only an admin can change autosave.");
return;
}
%name = %client.getPlayerName();
if (%cmd $= "on")
{
$AutoSave::Enabled = 1;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 turned autosave \c3on\c6.");
return;
}
if (%cmd $= "off")
{
$AutoSave::Enabled = 0;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 turned autosave \c3off\c6.");
return;
}
if (%cmd $= "save" || %cmd $= "now")
{
%result = autoSave();
if (%result $= "5")
AutoSave_Tell(%client, "Bricks are still loading.");
else if (%result $= "4")
AutoSave_Tell(%client, "Bricks are still being cleared.");
else if (%result $= "2")
AutoSave_Tell(%client, "No changes since the last save.");
else if (%result $= "0")
AutoSave_Tell(%client, "Could not start a save.");
return;
}
if (%cmd $= "load" || %cmd $= "reload")
{
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")
{
%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 screenshot on\c6 or \c3/autosaver screenshot off\c6.");
return;
}
$AutoSave::Preview = %on;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 turned save screenshots \c3" @ (%on ? "on" : "off") @ "\c6.");
return;
}
if (%cmd $= "interval" || %cmd $= "chunk" || %cmd $= "tick")
{
if (strlen(%arg) == 0 || stripChars(%arg, "0123456789") !$= "")
{
AutoSave_Tell(%client, "That needs to be a whole number.");
return;
}
%n = mFloor(%arg);
if (%cmd $= "interval")
{
if (%n < 30)
%n = 30;
if (%n > 86400)
%n = 86400;
$AutoSave::Interval = %n;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 set the interval to \c3" @ $AutoSave::Interval @ "\c6 seconds.");
}
else if (%cmd $= "chunk")
{
if (%n < 1)
%n = 1;
if (%n > 20000)
%n = 20000;
$AutoSave::Chunk = %n;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 set the slice to \c3" @ $AutoSave::Chunk @ "\c6 bricks.");
}
else
{
if (%n < 1)
%n = 1;
if (%n > 1000)
%n = 1000;
$AutoSave::Tick = %n;
autoSaveApply();
messageAll('', "\c6[\c3AutoSave\c6] \c3" @ %name @ "\c6 set the pause to \c3" @ $AutoSave::Tick @ "\c6 ms.");
}
return;
}
AutoSave_Tell(%client, "Unknown command. Type \c3/autosaver help\c6.");
}
function AutoSave_Join(%a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
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, %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 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 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 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";
static void installChat()
{
if (gChat || gInstalling)
return;
gInstalling = 1;
tsf_Eval(kChatScript);
gInstalling = 0;
gChat = truthy(varOf("$AutoSave::ChatInstalled"));
if (gChat)
BlPrintf("%s: /autosaver is ready", PROJECT_NAME);
else if (!gChatNoted) {
gChatNoted = true;
BlPrintf("%s: /autosaver failed to install", PROJECT_NAME);
}
}
static const char kWatchScript[] = R"TS(
function AutoSave_TouchAny(%obj)
{
if ($AutoSave::Loading || !isObject(%obj))
return;
if (%obj.getClassName() $= "fxDTSBrick" && !%obj.isPlanted())
return;
$AutoSave::Change++;
}
package AutoSaveWatch
{
function NMH_Type::send(%this)
{
%text = %this.getValue();
if (getSubStr(%text, 0, 1) $= "/")
{
%body = trim(getSubStr(%text, 1, 256));
if (strlwr(getWord(%body, 0)) $= "autosaver")
{
%client = findLocalClient();
if (isObject(%client))
AutoSave_Command(%client, getWord(%body, 1), getWords(%body, 2));
else
commandToServer('autosaver', getWord(%body, 1), getWords(%body, 2));
Canvas.popDialog(newMessageHud);
return;
}
}
Parent::send(%this);
}
function serverDirectSaveFileLoad(%filename, %colorMethod, %dirName, %ownership, %silent)
{
$AutoSave::Loading = 1;
Parent::serverDirectSaveFileLoad(%filename, %colorMethod, %dirName, %ownership, %silent);
if (!isObject($Server_LoadFileObj))
$AutoSave::Loading = 0;
}
function ServerLoadSaveFile_Start(%filename)
{
$AutoSave::Loading = 1;
Parent::ServerLoadSaveFile_Start(%filename);
if (!isObject($Server_LoadFileObj))
$AutoSave::Loading = 0;
}
function ServerLoadSaveFile_End()
{
Parent::ServerLoadSaveFile_End();
$AutoSave::Loading = 0;
$AutoSave::Change++;
if ($AutoSave::AutoLoading)
{
$AutoSave::AutoLoading = 0;
autoSaveBaseline();
}
}
function serverCmdCancelSaveFileUpload(%client)
{
Parent::serverCmdCancelSaveFileUpload(%client);
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)
{
if (isObject(%client.wrenchBrick))
$AutoSave::Change++;
Parent::serverCmdAddEvent(%client, %enabled, %inputEventIdx, %delay, %targetIdx, %NTNameIdx, %outputEventIdx, %par1, %par2, %par3, %par4);
}
function SimObject::clearEvents(%obj)
{
AutoSave_TouchAny(%obj);
Parent::clearEvents(%obj);
}
function SimObject::SetEventEnabled(%obj, %idxList, %val)
{
AutoSave_TouchAny(%obj);
Parent::SetEventEnabled(%obj, %idxList, %val);
}
function SimObject::ToggleEventEnabled(%obj, %idxList)
{
AutoSave_TouchAny(%obj);
Parent::ToggleEventEnabled(%obj, %idxList);
}
function SimObject::setNTObjectName(%obj, %name)
{
AutoSave_TouchAny(%obj);
Parent::setNTObjectName(%obj, %name);
}
function fxDTSBrick::onPlant(%obj)
{
Parent::onPlant(%obj);
$AutoSave::Change++;
}
function fxDTSBrick::onLoadPlant(%obj)
{
Parent::onLoadPlant(%obj);
$AutoSave::Change++;
}
function fxDTSBrick::onDeath(%obj)
{
$AutoSave::Change++;
Parent::onDeath(%obj);
}
function fxDTSBrick::onRemove(%obj)
{
$AutoSave::Change++;
Parent::onRemove(%obj);
}
function fxDTSBrick::setTransform(%obj, %transform)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setTransform(%obj, %transform);
}
function fxDTSBrick::setColor(%obj, %id)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setColor(%obj, %id);
}
function fxDTSBrick::setColorFX(%obj, %id)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setColorFX(%obj, %id);
}
function fxDTSBrick::setShapeFX(%obj, %id)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setShapeFX(%obj, %id);
}
function fxDTSBrick::setPrint(%obj, %id)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setPrint(%obj, %id);
}
function fxDTSBrick::setRayCasting(%obj, %val)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setRayCasting(%obj, %val);
}
function fxDTSBrick::setColliding(%obj, %val)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setColliding(%obj, %val);
}
function fxDTSBrick::setRendering(%obj, %val)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setRendering(%obj, %val);
}
function fxDTSBrick::setLight(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setLight(%obj, %data, %client);
}
function fxDTSBrick::setEmitter(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setEmitter(%obj, %data, %client);
}
function fxDTSBrick::setEmitterDirection(%obj, %dir)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setEmitterDirection(%obj, %dir);
}
function fxDTSBrick::setItem(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setItem(%obj, %data, %client);
}
function fxDTSBrick::setItemDirection(%obj, %dir)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setItemDirection(%obj, %dir);
}
function fxDTSBrick::setItemPosition(%obj, %pos)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setItemPosition(%obj, %pos);
}
function fxDTSBrick::setItemRespawntime(%obj, %time)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setItemRespawntime(%obj, %time);
}
function fxDTSBrick::setMusic(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setMusic(%obj, %data, %client);
}
function fxDTSBrick::setSound(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setSound(%obj, %data, %client);
}
function fxDTSBrick::setVehicle(%obj, %data, %client)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setVehicle(%obj, %data, %client);
}
function fxDTSBrick::setReColorVehicle(%obj, %val)
{
if (!$AutoSave::Loading && %obj.isPlanted())
$AutoSave::Change++;
Parent::setReColorVehicle(%obj, %val);
}
};
activatePackage(AutoSaveWatch);
$AutoSave::WatchInstalled = 1;
)TS";
static void installWatch()
{
if (gWatching || gInstalling)
return;
if (!truthy(callFn1("isFunction", "serverCmdAddEvent")))
return;
if (!truthy(callFn2("isFunction", "fxDTSBrick", "onPlant")))
return;
if (!truthy(callFn2("isFunction", "fxDTSBrick", "setLight")))
return;
gInstalling = 1;
tsf_Eval(kWatchScript);
gInstalling = 0;
gWatching = truthy(varOf("$AutoSave::WatchInstalled"));
if (gWatching)
BlPrintf("%s: saves only after brick changes", PROJECT_NAME);
}
static void autoSaveTick(ADDR obj, int argc, const char *argv[])
{
(void)obj;
(void)argc;
(void)argv;
notePreview();
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[])
{
int count;
(void)obj;
(void)argc;
(void)argv;
count = beginSave();
if (count == -5)
return "5";
if (count == -4)
return "4";
if (count == -3)
return "2";
if (count == -2)
return "1";
if (count < 0)
return "0";
std::snprintf(gReturnText, sizeof(gReturnText), "%d", count);
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[])
{
char folder[MAX_PATH];
char bestName[MAX_PATH];
WIN32_FIND_DATAA found;
HANDLE handle;
FILETIME bestTime;
bool any;
(void)obj;
(void)argc;
(void)argv;
gNewestPath[0] = '\0';
bestName[0] = '\0';
any = false;
if (!gameFile(folder, MAX_PATH, "autosave\\*"))
return "";
handle = FindFirstFileA(folder, &found);
if (handle == INVALID_HANDLE_VALUE)
return "";
do {
size_t len;
if (found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
continue;
len = std::strlen(found.cFileName);
if (len < 5 || _stricmp(found.cFileName + len - 4, ".bls") != 0)
continue;
if (!any || CompareFileTime(&found.ftLastWriteTime, &bestTime) > 0 || (CompareFileTime(&found.ftLastWriteTime, &bestTime) == 0 && std::strcmp(found.cFileName, bestName) > 0)) {
any = true;
bestTime = found.ftLastWriteTime;
std::snprintf(bestName, sizeof(bestName), "%s", found.cFileName);
}
} while (FindNextFileA(handle, &found));
FindClose(handle);
if (!any)
return "";
std::snprintf(gNewestPath, sizeof(gNewestPath), "saves/autosave/%s", bestName);
return gNewestPath;
}
static int gDeferred = 0;
static UINT_PTR gInstallTimer = 0;
static void deferredInstall()
{
if (gDeferred)
return;
gDeferred = 1;
if (!gArmed) {
gArmed = true;
reschedule();
scheduleHostCheck();
}
if (!gChat)
installChat();
if (!gWatching)
installWatch();
gDeferred = 0;
}
/* A thread timer runs from the main loop's message pump, outside script
execution. Patching Con::evaluate instead would hide it from every later
module that finds it by its first bytes. */
static VOID CALLBACK installTimerProc(HWND hwnd, UINT message, UINT_PTR id, DWORD time)
{
(void)hwnd;
(void)message;
(void)id;
(void)time;
deferredInstall();
if (gArmed && gChat && gWatching && gInstallTimer != 0) {
KillTimer(0, gInstallTimer);
gInstallTimer = 0;
}
}
/* 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())
return true;
if (!tsf_InitInternal()) {
BlPrintf("%s: Torque console was not found", PROJECT_NAME);
return true;
}
tsf_AddVar("AutoSave::Enabled", &gEnabled);
tsf_AddVar("AutoSave::Interval", &gInterval);
tsf_AddVar("AutoSave::Chunk", &gChunk);
tsf_AddVar("AutoSave::Tick", &gTickMs);
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);
gReady = true;
BlPrintf("%s (v%s): every %d seconds, %d bricks per tick, to saves/autosave/", PROJECT_NAME, PROJECT_VERSION, gInterval, gChunk);
return true;
}
bool deinit()
{
if (gInstallTimer != 0) {
KillTimer(0, gInstallTimer);
gInstallTimer = 0;
}
if (gReady) {
tsf_Eval("if (isEventPending($AutoSave::Schedule)) cancel($AutoSave::Schedule);");
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();
}
return true;
}
bool __stdcall DllMain(HINSTANCE hinstance, unsigned int reason, void *reserved)
{
(void)hinstance;
switch (reason) {
case DLL_PROCESS_ATTACH:
return init();
case DLL_PROCESS_DETACH:
if (reserved != 0)
return true;
return deinit();
default:
break;
}
return true;
}