From d98575d77b31b961d9e847c4a4bf436f8b7bcd78 Mon Sep 17 00:00:00 2001 From: fgsfds Date: Tue, 26 Sep 2023 23:05:07 +0200 Subject: [PATCH] port: add basic mod directory support with config files for custom stages select mod directory with --moddir whatever these should contain a modconfig.txt that can specify replacements for some stage table fields, etc --- port/include/fs.h | 2 + port/include/mod.h | 10 ++ port/include/romdata.h | 3 + port/include/utils.h | 15 +++ port/src/config.c | 89 ++++---------- port/src/fs.c | 78 ++++++++++--- port/src/main.c | 5 + port/src/mod.c | 256 +++++++++++++++++++++++++++++++++++++++++ port/src/romdata.c | 25 +++- port/src/utils.c | 198 +++++++++++++++++++++++++++++++ 10 files changed, 598 insertions(+), 83 deletions(-) create mode 100644 port/include/mod.h create mode 100644 port/include/utils.h create mode 100644 port/src/mod.c create mode 100644 port/src/utils.c diff --git a/port/include/fs.h b/port/include/fs.h index 561c41456..364ece5a2 100644 --- a/port/include/fs.h +++ b/port/include/fs.h @@ -20,4 +20,6 @@ FILE *fsFileOpenWrite(const char *name); FILE *fsFileOpenRead(const char *name); void fsFileClose(FILE *f); +const char *fsGetModDir(void); + #endif diff --git a/port/include/mod.h b/port/include/mod.h new file mode 100644 index 000000000..6f8b6a279 --- /dev/null +++ b/port/include/mod.h @@ -0,0 +1,10 @@ +#ifndef _IN_MOD_H +#define _IN_MOD_H + +#include + +#define MOD_CONFIG_FNAME "modconfig.txt" + +s32 modConfigLoad(const char *path); + +#endif diff --git a/port/include/romdata.h b/port/include/romdata.h index 70da7e824..d1d411f1e 100644 --- a/port/include/romdata.h +++ b/port/include/romdata.h @@ -11,10 +11,13 @@ s32 romdataInit(void); u8 *romdataFileLoad(s32 fileNum, u32 *outSize); void romdataFilePreprocess(s32 fileNum, s32 loadType, u8 *data, u32 size); void romdataFileFree(s32 fileNum); +const char *romdataFileGetName(s32 fileNum); u8 *romdataFileGetData(s32 fileNum); s32 romdataFileGetSize(s32 fileNum); +s32 romdataFileGetNumForName(const char *name); + u8 *romdataSegGetData(const char *segName); u8 *romdataSegGetDataEnd(const char *segName); u32 romdataSegGetSize(const char *segName); diff --git a/port/include/utils.h b/port/include/utils.h new file mode 100644 index 000000000..b47bd0941 --- /dev/null +++ b/port/include/utils.h @@ -0,0 +1,15 @@ +#ifndef _IN_UTILS_H +#define _IN_UTILS_H + +#define UTIL_MAX_TOKEN 1024 + +#include + +char *strRightTrim(char *str); +char *strTrim(char *str); +char *strUnquote(char *str); +char *strParseToken(char *str, char *out, s32 *outCount); +char *strFmt(const char *fmt, ...); +char *strDuplicate(const char *str); + +#endif diff --git a/port/src/config.c b/port/src/config.c index 4e6f52161..d3ba28251 100644 --- a/port/src/config.c +++ b/port/src/config.c @@ -6,6 +6,7 @@ #include "fs.h" #include "config.h" #include "system.h" +#include "utils.h" #define CONFIG_MAX_STR 512 #define CONFIG_MAX_SECNAME 128 @@ -216,6 +217,7 @@ s32 configLoad(const char *fname) char curSec[CONFIG_MAX_SECNAME + 1] = { 0 }; char keyBuf[CONFIG_MAX_SECNAME * 2 + 2] = { 0 }; // SECTION + . + KEY + \0 + char token[UTIL_MAX_TOKEN + 1] = { 0 }; char lineBuf[2048] = { 0 }; char *line = lineBuf; s32 lineLen = 0; @@ -223,77 +225,36 @@ s32 configLoad(const char *fname) while (fgets(lineBuf, sizeof(lineBuf), f)) { line = lineBuf; - // left-trim whitespace - while (*line && isspace(*line)) { - ++line; - } - - lineLen = strlen(line); - if (!lineLen) { - continue; - } + line = strParseToken(line, token, NULL); - // right-trim whitespace and \n - for (s32 i = lineLen - 1; i >= 0 && isspace(line[i]); --i, --lineLen); - line[lineLen] = '\0'; - - if (line[0] == ';' || line[0] == '#') { - // comment; skip the rest of the line - continue; - } else if (line[0] == '[') { - // section; skip [ and find matching ] - ++line; - char *end = line; - while (*end && *end != ']') { - ++end; - } - // found anything? - if (*end != ']') { - // didn't find shit + if (token[0] == '[' && token[1] == '\0') { + // section; get name + line = strParseToken(line, token, NULL); + if (!token[0]) { + sysLogPrintf(LOG_ERROR, "configLoad: malformed section line: %s", lineBuf); continue; } - // eat ] and whitespace on the right - while (end > line && isspace(end[-1])) { - --end; + strncpy(curSec, token, CONFIG_MAX_SECNAME); + // eat ] + line = strParseToken(line, token, NULL); + if (token[0] != ']' || token[1] != '\0') { + sysLogPrintf(LOG_ERROR, "configLoad: malformed section line: %s", lineBuf); } - *end = '\0'; - // empty []? - if (end == line) { + } else if (token[0]) { + // probably a key=value pair; append key name to section name + snprintf(keyBuf, sizeof(keyBuf) - 1, "%s.%s", curSec, token); + // eat = + line = strParseToken(line, token, NULL); + if (token[0] != '=' || token[1] != '\0') { + sysLogPrintf(LOG_ERROR, "configLoad: malformed keyvalue line: %s", lineBuf); continue; } - // eat whitespace on the left - while (line < end && isspace(*line)) { - ++line; - } - // copy out the section name - const s32 len = end - line; - if (len > CONFIG_MAX_SECNAME) { - continue; // too long - } - memcpy(curSec, line, len); - curSec[len] = '\0'; - } else { - // probably a key=value pair - char *eq = strchr(line, '='); - if (!eq) { - continue; // garbage - } - char *value = eq + 1; - if (!*value) { - continue; // just a = - } - // trim right whitespace on the key - --eq; - while (eq > line && isspace(*eq)) { - --eq; - } - if (eq == line) { - continue; // = with nothing on the left + // the rest of the line is the value + line = strTrim(line); + if (line[0] == '"') { + line = strUnquote(line); } - eq[1] = '\0'; - // assemble full key name and read the value - snprintf(keyBuf, sizeof(keyBuf) - 1, "%s.%s", curSec, line); - configSetFromString(keyBuf, value); + configSetFromString(keyBuf, line); } } diff --git a/port/src/fs.c b/port/src/fs.c index fb56a6af9..948cf8f93 100644 --- a/port/src/fs.c +++ b/port/src/fs.c @@ -9,11 +9,13 @@ #include "config.h" #include "system.h" #include "platform.h" +#include "utils.h" #include "fs.h" -#define DEFAULT_DATADIR_NAME "data" +#define DEFAULT_BASEDIR_NAME "data" -static char dataDir[FS_MAXPATH + 1]; // replaces $D +static char baseDir[FS_MAXPATH + 1]; // replaces $B +static char modDir[FS_MAXPATH + 1]; // replaces $M static char saveDir[FS_MAXPATH + 1]; // replaces $S static char homeDir[FS_MAXPATH + 1]; // replaces $H static char exeDir[FS_MAXPATH + 1]; // replaces $E @@ -57,7 +59,8 @@ const char *fsFullPath(const char *relPath) switch (relPath[1]) { case 'E': expStr = exeDir; break; case 'H': expStr = homeDir; break; - case 'D': expStr = dataDir; break; + case 'M': expStr = modDir; break; + case 'B': expStr = baseDir; break; case 'S': expStr = saveDir; break; default: break; } @@ -71,13 +74,20 @@ const char *fsFullPath(const char *relPath) } // couldn't expand anything, return as is return relPath; - } else if (!dataDir[0] || fsPathIsAbsolute(relPath) || fsPathIsCwdRelative(relPath)) { - // user explicitly wants working directory or this is an absolute path or we have no dataDir set up yet + } else if (!baseDir[0] || fsPathIsAbsolute(relPath) || fsPathIsCwdRelative(relPath)) { + // user explicitly wants working directory or this is an absolute path or we have no baseDir set up yet return relPath; } - // path relative to data dir - snprintf(pathBuf, FS_MAXPATH, "%s/%s", dataDir, relPath); + // path relative to mod or base dir; this will be a read request, so check where the file actually is + if (modDir[0]) { + snprintf(pathBuf, FS_MAXPATH, "%s/%s", modDir, relPath); + if (fsFileSize(pathBuf) >= 0) { + return pathBuf; + } + } + // fall back to basedir + snprintf(pathBuf, FS_MAXPATH, "%s/%s", baseDir, relPath); return pathBuf; } @@ -93,21 +103,45 @@ s32 fsInit(void) sysGetHomePath(homeDir, FS_MAXPATH); } - // get path to data dir and expand it if needed - const char *path = sysArgGetString("--datadir"); + // get path to base dir and expand it if needed + const char *path = sysArgGetString("--basedir"); if (!path) { - // check if there's a data directory in working directory or homeDir, otherwise default to exe directory - path = "$E/" DEFAULT_DATADIR_NAME; + // check if there's a `data` directory in working directory or homeDir, otherwise default to exe directory + path = "$E/" DEFAULT_BASEDIR_NAME; if (!portable) { - if (fsFileSize("./" DEFAULT_DATADIR_NAME) >= 0) { - path = "./" DEFAULT_DATADIR_NAME; - } else if (fsFileSize("$H/" DEFAULT_DATADIR_NAME)) { - path = "$H/" DEFAULT_DATADIR_NAME; + if (fsFileSize("./" DEFAULT_BASEDIR_NAME) >= 0) { + path = "./" DEFAULT_BASEDIR_NAME; + } else if (fsFileSize("$H/" DEFAULT_BASEDIR_NAME)) { + path = "$H/" DEFAULT_BASEDIR_NAME; } } } - - strncpy(dataDir, fsFullPath(path), FS_MAXPATH); + strncpy(baseDir, fsFullPath(path), FS_MAXPATH); + + // get path to mod dir and expand it if needed + // mod directory is overlaid on top of base directory + path = sysArgGetString("--moddir"); + if (path) { + if (fsPathIsAbsolute(path) || fsPathIsCwdRelative(path) || path[0] == '$') { + // path is explicit; check as-is + if (fsFileSize(path) >= 0) { + strncpy(modDir, fsFullPath(path), FS_MAXPATH); + } + } else { + // path is relative to workdir; try to find it + const char *priority[] = { ".", "$E", "$H" }; + for (s32 i = 0; i < 2 + (portable != 0); ++i) { + char *tmp = strFmt("%s/%s", priority[i], path); + if (fsFileSize(tmp) >= 0) { + strncpy(modDir, fsFullPath(tmp), FS_MAXPATH); + break; + } + } + } + if (!modDir[0]) { + sysLogPrintf(LOG_WARNING, "could not find specified moddir `%s`", path); + } + } // get path to save dir and expand it if needed path = sysArgGetString("--savedir"); @@ -136,12 +170,20 @@ s32 fsInit(void) strncpy(saveDir, fsFullPath(path), FS_MAXPATH); - sysLogPrintf(LOG_NOTE, "data dir: %s", dataDir); + if (modDir[0]) { + sysLogPrintf(LOG_NOTE, " mod dir: %s", modDir); + } + sysLogPrintf(LOG_NOTE, "base dir: %s", baseDir); sysLogPrintf(LOG_NOTE, "save dir: %s", saveDir); return 0; } +const char *fsGetModDir(void) +{ + return modDir[0] ? modDir : NULL; +} + void *fsFileLoad(const char *name, u32 *outSize) { const char *fullName = fsFullPath(name); diff --git a/port/src/main.c b/port/src/main.c index b92377ba8..b1566bf13 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -14,6 +14,7 @@ #include "fs.h" #include "romdata.h" #include "config.h" +#include "mod.h" #include "system.h" u32 g_OsMemSize = 0; @@ -95,6 +96,10 @@ int main(int argc, const char **argv) gameLoadConfig(); + if (fsGetModDir()) { + modConfigLoad(MOD_CONFIG_FNAME); + } + atexit(cleanup); bootCreateSched(); diff --git a/port/src/mod.c b/port/src/mod.c new file mode 100644 index 000000000..7d536bf35 --- /dev/null +++ b/port/src/mod.c @@ -0,0 +1,256 @@ +#include +#include +#include +#include "platform.h" +#include "system.h" +#include "fs.h" +#include "utils.h" +#include "romdata.h" +#include "mod.h" +#include "data.h" +#include "game/stagetable.h" + +extern struct stagemusic g_StageTracks[]; +extern struct stageallocation g_StageAllocations8Mb[]; + +static inline char *modConfigParseFileValue(char *p, char *token, s32 *filenum) +{ + p = strParseToken(p, token, NULL); + if (!token[0]) { + return NULL; // empty + } + // check if it is a number already + s32 num = strtol(token, NULL, 0); + if (num > 0 && romdataFileGetName(num)) { + *filenum = num; + return p; + } + // it's a filename + num = romdataFileGetNumForName(strUnquote(token)); + if (num >= 0) { + *filenum = num; + return p; + } + // the filename was invalid + return NULL; +} + +static inline char *modConfigParseNumericValue(char *p, char *token, s32 *out) +{ + p = strParseToken(p, token, NULL); + if (!token[0]) { + return NULL; // empty + } + char *endp = token; + const s32 num = strtol(token, &endp, 0); + if (num == 0 && (endp == token || *endp != '\0')) { + return NULL; + } + *out = num; + return p; +} + +static inline char *modConfigParseStageMusic(char *p, char *token, s32 stagenum) +{ + struct stagemusic *smus = NULL; + for (struct stagemusic *p = g_StageTracks; p->stagenum; ++p) { + if (p->stagenum == stagenum) { + smus = p; + break; + } + } + + if (!smus) { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: music can't be changed for this stage", stagenum); + return NULL; + } + + // eat opening bracket + p = strParseToken(p, token, NULL); + if (token[0] != '{' || token[1] != '\0') { + return NULL; + } + + // parse keyvalues until } is reached + s32 tmp = 0; + p = strParseToken(p, token, NULL); + while (p && token[0] && strcmp(token, "}") != 0) { + if (!strcmp(token, "primarytrack")) { + p = modConfigParseNumericValue(p, token, &tmp); + if (!p || tmp < 0 || tmp > 128) { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: music: invalid primarytrack value: %s", stagenum, token); + return NULL; + } + smus->primarytrack = tmp; + } else if (!strcmp(token, "ambienttrack")) { + p = modConfigParseNumericValue(p, token, &tmp); + if (!p || tmp < 0 || tmp > 128) { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: music: invalid ambienttrack value: %s", stagenum, token); + return NULL; + } + smus->ambienttrack = tmp; + } else if (!strcmp(token, "xtrack")) { + p = modConfigParseNumericValue(p, token, &tmp); + if (!p || tmp < 0 || tmp > 128) { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: music: invalid xtrack value: %s", stagenum, token); + return NULL; + } + smus->xtrack = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: music: invalid key: %s", stagenum, token); + return NULL; + } + p = strParseToken(p, token, NULL); + } + + if (token[0] != '}') { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: unterminated music block", stagenum); + return NULL; + } + + return p; +} + +static inline char *modConfigParseStage(char *p, char *token) +{ + // stage number + p = strParseToken(p, token, NULL); + const s32 stagenum = strtol(token, NULL, 0); + if (stagenum <= 0x01 || stagenum > 0x50) { + return NULL; + } + + // eat opening bracket + p = strParseToken(p, token, NULL); + if (token[0] != '{' || token[1] != '\0') { + return NULL; + } + + // find the stage table pointers this corresponds to + struct stagetableentry *stab = NULL; + struct stageallocation *salloc = NULL; + const s32 sidx = stageGetIndex(stagenum); + if (sidx >= 0) { + stab = &g_Stages[sidx]; + } + for (struct stageallocation *p = g_StageAllocations8Mb; p->stagenum; ++p) { + if (p->stagenum == stagenum) { + salloc = p; + break; + } + } + + // parse keyvalues until } is reached + s32 tmp = 0; + p = strParseToken(p, token, NULL); + while (p && token[0] && strcmp(token, "}") != 0) { + if (!strcmp(token, "bgfile")) { + // bg FILE_NAME_OR_NUM + p = modConfigParseFileValue(p, token, &tmp); + if (p && stab) { + stab->bgfileid = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid bgfile value: %s", stagenum, token); + return NULL; + } + } else if (!strcmp(token, "tilesfile")) { + // tiles FILE_NAME_OR_NUM + p = modConfigParseFileValue(p, token, &tmp); + if (p && stab) { + stab->tilefileid = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid tilesfile value: %s", stagenum, token); + return NULL; + } + } else if (!strcmp(token, "padsfile")) { + // tiles FILE_NAME_OR_NUM + p = modConfigParseFileValue(p, token, &tmp); + if (p && stab) { + stab->padsfileid = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid padsfile value: %s", stagenum, token); + return NULL; + } + } else if (!strcmp(token, "setupfile")) { + // setup FILE_NAME_OR_NUM + p = modConfigParseFileValue(p, token, &tmp); + if (p && stab) { + stab->setupfileid = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid setupfile value: %s", stagenum, token); + return NULL; + } + } else if (!strcmp(token, "mpsetupfile")) { + // mpsetup FILE_NAME_OR_NUM + p = modConfigParseFileValue(p, token, &tmp); + if (p && stab) { + stab->mpsetupfileid = tmp; + } else { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid mpsetupfile value: %s", stagenum, token); + return NULL; + } + } else if (!strcmp(token, "allocation")) { + // allocation "ALLOCSTRING" + p = strParseToken(p, token, NULL); + char *str = strUnquote(token); + if (!p || !str[0] || !salloc) { + sysLogPrintf(LOG_ERROR, "modconfig: stage 0x%02x: invalid allocation value: %s", stagenum, token); + return NULL; + } + // FIXME: this leaks + str = strDuplicate(str); + if (str) { + salloc->string = str; + } + } else if (!strcmp(token, "music")) { + // music { KEYVALUES... } + p = modConfigParseStageMusic(p, token, stagenum); + if (!p) { + return NULL; + } + } + p = strParseToken(p, token, NULL); + } + + if (token[0] != '}') { + sysLogPrintf(LOG_ERROR, "modconfig: unterminated stage 0x%02x block", stagenum); + return NULL; + } + + return p; +} + +s32 modConfigLoad(const char *fname) +{ + u32 dataLen = 0; + char *data = fsFileLoad(fname, &dataLen); + if (!data) { + return false; + } + + s32 success = true; + char token[UTIL_MAX_TOKEN + 1] = { 0 }; + char *end = data + dataLen; + char *p = strParseToken(data, token, NULL); + while (p && token[0]) { + if (!strcmp(token, "stage")) { + // stage NUMBER { KEYVALUES... } + char *prev = p; + p = modConfigParseStage(p, token); + if (!p) { + sysLogPrintf(LOG_ERROR, "modconfig: malformed stage block at offset %d", prev - data); + success = false; + break; + } + } else { + // garbage + sysLogPrintf(LOG_ERROR, "modconfig: unexpected %s at offset %d", token[0] ? token : "end of file", p - data); + success = false; + break; + } + p = strParseToken(p, token, NULL); + } + + sysMemFree(data); + return success; +} diff --git a/port/src/romdata.c b/port/src/romdata.c index b7f420f64..2bc02469f 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -431,13 +431,36 @@ void romdataFileFree(s32 fileNum) } if (fileSlots[fileNum].source == SRC_EXTERNAL) { - free(fileSlots[fileNum].data); + sysMemFree(fileSlots[fileNum].data); fileSlots[fileNum].data = NULL; } fileSlots[fileNum].source = SRC_UNLOADED; } +const char *romdataFileGetName(s32 fileNum) +{ + if (fileNum < 1 || fileNum >= ROMDATA_MAX_FILES) { + return NULL; + } + return fileSlots[fileNum].name; +} + +s32 romdataFileGetNumForName(const char *name) +{ + if (!name || !name[0]) { + return -1; + } + + for (s32 i = 0; i < ROMDATA_MAX_FILES; ++i) { + if (fileSlots[i].name && !strcmp(fileSlots[i].name, name)) { + return i; + } + } + + return -1; +} + u8 *romdataSegGetData(const char *segName) { return romdataGetSeg(segName)->data; diff --git a/port/src/utils.c b/port/src/utils.c new file mode 100644 index 000000000..319807e76 --- /dev/null +++ b/port/src/utils.c @@ -0,0 +1,198 @@ +#include +#include +#include +#include +#include +#include +#include +#include "platform.h" +#include "system.h" +#include "utils.h" + +static inline bool isSingleCharToken(const s32 ch) +{ + switch (ch) { + case '{': + case '}': + case '[': + case ']': + case '(': + case ')': + case ',': + case '\'': + case '=': + return true; + default: + return false; + } +} + +char *strFmt(const char *fmt, ...) +{ + static char buf[4096]; + + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + return buf; +} + +char *strRightTrim(char *str) +{ + if (!str) { + return NULL; + } + + const s32 len = strlen(str); + for (s32 i = len - 1; i >= 0 && isspace(str[i]); --i) { + str[i] = '\0'; + } + + return str; +} + +char *strTrim(char *str) +{ + if (!str) { + return NULL; + } + + // left trim + while (*str && (u8)*str < ' ') { + ++str; + } + + // right trim + const s32 len = strlen(str); + for (s32 i = len - 1; i > 0 && isspace(str[i]); --i) { + str[i] = '\0'; + } + + return str; +} + +char *strUnquote(char *str) +{ + if (!str) { + return NULL; + } + + if (*str == '"') { + ++str; + } + + char *end = strrchr(str, '"'); + if (end && end[1] == '\0') { + *end = '\0'; + } + + return str; +} + +char *strParseToken(char *str, char *out, s32 *outCount) +{ + if (outCount) { + *outCount = 0; + } + + if (!out) { + return NULL; + } + + out[0] = '\0'; + + if (!str) { + return NULL; + } + + s32 cnt = 0; + while (*str) { + // skip whitespace and other garbage + while (*str && (u8)*str <= ' ') { + ++str; + } + if (!*str) { + // just whitespace + return NULL; + } + + // skip #, ; and // comments + if (*str == ';' || *str == '#' || (str[0] == '/' && str[1] == '/')) { + while (*str && *str != '\n') { + ++str; + } + continue; + } + + if (*str == '"') { + // quoted string; treat it as an entire token, including the quotes + out[cnt++] = *str++; + while (true) { + if (*str == '\0') { + // unterminated quoted string + break; + } + + if (str[0] == '\\' && str[1] == '"') { + // escaped quote; add to token + if (cnt + 1 < UTIL_MAX_TOKEN) { + out[cnt++] = str[1]; + } + str += 2; + continue; + } + + // add char to token + const s32 ch = (u8)*str++; + if (cnt + 1 < UTIL_MAX_TOKEN) { + out[cnt++] = ch; + } + + if (ch == '"') { + // ch was the closing quote + break; + } + } + break; + } + + if (isSingleCharToken(*str)) { + // single char token + out[cnt++] = *str++; + break; + } + + // regular single word + do { + if (cnt + 1 < UTIL_MAX_TOKEN) { + out[cnt++] = *str++; + } + if (isSingleCharToken(*str)) { + break; + } + } while ((u8)*str > ' '); + break; + } + + out[cnt] = '\0'; + if (outCount) { + *outCount = cnt; + } + + return str; +} + +char *strDuplicate(const char *str) +{ + if (!str) { + return NULL; + } + const u32 len = strlen(str); + char *out = sysMemAlloc(len + 1); + if (out) { + memcpy(out, str, len + 1); + } + return out; +} \ No newline at end of file