diff --git a/Makefile b/Makefile index cd687a9e..c3f9fd25 100644 --- a/Makefile +++ b/Makefile @@ -455,7 +455,7 @@ SRC_DIRS := src src/engine src/game src/audio src/menu src/buffers actors levels BIN_DIRS := bin bin/$(VERSION) # PC files -SRC_DIRS += src/pc src/pc/gfx src/pc/audio src/pc/controller src/pc/fs src/pc/fs/packtypes src/pc/network src/pc/network/packets src/pc/network/socket src/pc/utils src/pc/djui src/pc/lua src/pc/lua/utils +SRC_DIRS += src/pc src/pc/gfx src/pc/audio src/pc/controller src/pc/fs src/pc/fs/packtypes src/pc/mods src/pc/network src/pc/network/packets src/pc/network/socket src/pc/utils src/pc/djui src/pc/lua src/pc/lua/utils #ifeq ($(DISCORDRPC),1) # SRC_DIRS += src/pc/discord diff --git a/src/pc/mods/mod.c b/src/pc/mods/mod.c new file mode 100644 index 00000000..680c7c62 --- /dev/null +++ b/src/pc/mods/mod.c @@ -0,0 +1,290 @@ +#include "mod.h" +#include "mods.h" +#include "mods_utils.h" +#include "pc/utils/misc.h" +#include "pc/debuglog.h" + +void mod_clear(struct Mod* mod) { + for (int j = 0; j < mod->fileCount; j++) { + struct ModFile* file = &mod->files[j]; + file = file; + } + + if (mod->name != NULL) { + free(mod->name); + mod->name = NULL; + } + + if (mod->incompatible != NULL) { + free(mod->incompatible); + mod->incompatible = NULL; + } + + if (mod->description != NULL) { + free(mod->description); + mod->description = NULL; + } + + if (mod->files != NULL) { + free(mod->files); + } + + mod->fileCount = 0; +} + +static struct ModFile* mod_allocate_file(struct Mod* mod, char* relativePath) { + // actual allocation + u16 fileIndex = mod->fileCount++; + mod->files = realloc(mod->files, sizeof(struct ModFile) * mod->fileCount); + if (mod->files == NULL) { + LOG_ERROR("Failed to allocate file: '%s'", relativePath); + return NULL; + } + + // clear memory + struct ModFile* file = &mod->files[fileIndex]; + memset(file, 0, sizeof(struct ModFile)); + + // set relative path + if (snprintf(file->relativePath, SYS_MAX_PATH - 1, "%s", relativePath) < 0) { + LOG_ERROR("Failed to remember relative path '%s'", relativePath); + return NULL; + } + + return file; +} + +static bool mod_load_files(struct Mod* mod, char* modName, char* fullPath) { + // read single lua file + if (!mod->isDirectory) { + return (mod_allocate_file(mod, modName) != NULL); + } + + // deal with mod directory + { + // open mod directory + struct dirent* dir = NULL; + DIR* d = opendir(fullPath); + if (!d) { + LOG_ERROR("Could not open directory '%s'", fullPath); + return false; + } + + // iterate mod directory + char path[SYS_MAX_PATH] = { 0 }; + while ((dir = readdir(d)) != NULL) { + // sanity check / fill path[] + if (!directory_sanity_check(dir, fullPath, path)) { continue; } + + // only consider lua files + if (!str_ends_with(path, ".lua")) { + continue; + } + + // allocate file + struct ModFile* file = mod_allocate_file(mod, dir->d_name); + if (file == NULL) { return false; } + } + + closedir(d); + } + + // deal with actors directory + { + // concat actors directory + char actorsPath[SYS_MAX_PATH] = { 0 }; + if (!concat_path(actorsPath, fullPath, "actors")) { + LOG_ERROR("Could not concat directory '%s' + '%s'", fullPath, "actors"); + return false; + } + + // open actors directory + struct dirent* dir = NULL; + DIR* d = opendir(actorsPath); + if (!d) { + LOG_ERROR("Could not open directory '%s'", actorsPath); + return false; + } + + // iterate mod directory + char path[SYS_MAX_PATH] = { 0 }; + char relativePath[SYS_MAX_PATH] = { 0 }; + while ((dir = readdir(d)) != NULL) { + // sanity check / fill path[] + if (!directory_sanity_check(dir, actorsPath, path)) { continue; } + if (snprintf(relativePath, SYS_MAX_PATH - 1, "%s/actors/%s", modName, dir->d_name) < 0) { + LOG_ERROR("Could not concat actor path!"); + return false; + } + + // only consider bin files + if (!str_ends_with(path, ".bin")) { + continue; + } + + // allocate file + struct ModFile* file = mod_allocate_file(mod, relativePath); + if (file == NULL) { return false; } + } + + closedir(d); + } + + return true; +} + +static void mod_extract_fields(struct Mod* mod) { + // get full path + char path[SYS_MAX_PATH] = { 0 }; + char* relativePath = NULL; + if (mod->isDirectory) { + for (int i = 0; i < mod->fileCount; i++) { + struct ModFile* file = &mod->files[i]; + if (!strcmp(file->relativePath, "main.lua")) { + relativePath = file->relativePath; + } + } + } else { + relativePath = mod->files[0].relativePath; + } + + if (relativePath == NULL || !concat_path(path, mod->basePath, relativePath)) { + LOG_ERROR("Failed to find main lua file."); + return; + } + + // open file + FILE* f = fopen(path, "rb"); + if (f == NULL) { + LOG_ERROR("Failed to open '%s'", path); + return; + } + fseek(f, 0, SEEK_SET); + + // default to null + mod->name = NULL; + mod->incompatible = NULL; + mod->description = NULL; + + // read line-by-line + char buffer[512] = { 0 }; + while (!feof(f)) { + file_get_line(buffer, 512, f); + + // no longer in header + if (buffer[0] != '-' || buffer[1] != '-') { + return; + } + + // extract the field + char* extracted = NULL; + if (mod->name == NULL && (extracted = extract_lua_field("-- name:", buffer))) { + mod->name = calloc(33, sizeof(char)); + if (snprintf(mod->name, 32, "%s", extracted) < 0) { + LOG_INFO("Truncated mod name field '%s'", mod->name); + } + } else if (mod->incompatible == NULL && (extracted = extract_lua_field("-- incompatible:", buffer))) { + mod->incompatible = calloc(257, sizeof(char)); + if (snprintf(mod->incompatible, 256, "%s", extracted) < 0) { + LOG_INFO("Truncated mod incompatible field '%s'", mod->incompatible); + } + } else if (mod->description == NULL && (extracted = extract_lua_field("-- description:", buffer))) { + mod->description = calloc(513, sizeof(char)); + if (snprintf(mod->description, 512, "%s", extracted) < 0) { + LOG_INFO("Truncated mod description field '%s'", mod->description); + } + } + } + + // close file + fclose(f); +} + +bool mod_load(struct Mods* mods, char* basePath, char* modName) { + bool valid = false; + + char fullPath[SYS_MAX_PATH] = { 0 }; + if (!concat_path(fullPath, basePath, modName)) { + LOG_ERROR("Failed to concat path '%s' + '%s'", basePath, modName); + return true; + } + + bool isDirectory = is_directory(fullPath); + + // make sure mod is valid + if (str_ends_with(modName, ".lua")) { + valid = true; + } else if (is_directory(fullPath)) { + char tmpPath[SYS_MAX_PATH] = { 0 }; + if (!concat_path(tmpPath, fullPath, "main.lua")) { + LOG_ERROR("Failed to concat path '%s' + '%s'", fullPath, "main.lua"); + return true; + } + valid = path_exists(tmpPath); + } + + if (!valid) { + LOG_ERROR("Found invalid mod '%s'", fullPath); + return true; + } + + // make sure mod is unique + for (int i = 0; i < mods->modCount; i++) { + struct Mod* compareMod = &mods->entries[i]; + if (!strcmp(compareMod->relativePath, modName)) { + return true; + } + } + + // allocate mod + u16 modIndex = mods->modCount++; + mods->entries = realloc(mods->entries, sizeof(struct Mod) * mods->modCount); + if (mods->entries == NULL) { + LOG_ERROR("Failed to allocate entries!"); + mods_clear(mods); + return false; + } + struct Mod* mod = &mods->entries[modIndex]; + memset(mod, 0, sizeof(struct Mod)); + + // set paths + char* cpyPath = isDirectory ? fullPath : basePath; + if (snprintf(mod->basePath, SYS_MAX_PATH - 1, "%s", cpyPath) < 0) { + LOG_ERROR("Failed to remember mod path '%s'!", cpyPath); + mods_clear(mods); + return false; + } + if (snprintf(mod->relativePath, SYS_MAX_PATH - 1, "%s", modName) < 0) { + LOG_ERROR("Failed to remember mod path '%s'!", modName); + mods_clear(mods); + return false; + } + + // set directory + mod->isDirectory = isDirectory; + + // read files + if (!mod_load_files(mod, modName, fullPath)) { + LOG_ERROR("Failed to load mod files for '%s'", modName); + return false; + } + + // extract fields + mod_extract_fields(mod); + + // set name + if (mod->name == NULL) { + mod->name = strdup(modName); + } + + // print + LOG_INFO(" %s", mod->name); + if (isDirectory) { + for (int i = 0; i < mod->fileCount; i++) { + struct ModFile* file = &mod->files[i]; + LOG_INFO(" - %s", file->relativePath); + } + } + + return true; +} diff --git a/src/pc/mods/mod.h b/src/pc/mods/mod.h new file mode 100644 index 00000000..bc292e76 --- /dev/null +++ b/src/pc/mods/mod.h @@ -0,0 +1,29 @@ +#ifndef MOD_H +#define MOD_H + +#include "PR/ultratypes.h" +#include +#include "src/pc/platform.h" + +struct Mods; + +struct ModFile { + char relativePath[SYS_MAX_PATH]; +}; + +struct Mod { + char* name; + char* incompatible; + char* description; + char relativePath[SYS_MAX_PATH]; + char basePath[SYS_MAX_PATH]; + struct ModFile* files; + u16 fileCount; + bool isDirectory; + bool enabled; +}; + +void mod_clear(struct Mod* mod); +bool mod_load(struct Mods* mods, char* basePath, char* modName); + +#endif \ No newline at end of file diff --git a/src/pc/mods/mods.c b/src/pc/mods/mods.c new file mode 100644 index 00000000..9ce4bec4 --- /dev/null +++ b/src/pc/mods/mods.c @@ -0,0 +1,85 @@ +#include +#include "mods.h" +#include "mods_utils.h" +#include "pc/debuglog.h" + +#define MOD_DIRECTORY "mods" + +static struct Mods gLocalMods = { 0 }; + +void mods_clear(struct Mods* mods) { + for (int i = 0; i < mods->modCount; i ++) { + struct Mod* mod = &mods->entries[i]; + mod_clear(mod); + } + + if (mods->entries != NULL) { + free(mods->entries); + mods->entries = NULL; + } + mods->modCount = 0; +} + +static void mods_load(struct Mods* mods, char* modsBasePath) { + // sanity check + if (modsBasePath == NULL) { + LOG_ERROR("Trying to load from NULL path!"); + return; + } + + // make the path normal + normalize_path(modsBasePath); + + // check for existence + if (!is_directory(modsBasePath)) { + LOG_ERROR("Could not find directory '%s'", modsBasePath); + } + + LOG_INFO("Loading mods in '%s':", modsBasePath); + + // open directory + struct dirent* dir = NULL; + DIR* d = opendir(modsBasePath); + if (!d) { + LOG_ERROR("Could not open directory '%s'", modsBasePath); + return; + } + + // iterate + char path[SYS_MAX_PATH] = { 0 }; + while ((dir = readdir(d)) != NULL) { + // sanity check / fill path[] + if (!directory_sanity_check(dir, modsBasePath, path)) { continue; } + + // load the mod + if (!mod_load(mods, modsBasePath, dir->d_name)) { + break; + } + } + + closedir(d); + +} + +void mods_init(void) { + // figure out user path + bool hasUserPath = true; + char userModPath[SYS_MAX_PATH] = { 0 }; + if (snprintf(userModPath, SYS_MAX_PATH - 1, "%s", fs_get_write_path(MOD_DIRECTORY)) < 0) { + hasUserPath = false; + } + if (!fs_sys_dir_exists(userModPath)) { + hasUserPath = fs_sys_mkdir(userModPath); + } + + // clear mods + mods_clear(&gLocalMods); + + // load mods + if (hasUserPath) { mods_load(&gLocalMods, userModPath); } + mods_load(&gLocalMods, "./" MOD_DIRECTORY); +} + +void mods_shutdown(void) { + mods_clear(&gLocalMods); +} diff --git a/src/pc/mods/mods.h b/src/pc/mods/mods.h new file mode 100644 index 00000000..36f4d9f4 --- /dev/null +++ b/src/pc/mods/mods.h @@ -0,0 +1,18 @@ +#ifndef MODS_H +#define MODS_H + +#include "PR/ultratypes.h" +#include +#include "src/pc/platform.h" +#include "mod.h" + +struct Mods { + struct Mod* entries; + u16 modCount; +}; + +void mods_clear(struct Mods* mods); +void mods_init(void); +void mods_shutdown(void); + +#endif \ No newline at end of file diff --git a/src/pc/mods/mods_utils.c b/src/pc/mods/mods_utils.c new file mode 100644 index 00000000..d35c7aa6 --- /dev/null +++ b/src/pc/mods/mods_utils.c @@ -0,0 +1,90 @@ +#include +#include +#include "mods_utils.h" +#include "pc/debuglog.h" + +bool str_ends_with(char* string, char* suffix) { + if (string == NULL || suffix == NULL) { return false; } + + size_t stringLength = strlen(string); + size_t suffixLength = strlen(suffix); + + if (suffixLength > stringLength) { return false; } + + return !strcmp(&string[stringLength - suffixLength], suffix); +} + + +char* extract_lua_field(char* fieldName, char* buffer) { + size_t length = strlen(fieldName); + if (strncmp(fieldName, buffer, length) == 0) { + char* s = &buffer[length]; + while (*s == ' ' || *s == '\t') { s++; } + return s; + } + return NULL; +} + +bool path_exists(char* path) { + struct stat sb = { 0 }; + return (stat(path, &sb) == 0); +} + +bool is_directory(char* path) { + struct stat sb = { 0 }; + return (stat(path, &sb) == 0 && S_ISDIR(sb.st_mode)); +} + +void normalize_path(char* path) { + // replace slashes + char* p = path; + while (*p) { +#if defined(_WIN32) + if (*p == '/') { *p = '\\'; } +#else + if (*p == '\\') { *p = '/'; } +#endif + p++; + } +} + +bool concat_path(char* destination, char* path, char* fname) { + return (snprintf(destination, SYS_MAX_PATH - 1, "%s/%s", path, fname) >= 0); +} + +char* path_basename(char* path) { + char* base = path; + while (*path != '\0') { + if (*(path + 1) != '\0') { + if (*path == '\\' || *path == '/') { + base = path + 1; + } + } + path++; + } + return base; +} + +bool directory_sanity_check(struct dirent* dir, char* dirPath, char* outPath) { + // skip anything that contains \ or / + if (strchr(dir->d_name, '/') != NULL) { return false; } + if (strchr(dir->d_name, '\\') != NULL) { return false; } + + // skip anything that starts with . + if (dir->d_name == NULL || dir->d_name[0] == '.') { return false; } + + // build path + if (!concat_path(outPath, dirPath, dir->d_name)) { + LOG_ERROR("Failed to concat path '%s' + '%s'", dirPath, dir->d_name); + return false; + } + normalize_path(outPath); + + // sanity check + if (!path_exists(outPath)) { + LOG_ERROR("Path doesn't exist: '%s'", outPath); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/pc/mods/mods_utils.h b/src/pc/mods/mods_utils.h new file mode 100644 index 00000000..33449b50 --- /dev/null +++ b/src/pc/mods/mods_utils.h @@ -0,0 +1,19 @@ +#ifndef MODS_UTILS_H +#define MODS_UTILS_H + +#include "PR/ultratypes.h" +#include +#include "src/pc/platform.h" + +bool str_ends_with(char* string, char* suffix); + +char* extract_lua_field(char* fieldName, char* buffer); + +bool path_exists(char* path); +bool is_directory(char* path); +void normalize_path(char* path); +bool concat_path(char* destination, char* path, char* fname); +char* path_basename(char* path); +bool directory_sanity_check(struct dirent* dir, char* dirPath, char* outPath); + +#endif \ No newline at end of file diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c index c181d5ab..8faeb2e4 100644 --- a/src/pc/pc_main.c +++ b/src/pc/pc_main.c @@ -50,6 +50,7 @@ #include "pc/djui/djui.h" #include "pc/mod_list.h" +#include "pc/mods/mods.h" OSMesg D_80339BEC; OSMesgQueue gSIEventMesgQueue; @@ -171,6 +172,7 @@ void game_deinit(void) { network_shutdown(true); smlua_shutdown(); mod_list_shutdown(); + mods_shutdown(); inited = false; } @@ -221,6 +223,7 @@ void main_func(void) { fs_init(sys_ropaths, gamedir, userpath); mod_list_init(); + mods_init(); configfile_load(configfile_name()); if (configPlayerModel >= CT_MAX) { configPlayerModel = 0; } if (configPlayerPalette >= PALETTE_MAX) { configPlayerPalette = 0; }