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