diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 508fc72..89c2b2a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -2,42 +2,28 @@ name: Build Check on: pull_request: permissions: - contents: write + contents: read jobs: build: strategy: matrix: platform: [ - {name: "Linux Debug", os: "ubuntu-22.04", ext: "tar.gz", config: "Debug"}, - {name: "Linux Release", os: "ubuntu-24.04", ext: "tar.gz", config: "Release"}, - {name: "Windows Debug", os: "windows-2022", ext: "zip", config: "Debug"}, - {name: "Windows Release", os: "windows-2022", ext: "zip", config: "Release"}, - {name: "macOS Debug", os: "macos-15", ext: "tar.gz", config: "Debug"}, - {name: "macOS Release", os: "macos-15", ext: "tar.gz", config: "Release"} + {name: "Linux Debug", os: "ubuntu-22.04", config: "Debug"}, + {name: "Linux Release", os: "ubuntu-24.04", config: "Release"}, + {name: "Windows Debug", os: "windows-2022", config: "Debug"}, + {name: "Windows Release", os: "windows-2022", config: "Release"}, + {name: "macOS Debug", os: "macos-15", config: "Debug"}, + {name: "macOS Release", os: "macos-15", config: "Release"} ] runs-on: ${{ matrix.platform.os }} name: ${{ matrix.platform.name }} steps: - - uses: actions/checkout@v4 - - name: Install macOS deps - if: runner.os == 'macOS' - run: brew install llvm - - name: Configure - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.platform.config }} - - name: Build - working-directory: ./build - run: cmake --build . --config ${{ matrix.platform.config }} - - name: Package - shell: bash - run: | - mkdir -p build/install - cmake --install build --config ${{ matrix.platform.config }} --prefix build/install - cd build/install - if [ "${{ matrix.platform.ext }}" = "zip" ]; then - 7z a ../../fsminit-${{ matrix.platform.config }}-${{ matrix.platform.os }}.${{ matrix.platform.ext }} * - else - tar -czf ../../fsminit-${{ matrix.platform.config }}-${{ matrix.platform.os }}.${{ matrix.platform.ext }} . - fi - - uses: softprops/action-gh-release@v2 - with: - files: fsminit-${{ matrix.platform.config }}-${{ matrix.platform.os }}.* + - uses: actions/checkout@v4 + - name: Install macOS deps + if: runner.os == 'macOS' + run: brew install llvm + - name: Configure + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.platform.config }} + - name: Build + working-directory: ./build + run: cmake --build . --config ${{ matrix.platform.config }} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2614e42..9413218 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # More reliable source listing (avoids globbing issues) -set(SRCS src/main.c src/file_subsystem.c) +set(SRCS src/main.c src/file_subsystem.c src/boilerplate_subsystem.c ) add_executable(FSMINIT ${SRCS}) target_include_directories(FSMINIT PRIVATE include) diff --git a/include/boilerplate_subsystem.h b/include/boilerplate_subsystem.h new file mode 100644 index 0000000..359da32 --- /dev/null +++ b/include/boilerplate_subsystem.h @@ -0,0 +1,26 @@ +#pragma once + +#include "def_type.h" + +/* Boilerplate string literals. Use macros so they remain constant expressions. */ +#define BPL_WEP_PRIMARY \ + "#Primary Weapons\n" \ + "$Name: placeholder\n" \ + "#End\n" + +/* Helper array */ +static const char *bpl_wep_primary_variants[] = { + BPL_WEP_PRIMARY}; + +/* Boilerplate table — keys must match base_name for .tbl and modular_suffix for .tbm */ +static const BPL_ENTRY bpl_table[] = { + {"weapons", bpl_wep_primary_variants, 1}, // for weapons.tbl + {"-wep", bpl_wep_primary_variants, 1} // for XXX‑wep.tbm +}; + +static const size_t bpl_table_count = sizeof(bpl_table) / sizeof(bpl_table[0]); +/* Public functions */ +bool verify_mod_structure(const char *base_path); +TABLE_FILE_LIST *verify_tables_directory(const char *base_path_tables); +const BPL_ENTRY *find_boilerplate(const char *filename); +void write_to_tables(const BPL_ENTRY *entry, const char *filepath, OP *operation); \ No newline at end of file diff --git a/include/def_type.h b/include/def_type.h index 700dab6..2987c2e 100644 --- a/include/def_type.h +++ b/include/def_type.h @@ -3,6 +3,10 @@ #include #include +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + /* ======================= CLI Option Structure ======================= */ @@ -19,6 +23,9 @@ typedef struct OP int dirs_created; int tables_created; int errors; + + /* For Boilerplates */ + int gen_boilerplate; } OP; /* ======================= @@ -107,9 +114,22 @@ typedef struct FS_TABLES const char *name; } FS_TABLES; -/* ======================= - External Declarations - ======================= */ +typedef struct BPL_ENTRY +{ + const char *key; /* "-wep" or "weapons", etc... */ + const char **variants; /* array of boilerplate strings */ + size_t variant_count; +} BPL_ENTRY; + +typedef struct TABLE_FILE_LIST +{ + char **paths; + size_t count; +} TABLE_FILE_LIST; + + /* ======================= + External Declarations + ======================= */ extern const char *voice_subdirs[]; extern const size_t voice_subdirs_count; diff --git a/include/file_subsystem.h b/include/file_subsystem.h index 5032ad0..0bed0f1 100644 --- a/include/file_subsystem.h +++ b/include/file_subsystem.h @@ -6,4 +6,5 @@ void create_directories(OP *operation); void create_tbl_tables(OP *operation); void create_tbm_tables(OP *operation); void create_static_tables(OP *operation); -bool check_if_mod_structure_exists(const char *base_path); \ No newline at end of file +bool check_if_mod_structure_exists(const char *base_path); +bool path_is_dir(const char *path); \ No newline at end of file diff --git a/src/boilerplate_subsystem.c b/src/boilerplate_subsystem.c new file mode 100644 index 0000000..9474e4a --- /dev/null +++ b/src/boilerplate_subsystem.c @@ -0,0 +1,318 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +#ifndef S_ISREG +#define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) +#endif + +#ifdef _WIN32 +#include +#include +#include +#include + +#define check_writable(p) (_access((p), 2) == 0) + +// Windows asprintf wrapper +#define asprintf(buf, fmt, ...) _asprintf(buf, fmt, __VA_ARGS__) +static int _asprintf(char **ret, const char *format, ...) +{ + va_list ap; + int size; + va_start(ap, format); + size = vsnprintf(NULL, 0, format, ap); + va_end(ap); + if (size < 0) + return -1; + *ret = malloc(size + 1); + if (!*ret) + return -1; + va_start(ap, format); + size = vsnprintf(*ret, size + 1, format, ap); + va_end(ap); + return size; +} + +struct dirent +{ + char d_name[260]; +}; + +typedef long long DIR; + +DIR *opendir(const char *dirname) +{ + char pattern[PATH_MAX + 4]; + snprintf(pattern, sizeof(pattern), "%s/*", dirname); + long handle = _findfirst(pattern, &((struct _finddata_t){0})); + return handle != -1L ? (DIR *)handle : NULL; +} + +struct dirent *readdir(DIR *dir) +{ + static struct _finddata_t entry; + static struct dirent de; + static long handle; + + if (dir == NULL) + return NULL; + + handle = (long)dir; + if (handle == 0 || handle == -1L) + return NULL; + + if (handle == (long)dir && _findfirst != 0) + { + handle = _findfirst((const char *)_findfirst, &entry); + if (handle == -1) + return NULL; + } + else + { + if (_findnext(handle, &entry) != 0) + return NULL; + } + + strcpy(de.d_name, entry.name); + return &de; +} + +int closedir(DIR *dir) +{ + if (dir == NULL) + return -1; + long handle = (long)dir; + if (handle == 0 || handle == -1L) + return -1; + _findclose(handle); + return 0; +} + +#else + +#include +#include +#define check_writable(p) (access((p), W_OK) == 0) + +#endif + +#include "def_type.h" +#include "color.h" +#include "text_type.h" +#include "file_subsystem.h" +#include "boilerplate_subsystem.h" + + +/* +Check to see if the mod structure has been generated successfully, if so, +focus only on the tables/ directory being present. +*/ +bool verify_mod_structure(const char *base_path) +{ + if (path_is_dir(base_path)) + { + for (size_t i = 0; i < fs_dirs_count; i++) + { + char *dir_path = NULL; + if (asprintf(&dir_path, "%s/%s", base_path, fs_dirs[i].name) == -1) + return false; + + bool exists = path_is_dir(dir_path); + free(dir_path); + if (!exists) + { + return false; + } + } + return true; + } + else + { + return false; + } +} + +/* Verify and collect the tables from the "tables/" directory and return them */ +TABLE_FILE_LIST *verify_tables_directory(const char *base_path_tables) +{ + TABLE_FILE_LIST *tfl = malloc(sizeof(TABLE_FILE_LIST)); + if (!tfl) + return NULL; + + tfl->paths = NULL; + tfl->count = 0; + + if (!path_is_dir(base_path_tables)) + { + free(tfl); + return NULL; + } + + DIR *dir = opendir(base_path_tables); + if (!dir) + { + free(tfl); + return NULL; + } + + struct dirent *entry; + char full_path[PATH_MAX]; + size_t capacity = 16; + + tfl->paths = malloc(capacity * sizeof(char *)); + if (!tfl->paths) + { + closedir(dir); + free(tfl); + return NULL; + } + + while ((entry = readdir(dir)) != NULL) + { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + struct stat st; + snprintf(full_path, sizeof(full_path), "%s/%s", base_path_tables, entry->d_name); + if (stat(full_path, &st) == 0 && S_ISREG(st.st_mode)) + { + if (tfl->count >= capacity) + { + capacity *= 2; + char **new_paths = realloc(tfl->paths, capacity * sizeof(char *)); + if (!new_paths) + { + for (size_t j = 0; j < tfl->count; j++) + free(tfl->paths[j]); + free(tfl->paths); + closedir(dir); + free(tfl); + return NULL; + } + tfl->paths = new_paths; + } + + tfl->paths[tfl->count] = strdup(full_path); + if (!tfl->paths[tfl->count]) + { + for (size_t j = 0; j < tfl->count; j++) + free(tfl->paths[j]); + free(tfl->paths); + closedir(dir); + free(tfl); + return NULL; + } + tfl->count++; + } + } + + closedir(dir); + return tfl; +} + +static char *strip_path_ext(const char *filename) +{ + const char *base = strrchr(filename, '/'); + if (!base) + base = filename; + else + base++; + + const char *dot = strrchr(base, '.'); + if (!dot) + dot = base + strlen(base); + + size_t len = dot - base; + char *full = malloc(len + 1); + if (full) + { + strncpy(full, base, len); + full[len] = '\0'; + } + return full; +} + +// Given "XXX‑wep", returns "wep" +static char *strip_prefix(const char *key) +{ + const char *dash = strchr(key, '-'); + if (!dash) + return strdup(key); + return strdup(dash); +} + +const BPL_ENTRY *find_boilerplate(const char *filename) +{ + char *full = strip_path_ext(filename); + if (!full) + return NULL; + + char *key = strip_prefix(full); + free(full); + if (!key) + return NULL; + + for (size_t i = 0; i < bpl_table_count; i++) + { + if (strcmp(key, bpl_table[i].key) == 0) + { + free(key); + return &bpl_table[i]; + } + + /* Optional: allow suffix matching if you want .tbm entries to be themselves */ + size_t suffix_len = strlen(bpl_table[i].key); + size_t key_len = strlen(key); + + if (key_len >= suffix_len && + strcmp(key + key_len - suffix_len, bpl_table[i].key) == 0) + { + free(key); + return &bpl_table[i]; + } + } + + free(key); + return NULL; +} + +void write_to_tables(const BPL_ENTRY *entry, const char *filepath, OP *operation) +{ + FILE *fp; + + if (!check_writable(filepath)) + { + fprintf(stderr, "%s%sError:%s Target path is not writable: %s\n", + TEX_BOLD, COL_RED, COL_RESET, operation->path); + return; + } + + fp = fopen(filepath, "w"); + if (!fp) + { + perror("fopen"); + return; + } + + if (entry && entry->variants) + { + for (size_t i = 0; i < entry->variant_count; i++) + { + if (entry->variants[i]) + { + fprintf(fp, "%s\n", entry->variants[i]); + } + } + } + + fclose(fp); +} \ No newline at end of file diff --git a/src/file_subsystem.c b/src/file_subsystem.c index 89fde7c..9fe23a4 100644 --- a/src/file_subsystem.c +++ b/src/file_subsystem.c @@ -1,4 +1,3 @@ -/* file_subsystem.c */ #include #include #include @@ -147,7 +146,7 @@ const size_t static_tables_count = 1; File & Directory Creation ===================================== */ -static bool path_is_dir(const char *path) +bool path_is_dir(const char *path) { struct stat st; if (stat(path, &st) != 0) diff --git a/src/main.c b/src/main.c index 2341152..4285dd9 100644 --- a/src/main.c +++ b/src/main.c @@ -1,27 +1,46 @@ -/* main.c */ #include #include #include +#include // for va_list / va_start / va_end #include #include #ifdef _WIN32 #include #include + #define isatty _isatty + +// Windows asprintf wrapper +#define asprintf(buf, fmt, ...) _asprintf(buf, fmt, __VA_ARGS__) +static int _asprintf(char **ret, const char *format, ...) +{ + va_list ap; + int size; + va_start(ap, format); + size = vsnprintf(NULL, 0, format, ap); + va_end(ap); + if (size < 0) + return -1; + *ret = malloc(size + 1); + if (!*ret) + return -1; + va_start(ap, format); + size = vsnprintf(*ret, size + 1, format, ap); + va_end(ap); + return size; +} + #else #include #endif -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif - #include "version.h" #include "def_type.h" #include "file_subsystem.h" #include "text_type.h" #include "color.h" +#include "boilerplate_subsystem.h" bool g_color_enabled = false; @@ -50,6 +69,7 @@ int main(int argc, char *argv[]) operation.prefix = NULL; operation.debug = 0; operation.dry_run = 0; + operation.gen_boilerplate = 0; if (strcmp(argv[1], "-help") == 0) { @@ -66,8 +86,9 @@ int main(int argc, char *argv[]) "2. : Target directory\n" "3. <-tbl/-tbm> : Table type (stdmc only)\n" "4. : Required for -tbm\n" - "5. [-debug] : Log to log.txt\n" - "6. [-dry-run] : Simulate only\n"); + "5. [-bpl] : Write boilerplate to tables\n" + "6. [-debug] : Log to log.txt\n" + "7. [-dry-run] : Simulate only\n"); return 0; } @@ -91,7 +112,7 @@ int main(int argc, char *argv[]) fprintf(stderr, "%s%sUsage:%s -stdm [-debug] [-dry-run]\n", TEX_BOLD, COL_YELLOW, COL_RESET); else - fprintf(stderr, "%s%sUsage:%s -stdmc <-tbl/-tbm> [prefix] [-debug] [-dry-run]\n", + fprintf(stderr, "%s%sUsage:%s -stdmc <-tbl/-tbm> [prefix] [-bpl] [-debug] [-dry-run]\n", TEX_BOLD, COL_YELLOW, COL_RESET); return 1; } @@ -122,10 +143,23 @@ int main(int argc, char *argv[]) for (int i = start_index; i < argc; ++i) { - if (strcmp(argv[i], "-debug") == 0) + if (strcmp(argv[i], "-bpl") == 0) + { + operation.gen_boilerplate = 1; + } + else if (strcmp(argv[i], "-debug") == 0) + { operation.debug = 1; + } else if (strcmp(argv[i], "-dry-run") == 0) + { operation.dry_run = 1; + } + else + { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + return 1; + } } if (operation.debug) @@ -170,6 +204,38 @@ int main(int argc, char *argv[]) } } + if (operation.table_type && operation.gen_boilerplate && operation.dry_run == 0) + { + char *tables_path = NULL; + if (asprintf(&tables_path, "%s/tables", operation.path) == -1) + { + operation.errors++; + } + else + { + TABLE_FILE_LIST *tables = verify_tables_directory(tables_path); + if (tables) + { + printf("%sWriting boilerplate to tables...%s%s\n", TEX_BOLD, COL_MAGENTA, COL_RESET); + + for (size_t i = 0; i < tables->count; i++) + { + const BPL_ENTRY *entry = find_boilerplate(tables->paths[i]); + if (entry) + { + write_to_tables(entry, tables->paths[i], &operation); + } + } + // Cleanup + for (size_t i = 0; i < tables->count; i++) + free(tables->paths[i]); + free(tables->paths); + free(tables); + } + free(tables_path); + } + } + printf("\n%s%s--- Summary ---%s\n", TEX_BOLD, COL_CYAN, COL_RESET); printf(" %sDirectories:%s %s%d%s\n", TEX_BOLD, COL_RESET, COL_GREEN, operation.dirs_created, COL_RESET); printf(" %sTables:%s %s%d%s\n", TEX_BOLD, COL_RESET, COL_GREEN, operation.tables_created, COL_RESET);