Compare commits

...
Author SHA1 Message Date
github-actions[bot] 139fc3c440 chore: auto-format with clang-format 2026-04-20 20:41:03 +00:00
phobos2077 90ca28a13c Mod config patching system 2026-04-20 22:30:31 +02:00
4 changed files with 102 additions and 38 deletions
+20
View File
@@ -331,6 +331,26 @@ bool configRead(Config* config, const char* filePath, bool isDb)
configParseLine(config, string);
}
fileClose(stream);
// Build patch file path by inserting "#patch" before the extension.
char patchPath[COMPAT_MAX_PATH];
const char* dot = strrchr(filePath, '.');
if (dot != nullptr) {
snprintf(patchPath, sizeof(patchPath), "%.*s#patch%s", (int)(dot - filePath), filePath, dot);
} else {
snprintf(patchPath, sizeof(patchPath), "%s#patch", filePath);
}
struct PatchContext {
Config* config;
char string[CONFIG_FILE_MAX_LINE_LENGTH];
} patchCtx = { config };
xfileOpenEachReverse(patchPath, "rb", [](XFile* file, void* ctx) {
auto* pc = static_cast<PatchContext*>(ctx);
while (fileReadString(pc->string, sizeof(pc->string), file) != nullptr) {
configParseLine(pc->config, pc->string);
} }, &patchCtx);
} else {
FILE* stream = compat_fopen(filePath, "rt");
-3
View File
@@ -23,9 +23,6 @@ void contentConfigInit()
}
configRead(&gContentConfig, kConfigPath, true);
// Patch config allows to override only certain fields, without replacing the whole file.
// TODO: remove this after config patching by mods is implemented inside configRead
configRead(&gContentConfig, kConfigPatchPath, true);
}
void contentConfigExit()
+80 -35
View File
@@ -67,6 +67,46 @@ int xfileClose(XFile* stream)
return rc;
}
// Attempts to open [filePath] from a single [xbase]. On success, fills [stream]
// and writes the resolved path into [path]/[pathSize]. Returns true on success.
static bool xfileOpenFromXBaseInto(XBase* xbase, const char* filePath, const char* mode, XFile* stream, char* path, size_t pathSize)
{
if (xbase->isDbase) {
stream->dfile = dfileOpen(xbase->dbase, filePath, mode);
if (stream->dfile == nullptr) {
return false;
}
stream->type = XFILE_TYPE_DFILE;
snprintf(path, pathSize, "%s", filePath);
} else {
snprintf(path, pathSize, "%s\\%s", xbase->path, filePath);
stream->file = compat_fopen(path, mode);
if (stream->file == nullptr) {
return false;
}
stream->type = XFILE_TYPE_FILE;
}
return true;
}
// Checks if a plain file stream is gzipped and reopens it accordingly.
// [path] must be the resolved filesystem path used to open [stream->file].
static void xfileDetectGzip(XFile* stream, const char* path, const char* mode)
{
if (stream->type != XFILE_TYPE_FILE) {
return;
}
int ch1 = fgetc(stream->file);
int ch2 = fgetc(stream->file);
if (ch1 == 0x1F && ch2 == 0x8B) {
fclose(stream->file);
stream->type = XFILE_TYPE_GZFILE;
stream->gzfile = compat_gzopen(path, mode);
} else {
rewind(stream->file);
}
}
// 0x4DEE2C
XFile* xfileOpen(const char* filePath, const char* mode)
{
@@ -101,24 +141,8 @@ XFile* xfileOpen(const char* filePath, const char* mode)
// open [filePath] from appropriate xbase.
XBase* curr = gXbaseHead;
while (curr != nullptr) {
if (curr->isDbase) {
// Attempt to open dfile stream from dbase.
stream->dfile = dfileOpen(curr->dbase, filePath, mode);
if (stream->dfile != nullptr) {
stream->type = XFILE_TYPE_DFILE;
snprintf(path, sizeof(path), "%s", filePath);
break;
}
} else {
// Build path relative to directory-based xbase.
snprintf(path, sizeof(path), "%s\\%s", curr->path, filePath);
// Attempt to open plain stream.
stream->file = compat_fopen(path, mode);
if (stream->file != nullptr) {
stream->type = XFILE_TYPE_FILE;
break;
}
if (xfileOpenFromXBaseInto(curr, filePath, mode, stream, path, sizeof(path))) {
break;
}
curr = curr->next;
}
@@ -137,23 +161,8 @@ XFile* xfileOpen(const char* filePath, const char* mode)
}
}
if (stream->type == XFILE_TYPE_FILE) {
// Opened file is a plain stream, which might be gzipped. In this case
// first two bytes will contain magic numbers.
int ch1 = fgetc(stream->file);
int ch2 = fgetc(stream->file);
if (ch1 == 0x1F && ch2 == 0x8B) {
// File is gzipped. Close plain stream and reopen this file as
// gzipped stream.
fclose(stream->file);
stream->type = XFILE_TYPE_GZFILE;
stream->gzfile = compat_gzopen(path, mode);
} else {
// File is not gzipped.
rewind(stream->file);
}
}
// Opened file is a plain stream, which might be gzipped.
xfileDetectGzip(stream, path, mode);
return stream;
}
@@ -550,6 +559,42 @@ bool xbaseOpen(const char* path)
return false; // return false to trigger messages on game load
}
static XFile* xfileOpenFromXBase(XBase* xbase, const char* filePath, const char* mode)
{
XFile* stream = (XFile*)malloc(sizeof(*stream));
if (stream == nullptr) {
return nullptr;
}
memset(stream, 0, sizeof(*stream));
char path[COMPAT_MAX_PATH];
if (!xfileOpenFromXBaseInto(xbase, filePath, mode, stream, path, sizeof(path))) {
free(stream);
return nullptr;
}
xfileDetectGzip(stream, path, mode);
return stream;
}
static void xfileOpenEachReverseHelper(XBase* xbase, const char* filePath, const char* mode, XFileEachHandler* handler, void* context)
{
if (xbase == nullptr) {
return;
}
xfileOpenEachReverseHelper(xbase->next, filePath, mode, handler, context);
XFile* file = xfileOpenFromXBase(xbase, filePath, mode);
if (file != nullptr) {
handler(file, context);
xfileClose(file);
}
}
void xfileOpenEachReverse(const char* filePath, const char* mode, XFileEachHandler* handler, void* context)
{
xfileOpenEachReverseHelper(gXbaseHead, filePath, mode, handler, context);
}
// 0x4DFB3C
static bool xlistEnumerate(const char* pattern, XListEnumerationHandler* handler, XList* xlist)
{
+2
View File
@@ -65,6 +65,8 @@ int xfileEof(XFile* stream);
long xfileGetSize(XFile* stream);
bool xbaseReopenAll(char* paths);
bool xbaseOpen(const char* path);
typedef void XFileEachHandler(XFile* file, void* context);
void xfileOpenEachReverse(const char* filePath, const char* mode, XFileEachHandler* handler, void* context);
bool xlistInit(const char* pattern, XList* xlist);
void xlistFree(XList* xlist);