diff --git a/src/main.cpp b/src/main.cpp index f11d67e..58c2a4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,12 +5,14 @@ #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 + 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 @@ -32,12 +34,8 @@ 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; +static bool gPreview = true; struct Row { ADDR obj; @@ -106,6 +104,89 @@ static std::string dataField(ADDR obj, const char *slot, const char *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); @@ -232,16 +313,12 @@ static void remember(ADDR obj, std::vector &rows, bool &anyValid) rows.push_back(row); } -static std::string printToken(ADDR brick, ADDR datablock) +static std::string printTokenFor(const std::string &filename) { - std::string path; + std::string path = callFn1("filePath", filename.c_str()); 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) @@ -254,20 +331,30 @@ static std::string printToken(ADDR brick, ADDR datablock) return dir.substr(underA + 1, underB - underA - 1) + "/" + callFn1("fileBase", filename.c_str()); } -static void writeEvents(FILE *file, ADDR brick, const std::string &className) +static const std::string &printToken(const std::string &printId) { - int count = std::atoi(dataField(brick, "numEvents", 0).c_str()); - if (count < 0 || count > 256) + 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 = dataField(brick, "eventInputIdx", index); - std::string targetIdx = dataField(brick, "eventTargetIdx", index); - std::string outputIdx = dataField(brick, "eventOutputIdx", index); + 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 = dataField(brick, "eventNT", index); + 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())); @@ -277,9 +364,11 @@ static void writeEvents(FILE *file, ADDR brick, const std::string &className) if (targetName.empty()) targetName = "-1"; } - std::string line = std::string("+-EVENT\t") + index + "\t" + dataField(brick, "eventEnabled", index) + 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" + dataField(brick, "eventDelay", index) + + "\t" + delay + "\t" + targetName + "\t" + nt + "\t" + table("OutputEvent_Name", targetClass, outputIdx); std::string params = table("OutputEvent_parameterList", targetClass, outputIdx); @@ -287,7 +376,7 @@ static void writeEvents(FILE *file, ADDR brick, const std::string &className) 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); + std::string value = fieldOf(brick, kEventParameter, slot); if (kind == "dataBlock") { ADDR named = 0; if (!value.empty() && value != "0" && value != "-1") @@ -310,27 +399,93 @@ static bool writeBrick(FILE *file, ADDR brick, bool ownership, bool extended, bo 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")); + const DatablockInfo &info = datablockInfo(datablock); + bool verify = gVerifyLeft > 0; + if (verify) + gVerifyLeft--; - if (ownership && local && truthy(dataField(brick, "isBasePlate", 0))) { + 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 (!groupId.empty() && groupId != "0" && groupId != "-1") + if (!emptyId(groupId.c_str())) group = tsf_FindObject((unsigned int)std::atoi(groupId.c_str())); if (group != 0) - writeLine(file, "+-OWNER " + dataField(group, "bl_id", 0)); + writeLine(file, std::string("+-OWNER ") + fieldOf(group, kBlid)); } if (!extended) return true; @@ -338,44 +493,32 @@ static bool writeBrick(FILE *file, ADDR brick, bool ownership, bool extended, bo std::string name = callObj(brick, "getName"); if (!name.empty()) writeLine(file, "+-NTOBJECTNAME " + name); - writeEvents(file, brick, callObj(brick, "getClassName")); + writeEvents(file, brick); - 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())); + 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 emitterDb = callObj(emitter, "getEmitterDataBlock"); - ADDR db = tsf_FindObject((unsigned int)std::atoi(emitterDb.c_str())); - std::string ui = db != 0 ? dataField(db, "uiName", 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); } - 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())); + const char *lightId = fieldOf(brick, kLight); + ADDR light = emptyId(lightId) ? 0 : tsf_FindObject((unsigned int)std::atoi(lightId)); 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 ui = uiNameOf(callObj(light, "getDataBlock").c_str()); + writeLine(file, "+-LIGHT " + ui + "\" " + fieldOf(light, kEnable)); } - 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); + 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 itemDb = callObj(item, "getDataBlock"); - ADDR db = tsf_FindObject((unsigned int)std::atoi(itemDb.c_str())); - std::string ui = db != 0 ? dataField(db, "uiName", 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 @@ -383,29 +526,22 @@ static bool writeBrick(FILE *file, ADDR brick, bool ownership, bool extended, bo 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())); + const char *audioId = fieldOf(brick, kAudioEmitter); + ADDR audio = emptyId(audioId) ? 0 : tsf_FindObject((unsigned int)std::atoi(audioId)); 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) : ""; + std::string ui = uiNameOf(callObj(audio, "getProfileId").c_str()); 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())); + 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 gameFile(char *out, int outLen, const char *leaf) +static bool gameDir(char *path) { - char path[MAX_PATH]; int n = (int)GetModuleFileNameA(0, path, MAX_PATH); if (n <= 0 || n >= MAX_PATH) return false; @@ -415,11 +551,85 @@ static bool gameFile(char *out, int outLen, const char *leaf) 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;", + gEnabled ? 1 : 0, gInterval, gChunk, gTickMs, gPreview ? 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); + 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"); @@ -715,6 +925,383 @@ static void serverChat(const char *text) 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; @@ -739,6 +1326,7 @@ static void finishSave() 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(); @@ -934,6 +1522,9 @@ static int beginSave() gDirect = false; gStarted = GetTickCount(); gReported = gStarted; + gDatablocks.clear(); + gPrintTokens.clear(); + gVerifyLeft = 64; gNextAllowed = 0; gLocal = false; connection = tsf_FindObject("ServerConnection"); @@ -1001,10 +1592,13 @@ static int clampInterval() return gInterval; } +static bool gHostSeen = false; + static void reschedule() { char code[128]; - std::snprintf(code, sizeof(code), "$AutoSave::Schedule = schedule(%d, 0, autoSaveTick);", clampInterval() * 1000); + int delay = gHostSeen ? clampInterval() * 1000 : 5000; + std::snprintf(code, sizeof(code), "$AutoSave::Schedule = schedule(%d, 0, autoSaveTick);", delay); tsf_Eval(code); } @@ -1021,18 +1615,12 @@ 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; + clampPrefs(); + if (!gPreview) + cancelPreview(); + prefsToScript(); + if (hosting()) + exportPrefs(); tsf_Eval("if (isEventPending($AutoSave::Schedule)) cancel($AutoSave::Schedule);"); if (gArmed) reschedule(); @@ -1061,6 +1649,7 @@ function AutoSave_Help(%client) 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"); } @@ -1145,7 +1734,7 @@ function AutoSave_Command(%client, %cmd, %arg) } if (%cmd $= "status") { - AutoSave_Tell(%client, "Enabled \c3" @ ($AutoSave::Enabled ? "on" : "off") @ "\c6, every \c3" @ $AutoSave::Interval @ "\c6 seconds, \c3" @ $AutoSave::Chunk @ "\c6 bricks per slice, \c3" @ $AutoSave::Tick @ "\c6 ms between slices."); + AutoSave_Tell(%client, "Enabled \c3" @ ($AutoSave::Enabled ? "on" : "off") @ "\c6, every \c3" @ $AutoSave::Interval @ "\c6 seconds, \c3" @ $AutoSave::Chunk @ "\c6 bricks per slice, \c3" @ $AutoSave::Tick @ "\c6 ms between slices, screenshots \c3" @ ($AutoSave::Preview ? "on" : "off") @ "\c6."); return; } if (!AutoSave_Admin(%client)) @@ -1186,6 +1775,23 @@ function AutoSave_Command(%client, %cmd, %arg) AutoSave_Reload(%client); 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") !$= "") @@ -1264,9 +1870,9 @@ static void installChat() } static const char kWatchScript[] = R"TS( -function AutoSave_Touch(%obj) +function AutoSave_TouchAny(%obj) { - if (!isObject(%obj)) + if ($AutoSave::Loading || !isObject(%obj)) return; if (%obj.getClassName() $= "fxDTSBrick" && !%obj.isPlanted()) return; @@ -1311,12 +1917,14 @@ package AutoSaveWatch { Parent::ServerLoadSaveFile_End(); $AutoSave::Loading = 0; + $AutoSave::Change++; } function serverCmdCancelSaveFileUpload(%client) { Parent::serverCmdCancelSaveFileUpload(%client); if (!isObject($Server_LoadFileObj)) $AutoSave::Loading = 0; + $AutoSave::Change++; } function destroyServer() { @@ -1331,22 +1939,22 @@ package AutoSaveWatch } function SimObject::clearEvents(%obj) { - AutoSave_Touch(%obj); + AutoSave_TouchAny(%obj); Parent::clearEvents(%obj); } function SimObject::SetEventEnabled(%obj, %idxList, %val) { - AutoSave_Touch(%obj); + AutoSave_TouchAny(%obj); Parent::SetEventEnabled(%obj, %idxList, %val); } function SimObject::ToggleEventEnabled(%obj, %idxList) { - AutoSave_Touch(%obj); + AutoSave_TouchAny(%obj); Parent::ToggleEventEnabled(%obj, %idxList); } function SimObject::setNTObjectName(%obj, %name) { - AutoSave_Touch(%obj); + AutoSave_TouchAny(%obj); Parent::setNTObjectName(%obj, %name); } function fxDTSBrick::onPlant(%obj) @@ -1371,97 +1979,116 @@ package AutoSaveWatch } function fxDTSBrick::setTransform(%obj, %transform) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setTransform(%obj, %transform); } function fxDTSBrick::setColor(%obj, %id) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setColor(%obj, %id); } function fxDTSBrick::setColorFX(%obj, %id) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setColorFX(%obj, %id); } function fxDTSBrick::setShapeFX(%obj, %id) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setShapeFX(%obj, %id); } function fxDTSBrick::setPrint(%obj, %id) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setPrint(%obj, %id); } function fxDTSBrick::setRayCasting(%obj, %val) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setRayCasting(%obj, %val); } function fxDTSBrick::setColliding(%obj, %val) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setColliding(%obj, %val); } function fxDTSBrick::setRendering(%obj, %val) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setRendering(%obj, %val); } function fxDTSBrick::setLight(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setLight(%obj, %data, %client); } function fxDTSBrick::setEmitter(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setEmitter(%obj, %data, %client); } function fxDTSBrick::setEmitterDirection(%obj, %dir) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setEmitterDirection(%obj, %dir); } function fxDTSBrick::setItem(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setItem(%obj, %data, %client); } function fxDTSBrick::setItemDirection(%obj, %dir) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setItemDirection(%obj, %dir); } function fxDTSBrick::setItemPosition(%obj, %pos) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setItemPosition(%obj, %pos); } function fxDTSBrick::setItemRespawntime(%obj, %time) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setItemRespawntime(%obj, %time); } function fxDTSBrick::setMusic(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setMusic(%obj, %data, %client); } function fxDTSBrick::setSound(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setSound(%obj, %data, %client); } function fxDTSBrick::setVehicle(%obj, %data, %client) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setVehicle(%obj, %data, %client); } function fxDTSBrick::setReColorVehicle(%obj, %val) { - AutoSave_Touch(%obj); + if (!$AutoSave::Loading && %obj.isPlanted()) + $AutoSave::Change++; Parent::setReColorVehicle(%obj, %val); } }; @@ -1492,6 +2119,20 @@ static void autoSaveTick(ADDR obj, int argc, const char *argv[]) (void)obj; (void)argc; (void)argv; + notePreview(); + if (!hosting()) { + gHostSeen = false; + reschedule(); + return; + } + if (!gHostSeen) { + gHostSeen = true; + prefsFromScript(); + BlPrintf("%s: $Pref::Server::AutoSave: %s, every %d seconds, %d bricks per slice, %d ms, preview %s", PROJECT_NAME, + gEnabled ? "on" : "off", gInterval, gChunk, gTickMs, gPreview ? "on" : "off"); + reschedule(); + return; + } if (gEnabled && gPhase == 0) beginSave(); reschedule(); @@ -1561,8 +2202,8 @@ static const char *autoSaveNewest(ADDR obj, int argc, const char *argv[]) return gNewestPath; } -static int gInEval = 0; static int gDeferred = 0; +static UINT_PTR gInstallTimer = 0; static void deferredInstall() { @@ -1580,15 +2221,20 @@ static void deferredInstall() gDeferred = 0; } -static const char *consoleEvaluateHook(ADDR obj, int argc, const char **argv) +/* 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) { - const char *result; - gInEval++; - result = consoleEvaluateOriginal(obj, argc, argv); - gInEval--; - if (gInEval == 0 && (!gArmed || !gChat || !gWatching)) - deferredInstall(); - return result; + (void)hwnd; + (void)message; + (void)id; + (void)time; + deferredInstall(); + if (gArmed && gChat && gWatching && gInstallTimer != 0) { + KillTimer(0, gInstallTimer); + gInstallTimer = 0; + } } bool init() @@ -1605,16 +2251,18 @@ bool init() tsf_AddVar("AutoSave::Tick", &gTickMs); tsf_AddVar("AutoSave::Change", &gChange); tsf_AddVar("AutoSave::Loading", &gLoading); + tsf_AddVar("AutoSave::Preview", &gPreview); + 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 and reschedule", 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); - 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; - } + 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); + 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; @@ -1622,11 +2270,15 @@ bool init() bool deinit() { - if (gHooked && consoleEvaluate != 0) - MH_DisableHook((void *)consoleEvaluate); + 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);"); if (gWatching) tsf_Eval("deactivatePackage(AutoSaveWatch);"); clearSave(); @@ -1648,4 +2300,4 @@ bool __stdcall DllMain(HINSTANCE hinstance, unsigned int reason, void *reserved) break; } return true; -} \ No newline at end of file +}