From eaf3166bad4e84fc78164f6857a9551705fbdc04 Mon Sep 17 00:00:00 2001 From: RallyBlock Date: Mon, 26 Oct 2026 11:08:06 -0400 Subject: [PATCH] Add AutoSave source --- src/main.cpp | 1651 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1651 insertions(+) create mode 100644 src/main.cpp diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f11d67e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,1651 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "TSFuncs.hpp" + +/* Writes saves/autosave/YYYY-MM-DD HH-MM-SS.bls in the same layout as + saveBricks(), without the GUI list or the 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 gHooked = false; +static bool gReady = false; + +typedef const char *(*EvaluateFn)(ADDR, int, const char **); +static EvaluateFn consoleEvaluate = 0; +static EvaluateFn consoleEvaluateOriginal = 0; + +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 : ""; +} + +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 &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 printToken(ADDR brick, ADDR datablock) +{ + std::string path; + std::string dir; + size_t underA; + size_t underB; + if (!truthy(dataField(datablock, "hasPrint", 0))) + return ""; + std::string filename = callFn1("getPrintTexture", callObj(brick, "getPrintID").c_str()); + path = callFn1("filePath", filename.c_str()); + 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 void writeEvents(FILE *file, ADDR brick, const std::string &className) +{ + int count = std::atoi(dataField(brick, "numEvents", 0).c_str()); + if (count < 0 || count > 256) + return; + for (int i = 0; i < count; ++i) { + char index[16]; + std::snprintf(index, sizeof(index), "%d", i); + std::string inputIdx = dataField(brick, "eventInputIdx", index); + std::string targetIdx = dataField(brick, "eventTargetIdx", index); + std::string outputIdx = dataField(brick, "eventOutputIdx", index); + std::string targetName = "-1"; + std::string targetClass = "FxDTSBrick"; + std::string nt = dataField(brick, "eventNT", 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 line = std::string("+-EVENT\t") + index + "\t" + dataField(brick, "eventEnabled", index) + + "\t" + table("InputEvent_Name", className, inputIdx) + + "\t" + dataField(brick, "eventDelay", index) + + "\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 = dataField(brick, "eventOutputParameter", 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; + std::string uiName = dataField(datablock, "uiName", 0); + std::string transform = callObj(brick, "getTransform"); + std::string pos = word(transform, 0) + " " + word(transform, 1) + " " + word(transform, 2); + std::string print = printToken(brick, datablock); + writeLine(file, uiName + "\"" + " " + pos + " " + callObj(brick, "getAngleID") + + " " + callObj(brick, "isBasePlate") + + " " + callObj(brick, "getColorID") + + " " + print + + " " + callObj(brick, "getColorFxID") + + " " + callObj(brick, "getShapeFxID") + + " " + callObj(brick, "isRayCasting") + + " " + callObj(brick, "isColliding") + + " " + callObj(brick, "isRendering")); + + if (ownership && local && truthy(dataField(brick, "isBasePlate", 0))) { + std::string groupId = callObj(brick, "getGroup"); + ADDR group = 0; + if (!groupId.empty() && groupId != "0" && groupId != "-1") + group = tsf_FindObject((unsigned int)std::atoi(groupId.c_str())); + if (group != 0) + writeLine(file, "+-OWNER " + dataField(group, "bl_id", 0)); + } + if (!extended) + return true; + + std::string name = callObj(brick, "getName"); + if (!name.empty()) + writeLine(file, "+-NTOBJECTNAME " + name); + writeEvents(file, brick, callObj(brick, "getClassName")); + + std::string emitterDir = dataField(brick, "emitterDirection", 0); + std::string emitterId = dataField(brick, "emitter", 0); + ADDR emitter = 0; + if (!emitterId.empty() && emitterId != "0" && emitterId != "-1") + emitter = tsf_FindObject((unsigned int)std::atoi(emitterId.c_str())); + if (emitter != 0) { + std::string emitterDb = callObj(emitter, "getEmitterDataBlock"); + ADDR db = tsf_FindObject((unsigned int)std::atoi(emitterDb.c_str())); + std::string ui = db != 0 ? dataField(db, "uiName", 0) : ""; + writeLine(file, "+-EMITTER " + ui + "\" " + emitterDir); + } else if (!emitterDir.empty() && std::atof(emitterDir.c_str()) != 0.0) { + writeLine(file, "+-EMITTER NONE\" " + emitterDir); + } + + std::string lightId = dataField(brick, "light", 0); + ADDR light = 0; + if (!lightId.empty() && lightId != "0" && lightId != "-1") + light = tsf_FindObject((unsigned int)std::atoi(lightId.c_str())); + if (light != 0) { + std::string lightDb = callObj(light, "getDataBlock"); + ADDR db = tsf_FindObject((unsigned int)std::atoi(lightDb.c_str())); + std::string ui = db != 0 ? dataField(db, "uiName", 0) : ""; + writeLine(file, "+-LIGHT " + ui + "\"" + " " + dataField(light, "Enable", 0)); + } + + std::string itemId = dataField(brick, "Item", 0); + ADDR item = 0; + if (!itemId.empty() && itemId != "0" && itemId != "-1") + item = tsf_FindObject((unsigned int)std::atoi(itemId.c_str())); + std::string itemPos = dataField(brick, "itemPosition", 0); + std::string itemDir = dataField(brick, "itemDirection", 0); + std::string itemTime = dataField(brick, "itemRespawnTime", 0); + if (item != 0) { + std::string itemDb = callObj(item, "getDataBlock"); + ADDR db = tsf_FindObject((unsigned int)std::atoi(itemDb.c_str())); + std::string ui = db != 0 ? dataField(db, "uiName", 0) : ""; + 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); + } + + std::string audioId = dataField(brick, "AudioEmitter", 0); + ADDR audio = 0; + if (!audioId.empty() && audioId != "0" && audioId != "-1") + audio = tsf_FindObject((unsigned int)std::atoi(audioId.c_str())); + if (audio != 0) { + std::string profile = callObj(audio, "getProfileId"); + ADDR db = tsf_FindObject((unsigned int)std::atoi(profile.c_str())); + std::string ui = db != 0 ? dataField(db, "uiName", 0) : ""; + writeLine(file, "+-AUDIOEMITTER " + ui + "\" "); + } + + std::string vehicleId = dataField(brick, "VehicleSpawnMarker", 0); + ADDR vehicle = 0; + if (!vehicleId.empty() && vehicleId != "0" && vehicleId != "-1") + vehicle = tsf_FindObject((unsigned int)std::atoi(vehicleId.c_str())); + if (vehicle != 0) + writeLine(file, "+-VEHICLE " + callObj(vehicle, "getUiName") + "\" " + callObj(vehicle, "getReColorVehicle")); + return true; +} + +static bool gameFile(char *out, int outLen, const char *leaf) +{ + char path[MAX_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; + } + } + if (std::snprintf(out, outLen, "%s\\saves\\%s", path, leaf) >= outLen) + return false; + return true; +} + +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 gGroups; +static std::vector 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); +} + +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); + } + 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; + 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 void reschedule() +{ + char code[128]; + std::snprintf(code, sizeof(code), "$AutoSave::Schedule = schedule(%d, 0, autoSaveTick);", clampInterval() * 1000); + 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; + 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; + 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 "); + AutoSave_Tell(%client, "/autosaver chunk "); + AutoSave_Tell(%client, "/autosaver tick "); + AutoSave_Tell(%client, "/autosaver save"); + AutoSave_Tell(%client, "/autosaver load"); +} +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_Reload(%client) +{ + if ($Game::MissionCleaningUp || AutoSave_Busy() || $AutoSave::ReloadFile !$= "") + { + AutoSave_Tell(%client, "Bricks are still being cleared or loaded."); + return; + } + %file = AutoSave_NewestFile(); + if (%file $= "") + { + AutoSave_Tell(%client, "There is no autosave to load."); + 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."); + 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); + 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 serverCmdautosaver(%client, %cmd, %arg) +{ + AutoSave_Command(%client, %cmd, %arg); +} +function serverCmdAutosaver(%client, %cmd, %arg) +{ + AutoSave_Command(%client, %cmd, %arg); +} +function serverCmdAutoSaver(%client, %cmd, %arg) +{ + AutoSave_Command(%client, %cmd, %arg); +} +function serverCmdAUTOSAVER(%client, %cmd, %arg) +{ + AutoSave_Command(%client, %cmd, %arg); +} +$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_Touch(%obj) +{ + if (!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), getWord(%body, 2)); + else + commandToServer('autosaver', getWord(%body, 1), getWord(%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; + } + function serverCmdCancelSaveFileUpload(%client) + { + Parent::serverCmdCancelSaveFileUpload(%client); + if (!isObject($Server_LoadFileObj)) + $AutoSave::Loading = 0; + } + function destroyServer() + { + $AutoSave::Loading = 0; + Parent::destroyServer(); + } + 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_Touch(%obj); + Parent::clearEvents(%obj); + } + function SimObject::SetEventEnabled(%obj, %idxList, %val) + { + AutoSave_Touch(%obj); + Parent::SetEventEnabled(%obj, %idxList, %val); + } + function SimObject::ToggleEventEnabled(%obj, %idxList) + { + AutoSave_Touch(%obj); + Parent::ToggleEventEnabled(%obj, %idxList); + } + function SimObject::setNTObjectName(%obj, %name) + { + AutoSave_Touch(%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) + { + AutoSave_Touch(%obj); + Parent::setTransform(%obj, %transform); + } + function fxDTSBrick::setColor(%obj, %id) + { + AutoSave_Touch(%obj); + Parent::setColor(%obj, %id); + } + function fxDTSBrick::setColorFX(%obj, %id) + { + AutoSave_Touch(%obj); + Parent::setColorFX(%obj, %id); + } + function fxDTSBrick::setShapeFX(%obj, %id) + { + AutoSave_Touch(%obj); + Parent::setShapeFX(%obj, %id); + } + function fxDTSBrick::setPrint(%obj, %id) + { + AutoSave_Touch(%obj); + Parent::setPrint(%obj, %id); + } + function fxDTSBrick::setRayCasting(%obj, %val) + { + AutoSave_Touch(%obj); + Parent::setRayCasting(%obj, %val); + } + function fxDTSBrick::setColliding(%obj, %val) + { + AutoSave_Touch(%obj); + Parent::setColliding(%obj, %val); + } + function fxDTSBrick::setRendering(%obj, %val) + { + AutoSave_Touch(%obj); + Parent::setRendering(%obj, %val); + } + function fxDTSBrick::setLight(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setLight(%obj, %data, %client); + } + function fxDTSBrick::setEmitter(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setEmitter(%obj, %data, %client); + } + function fxDTSBrick::setEmitterDirection(%obj, %dir) + { + AutoSave_Touch(%obj); + Parent::setEmitterDirection(%obj, %dir); + } + function fxDTSBrick::setItem(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setItem(%obj, %data, %client); + } + function fxDTSBrick::setItemDirection(%obj, %dir) + { + AutoSave_Touch(%obj); + Parent::setItemDirection(%obj, %dir); + } + function fxDTSBrick::setItemPosition(%obj, %pos) + { + AutoSave_Touch(%obj); + Parent::setItemPosition(%obj, %pos); + } + function fxDTSBrick::setItemRespawntime(%obj, %time) + { + AutoSave_Touch(%obj); + Parent::setItemRespawntime(%obj, %time); + } + function fxDTSBrick::setMusic(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setMusic(%obj, %data, %client); + } + function fxDTSBrick::setSound(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setSound(%obj, %data, %client); + } + function fxDTSBrick::setVehicle(%obj, %data, %client) + { + AutoSave_Touch(%obj); + Parent::setVehicle(%obj, %data, %client); + } + function fxDTSBrick::setReColorVehicle(%obj, %val) + { + AutoSave_Touch(%obj); + 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; + if (gEnabled && gPhase == 0) + beginSave(); + reschedule(); +} + +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 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 gInEval = 0; +static int gDeferred = 0; + +static void deferredInstall() +{ + if (gDeferred) + return; + gDeferred = 1; + if (!gArmed) { + gArmed = true; + reschedule(); + } + if (!gChat) + installChat(); + if (!gWatching) + installWatch(); + gDeferred = 0; +} + +static const char *consoleEvaluateHook(ADDR obj, int argc, const char **argv) +{ + const char *result; + gInEval++; + result = consoleEvaluateOriginal(obj, argc, argv); + gInEval--; + if (gInEval == 0 && (!gArmed || !gChat || !gWatching)) + deferredInstall(); + return result; +} + +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_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 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); + consoleEvaluate = (EvaluateFn)tsf_BlCon__evaluate; + if (consoleEvaluate != 0 && MH_CreateHook((void *)consoleEvaluate, (void *)&consoleEvaluateHook, (void **)&consoleEvaluateOriginal) == MH_OK) { + if (MH_EnableHook((void *)consoleEvaluate) == MH_OK) + gHooked = true; + } + 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 (gHooked && consoleEvaluate != 0) + MH_DisableHook((void *)consoleEvaluate); + if (gReady) { + tsf_Eval("if (isEventPending($AutoSave::Schedule)) cancel($AutoSave::Schedule);"); + tsf_Eval("if (isEventPending($AutoSave::ChunkSchedule)) cancel($AutoSave::ChunkSchedule);"); + 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; +} \ No newline at end of file