From 9ee12a6c7ef0c83d2870f5d0828d51c0a0a2e446 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 8 Oct 2022 10:59:32 +0200 Subject: [PATCH] Added loadOrder support + fixed some bug add the getDataPath function / .h that will find the data folder of the game. this fixe some issue with older games like morrowind --- CMakeLists.txt | 2 ++ src/getDataPath.c | 40 +++++++++++++++++++++ src/getDataPath.h | 15 ++++++++ src/loadOrder.c | 88 ++++++++++++++++++++++++++++++++++++++--------- src/loadOrder.h | 25 ++++++++++++++ src/main.c | 54 +++++++++++------------------ src/steam.h | 6 ++-- 7 files changed, 179 insertions(+), 51 deletions(-) create mode 100644 src/getDataPath.c create mode 100644 src/getDataPath.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 777ab41..d9dc092 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,8 +21,10 @@ project(mod-manager) set(SOURCES src/steam.c + src/loadOrder.c src/install.c src/overlayfs.c + src/getDataPath.c src/getHome.c src/file.c src/order.c diff --git a/src/getDataPath.c b/src/getDataPath.c new file mode 100644 index 0000000..efcc343 --- /dev/null +++ b/src/getDataPath.c @@ -0,0 +1,40 @@ + +#include "getDataPath.h" +#include "main.h" +#include "steam.h" + +#include +#include + +error_t getDataPath(int appid, char ** destination) { + GHashTable * gamePaths; + error_t status = search_games(&gamePaths); + if(status == ERR_FAILURE) { + return ERR_FAILURE; + } + + int gameId = getGameIdFromAppId(appid); + if(gameId < 0 ) { + return ERR_FAILURE; + } + + const char * path = g_hash_table_lookup(gamePaths, &appid); + char * gameFolder = g_build_filename(path, "steamapps/common", GAMES_NAMES[gameId], NULL); + //the folder names for older an newer titles + char * dataFolderOld = g_build_filename(gameFolder, "Data Files", NULL); + char * dataFolderNew = g_build_filename(gameFolder, "Data", NULL); + g_free(gameFolder); + + *destination = NULL; + if(access(dataFolderOld, F_OK) == 0) { + *destination = strdup(dataFolderOld); + } else if(access(dataFolderNew, F_OK) == 0) { + *destination = strdup(dataFolderOld); + } + + g_free(dataFolderNew); + g_free(dataFolderOld); + + if(*destination == NULL) return ERR_FAILURE; + else return ERR_SUCCESS; +} diff --git a/src/getDataPath.h b/src/getDataPath.h new file mode 100644 index 0000000..b0dcca7 --- /dev/null +++ b/src/getDataPath.h @@ -0,0 +1,15 @@ +#ifndef __DATA_PATH_H__ +#define __DATA_PATH_H__ + +#include "main.h" + +/** + * @brief Get the path to the data folder + * @param appid appid of the game + * @param destination pointer to a null char * variable. it's value will be allocated with malloc + * @return error_t + */ +error_t getDataPath(int appid, char ** destination); + + +#endif diff --git a/src/loadOrder.c b/src/loadOrder.c index 2c812dc..d906083 100644 --- a/src/loadOrder.c +++ b/src/loadOrder.c @@ -4,19 +4,16 @@ #include "file.h" #include +#include #include -error_t listPlugins(int appid, GList ** plugins) { - GHashTable * gamePaths; - error_t status = search_games(&gamePaths); - if(status == ERR_FAILURE) { - return ERR_FAILURE; - } +#include "getDataPath.h" + +//TODO: detect if the game is running +//TODO: deploy the game + +error_t listPlugins(int appid, GList ** plugins) { - int gameId = getGameIdFromAppId(appid); - if(gameId < 0 ) { - return ERR_FAILURE; - } //save appid parsing //TODO: apply a similar mechanism everywhere @@ -24,13 +21,14 @@ error_t listPlugins(int appid, GList ** plugins) { char appidStr[appidStrLen]; sprintf(appidStr, "%d", appid); - - - const char * path = g_hash_table_lookup(gamePaths, &appid); - char * steamGameFolder = g_build_filename(path, "steamapps/common", GAMES_NAMES[gameId], "Data", NULL); + char * dataFolder = NULL; + error_t error = getDataPath(appid, &dataFolder); + if(error != ERR_SUCCESS) { + return ERR_FAILURE; + } //esp && esm files are loadable - DIR * d = opendir(steamGameFolder); + DIR * d = opendir(dataFolder); struct dirent *dir; if (d) { while ((dir = readdir(d)) != NULL) { @@ -41,7 +39,7 @@ error_t listPlugins(int appid, GList ** plugins) { } } - g_free(steamGameFolder); + free(dataFolder); return ERR_SUCCESS; } @@ -139,3 +137,61 @@ error_t setLoadOrder(int appid, GList * loadOrder) { return ERR_SUCCESS; } + +//TODO: support compression since it can change how we read the file +//https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format#Records +//https://www.mwmythicmods.com/argent/tech/es_format.html +error_t getModDependencies(const char * esmPath, GList ** dependencies) { + FILE * file = fopen(esmPath, "r"); + + char sectionName[5]; + sectionName[4] = '\0'; + fread(sectionName, sizeof(char), 4, file); + + size_t sizeFieldSize = 0; + u_int8_t recordHeaderToIgnore = 0; + + //the field "length" in the sub-record change between games. + //TES4 => Fallout4 Oblivion Skyrim(+SE) + //TES3 => Morrowind + if(strcmp(sectionName, "TES3") == 0) { + printf("Using tes3 file format\n"); + recordHeaderToIgnore = 8; + sizeFieldSize = 4; + } else if(strcmp(sectionName, "TES4") == 0) { + printf("Using tes4 file format\n"); + recordHeaderToIgnore = 16; + sizeFieldSize = 2; + } else { + fprintf(stderr, "Unrecognized file format %s\n", sectionName); + fclose(file); + return ERR_FAILURE; + } + + + u_int32_t lengthVal = 0; + fread(&lengthVal, 4, 1, file); + //ignore the rest of the data + fseek(file, recordHeaderToIgnore, SEEK_CUR); + + int64_t length = lengthVal; + while(length > 0) { + char sectionName[5]; + sectionName[4] = '\0'; + fread(sectionName, sizeof(char), 4, file); + unsigned long subsectionLength = 0; + fread(&subsectionLength, sizeFieldSize, 1, file); + + length -= 8 + subsectionLength; + if(strcmp(sectionName, "MAST") == 0) { + char * dependency = malloc(subsectionLength + 1); + dependency[subsectionLength] = '\0'; + fread(dependency, sizeof(char), subsectionLength, file); + *dependencies = g_list_append(*dependencies, dependency); + } else { + fseek(file, subsectionLength, SEEK_CUR); + } + } + fclose(file); + return ERR_SUCCESS; +} diff --git a/src/loadOrder.h b/src/loadOrder.h index 2b17a68..9b07adb 100644 --- a/src/loadOrder.h +++ b/src/loadOrder.h @@ -4,8 +4,33 @@ #include #include "main.h" +/** + * @brief find all plugins in the data folder of the game + * @param appid the appid of the game + * @param order a pointer to a null Glist *. in which there will be the list of esm files. + */ error_t listPlugins(int appid, GList ** list) __attribute__((warn_unused_result)); + +/** + * @brief fetch the load order of the game it might be null if setLoadOrder was never called. + * @param appid the appid of the game + * @param order a pointer to a null Glist *. in which there will be the list of esm files. + */ error_t getLoadOrder(int appid, GList ** order) __attribute__((warn_unused_result)); + +/** + * @brief change the plugin load order of the game + * @param appid the appid of the game + * @param loadOrder the load order + */ error_t setLoadOrder(int appid, GList * loadOrder) __attribute__((warn_unused_result)); +/** + * @brief List all dependencies for a esm mod. + * @param esmPath path to the esm file + * @param dependencies a pointer to a null Glist *. in which there will be the list of esm files. + * free it using g_list_free_full(dependencies, free); + */ +error_t getModDependencies(const char * esmPath, GList ** dependencies) __attribute__((warn_unused_result)); + #endif diff --git a/src/main.c b/src/main.c index 46591f0..b163327 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include #include +#include "getDataPath.h" #include "overlayfs.h" #include "install.h" #include "getHome.h" @@ -312,19 +313,14 @@ static int deploy(int argc, char ** argv) { modsToInstall[modCount] = gameFolder; modsToInstall[modCount + 1] = NULL; printf("Mounting the overlay\n"); - int gameId = getGameIdFromAppId(appid); - GHashTable * gamePaths; - error_t lookup_status = search_games(&gamePaths); - if(lookup_status == ERR_FAILURE) { + char * dataFolder = NULL; + error_t error = getDataPath(appid, &dataFolder); + if(error != ERR_SUCCESS) { free(home); return ERR_FAILURE; } - - const char * path = g_hash_table_lookup(gamePaths, &gameId); - char * steamGameFolder = g_build_path("/", path, "steamapps/common", GAMES_NAMES[gameId], "Data", NULL); - char * gameUpperDir = g_build_filename(home, MANAGER_FILES, GAME_UPPER_DIR_NAME, appIdStr, NULL); char * gameWorkDir = g_build_filename(home, MANAGER_FILES, GAME_WORK_DIR_NAME, appIdStr, NULL); free(home); @@ -333,8 +329,8 @@ static int deploy(int argc, char ** argv) { //DETACH + FORCE allow us to be sure it will be unload. //it might crash / corrupt game file if the user do it while the game is running //but it's still very unlikely - while(umount2(steamGameFolder, MNT_FORCE | MNT_DETACH) == 0); - enum overlayErrors status = overlayMount(modsToInstall, steamGameFolder, gameUpperDir, gameWorkDir); + while(umount2(dataFolder, MNT_FORCE | MNT_DETACH) == 0); + enum overlayErrors status = overlayMount(modsToInstall, dataFolder, gameUpperDir, gameWorkDir); if(status == SUCESS) { printf("Everything is ready, just launch the game\n"); } else if(status == FAILURE) { @@ -348,7 +344,7 @@ static int deploy(int argc, char ** argv) { return EXIT_FAILURE; } - g_free(steamGameFolder); + g_free(dataFolder); g_free(gameUpperDir); g_free(gameWorkDir); @@ -362,7 +358,6 @@ static int setup(int argc, char ** argv) { if(appid < 0) { return EXIT_FAILURE; } - int gameId = getGameIdFromAppId(appid); char * home = getHome(); char * gameUpperDir = g_build_filename(home, MANAGER_FILES, GAME_UPPER_DIR_NAME, appIdStr, NULL); @@ -372,16 +367,12 @@ static int setup(int argc, char ** argv) { free(gameUpperDir); free(gameWorkDir); - GHashTable * gamePaths; - error_t status = search_games(&gamePaths); - if(status == ERR_FAILURE) { - free(home); + char * dataFolder = NULL; + error_t error = getDataPath(appid, &dataFolder); + if(error != ERR_SUCCESS) { return ERR_FAILURE; } - const char * path = g_hash_table_lookup(gamePaths, &gameId); - char * steamGameFolder = g_build_path("/", path, "steamapps/common", GAMES_NAMES[gameId], "Data", NULL); - char * gameFolder = g_build_filename(home, MANAGER_FILES, GAME_FOLDER_NAME, appIdStr, NULL); if(access(gameFolder, F_OK) == 0) { //if the game folder alredy exists just delete it @@ -392,20 +383,20 @@ static int setup(int argc, char ** argv) { //links don't conflict with overlayfs and avoid coping 17Gb of files. //but links require the files to be on the same filesystem - int returnValue = copy(steamGameFolder, gameFolder, CP_RECURSIVE | CP_NO_TARGET_DIR | CP_LINK); + int returnValue = copy(dataFolder, gameFolder, CP_RECURSIVE | CP_NO_TARGET_DIR | CP_LINK); if(returnValue < 0) { printf("Coping game files. HINT: having the game on the same partition as you home director will make this operation use zero extra space"); - returnValue = copy(steamGameFolder, gameFolder, CP_RECURSIVE | CP_NO_TARGET_DIR); + returnValue = copy(dataFolder, gameFolder, CP_RECURSIVE | CP_NO_TARGET_DIR); if(returnValue < 0) { fprintf(stderr, "Copy failed make sure you have enough space on your device."); - free(steamGameFolder); + free(dataFolder); free(gameFolder); return EXIT_FAILURE; } } casefold(gameFolder); - free(steamGameFolder); + free(dataFolder); free(gameFolder); printf("Done\n"); return EXIT_SUCCESS; @@ -420,18 +411,15 @@ static int unbind(int argc, char ** argv) { return EXIT_FAILURE; } - GHashTable * gamePaths; - error_t status = search_games(&gamePaths); - if(status == ERR_FAILURE) - return EXIT_FAILURE; + char * dataFolder = NULL; + error_t error = getDataPath(appid, &dataFolder); + if(error != ERR_SUCCESS) { + return ERR_FAILURE; + } - int gameId = getGameIdFromAppId(appid); - const char * path = g_hash_table_lookup(gamePaths, &gameId); - char * steamGameFolder = g_build_path("/", path, "steamapps/common", GAMES_NAMES[gameId], "Data", NULL); + while(umount2(dataFolder, MNT_FORCE | MNT_DETACH) == 0); - while(umount2(steamGameFolder, MNT_FORCE | MNT_DETACH) == 0); - - free(steamGameFolder); + free(dataFolder); return EXIT_SUCCESS; } diff --git a/src/steam.h b/src/steam.h index 92f3e13..7866ec1 100644 --- a/src/steam.h +++ b/src/steam.h @@ -31,14 +31,16 @@ typedef struct ValveLibraries { static const u_int32_t GAMES_APPIDS[] = { 489830, 22330, - 377160 + 377160, + 22320 }; //the name of the game in the steamapps/common folder static const char * GAMES_NAMES[] = { "Skyrim Special Edition", "Oblivion", - "Fallout 4" + "Fallout 4", + "Morrowind" }; _Static_assert(LEN(GAMES_APPIDS) == LEN(GAMES_NAMES), "Game APPIDS and Game Names doesn't match");