#include #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. 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 gDatablocks; static std::unordered_map 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::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 &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::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 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); } /* 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, ¶ms) == 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 "); AutoSave_Tell(%client, "/autosaver chunk "); AutoSave_Tell(%client, "/autosaver tick "); AutoSave_Tell(%client, "/autosaver screenshot "); AutoSave_Tell(%client, "/autosaver save"); AutoSave_Tell(%client, "/autosaver load "); AutoSave_Tell(%client, "/autosaver autoload "); } 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/.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; }