From 83af8d060fd0d2bc9bface55e1c3aa4c624adce6 Mon Sep 17 00:00:00 2001 From: Andrei Drexler Date: Sun, 29 Aug 2021 17:11:28 +0300 Subject: [PATCH] Add basic localized strings support for 2021 re-release See https://github.com/Novum/vkQuake/pull/345 --- Quake/common.c | 421 ++++++++++++++++++++++++++++++++++++++++++++++++ Quake/common.h | 9 ++ Quake/host.c | 4 + Quake/pr_cmds.c | 35 +++- 4 files changed, 463 insertions(+), 6 deletions(-) diff --git a/Quake/common.c b/Quake/common.c index 9b9d708b..6206c3fe 100644 --- a/Quake/common.c +++ b/Quake/common.c @@ -2463,3 +2463,424 @@ long FS_filelength (fshandle_t *fh) return fh->length; } +/* +============================================================================ + LOCALIZATION +============================================================================ +*/ +typedef struct +{ + char *key; + char *value; +} locentry_t; + +typedef struct +{ + int numentries; + int maxnumentries; + int numindices; + unsigned *indices; + locentry_t *entries; + char *text; +} localization_t; + +static localization_t localization; + +/* +================ +COM_HashString +Computes the FNV-1a hash of string str +================ +*/ +unsigned COM_HashString (const char *str) +{ + unsigned hash = 0x811c9dc5u; + while (*str) + { + hash ^= *str++; + hash *= 0x01000193u; + } + return hash; +} + +/* +================ +LOC_LoadFile +================ +*/ +void LOC_LoadFile (const char *file) +{ + char path[1024]; + FILE *fp = NULL; + int i,lineno; + char *cursor; + + // clear existing data + if (localization.text) + { + free(localization.text); + localization.text = NULL; + } + localization.numentries = 0; + localization.numindices = 0; + + if (!file || !*file) + return; + + Con_Printf("\nLanguage initialization\n"); + + q_snprintf(path, sizeof(path), "%s/%s", com_basedir, file); + fp = fopen(path, "r"); + if (!fp) goto fail; + fseek(fp, 0, SEEK_END); + i = ftell(fp); + if (i <= 0) goto fail; + localization.text = (char *) calloc(1, i+1); + if (!localization.text) + { +fail: if (fp) fclose(fp); + Con_Printf("Couldn't load '%s'\nfrom '%s'\n", file, com_basedir); + return; + } + fseek(fp, 0, SEEK_SET); + fread(localization.text, 1, i, fp); + fclose(fp); + + cursor = localization.text; + + // skip BOM + if ((unsigned char)(cursor[0]) == 0xEF && (unsigned char)(cursor[1]) == 0xBB && cursor[2] == 0xB) + cursor += 3; + + lineno = 0; + while (*cursor) + { + char *line, *equals; + + lineno++; + + // skip leading whitespace + while (q_isblank(*cursor)) + ++cursor; + + line = cursor; + equals = NULL; + // find line end and first equals sign, if any + while (*cursor && *cursor != '\n') + { + if (*cursor == '=' && !equals) + equals = cursor; + cursor++; + } + + if (line[0] == '/') + { + if (line[1] != '/') + Con_DPrintf("LOC_LoadFile: malformed comment on line %d\n", lineno); + } + else if (equals) + { + char *key_end = equals; + qboolean leading_quote; + qboolean trailing_quote; + locentry_t *entry; + char *value_src; + char *value_dst; + char *value; + + // trim whitespace before equals sign + while (key_end != line && q_isspace(key_end[-1])) + key_end--; + *key_end = 0; + + value = equals + 1; + // skip whitespace after equals sign + while (value != cursor && q_isspace(*value)) + value++; + + leading_quote = (*value == '\"'); + trailing_quote = false; + value += leading_quote; + + // transform escape sequences in-place + value_src = value; + value_dst = value; + while (value_src != cursor) + { + if (*value_src == '\\' && value_src + 1 != cursor) + { + char c = value_src[1]; + value_src += 2; + switch (c) + { + case 'n': *value_dst++ = '\n'; break; + case 't': *value_dst++ = '\t'; break; + case 'v': *value_dst++ = '\v'; break; + case 'b': *value_dst++ = '\b'; break; + case 'f': *value_dst++ = '\f'; break; + + case '"': + case '\'': + *value_dst++ = c; + break; + + default: + Con_Printf("LOC_LoadFile: unrecognized escape sequence \\%c on line %d\n", c, lineno); + *value_dst++ = c; + break; + } + continue; + } + + if (*value_src == '\"') + { + trailing_quote = true; + *value_dst = 0; + break; + } + + *value_dst++ = *value_src++; + } + + // if not a quoted string, trim trailing whitespace + if (!trailing_quote) + { + while (value_dst != value && q_isblank(value_dst[-1])) + { + *value_dst = 0; + value_dst--; + } + } + + if (localization.numentries == localization.maxnumentries) + { + // grow by 50% + localization.maxnumentries += localization.maxnumentries >> 1; + localization.maxnumentries = q_max(localization.maxnumentries, 32); + localization.entries = (locentry_t*) realloc(localization.entries, sizeof(*localization.entries) * localization.maxnumentries); + } + + entry = &localization.entries[localization.numentries++]; + entry->key = line; + entry->value = value; + } + + if (*cursor) + *cursor++ = 0; // terminate line and advance to next + } + + // hash all entries + + localization.numindices = localization.numentries * 2; // 50% load factor + if (localization.numindices == 0) + { + Con_Printf("No localized strings in file '%s'\n", file); + return; + } + + localization.indices = (unsigned*) realloc(localization.indices, localization.numindices * sizeof(*localization.indices)); + memset(localization.indices, 0, localization.numindices * sizeof(*localization.indices)); + + for (i = 0; i < localization.numentries; i++) + { + locentry_t *entry = &localization.entries[i]; + unsigned pos = COM_HashString(entry->key) % localization.numindices, end = pos; + + for (;;) + { + if (!localization.indices[pos]) + { + localization.indices[pos] = i + 1; + break; + } + + ++pos; + if (pos == localization.numindices) + pos = 0; + + if (pos == end) + Sys_Error("LOC_LoadFile failed"); + } + } + + Con_Printf("Loaded %d strings from '%s'\n", localization.numentries, file); +} + +/* +================ +LOC_Init +================ +*/ +void LOC_Init(void) +{ + LOC_LoadFile("localization/loc_english.txt"); +} + +/* +================ +LOC_Shutdown +================ +*/ +void LOC_Shutdown(void) +{ + free(localization.indices); + free(localization.entries); + free(localization.text); +} + +/* +================ +LOC_GetRawString + +Returns localized string if available, or NULL otherwise +================ +*/ +const char* LOC_GetRawString (const char *key) +{ + unsigned pos, end; + + if (!localization.numindices || !key || !*key || *key != '$') + return NULL; + key++; + + pos = COM_HashString(key) % localization.numindices; + end = pos; + + do + { + unsigned idx = localization.indices[pos]; + locentry_t *entry; + if (!idx) + return NULL; + + entry = &localization.entries[idx - 1]; + if (!Q_strcmp(entry->key, key)) + return entry->value; + + ++pos; + if (pos == localization.numindices) + pos = 0; + } while (pos != end); + + return NULL; +} + +/* +================ +LOC_GetString + +Returns localized string if available, or input string otherwise +================ +*/ +const char* LOC_GetString (const char *key) +{ + const char* value = LOC_GetRawString(key); + return value ? value : key; +} + +/* +================ +LOC_ParseArg + +Returns argument index (>= 0) and advances the string if it starts with a placeholder ({} or {N}), +otherwise returns a negative value and leaves the pointer unchanged +================ +*/ +static int LOC_ParseArg (const char **pstr) +{ + int arg; + const char *start; + const char *str = *pstr; + + // opening brace + if (*str != '{') + return -1; + start = ++str; + + // optional index, defaulting to 0 + arg = 0; + while (q_isdigit(*str)) + arg = arg * 10 + *str++ - '0'; + + // closing brace + if (*str != '}') + return -1; + *pstr = ++str; + + return arg; +} + +/* +================ +LOC_HasPlaceholders +================ +*/ +qboolean LOC_HasPlaceholders (const char *str) +{ + if (!localization.numindices) + return false; + while (*str) + { + if (LOC_ParseArg(&str) >= 0) + return true; + str++; + } + return false; +} + +/* +================ +LOC_Format + +Replaces placeholders (of the form {} or {N}) with the corresponding arguments + +Returns number of written chars, excluding the NUL terminator +If len > 0, output is always NUL-terminated +================ +*/ +size_t LOC_Format (const char *format, const char* (*getarg_fn) (int idx, void* userdata), void* userdata, char* out, size_t len) +{ + size_t written = 0; + int numargs = 0; + + if (!len) + { + Con_DPrintf("LOC_Format: no output space\n"); + return 0; + } + --len; // reserve space for the terminator + + while (*format && written < len) + { + const char* insert; + size_t space_left; + size_t insert_len; + int argindex = LOC_ParseArg(&format); + + if (argindex < 0) + { + out[written++] = *format++; + continue; + } + + insert = getarg_fn(argindex, userdata); + space_left = len - written; + insert_len = Q_strlen(insert); + + if (insert_len > space_left) + { + Con_DPrintf("LOC_Format: overflow at argument #%d\n", numargs); + insert_len = space_left; + } + + Q_memcpy(out + written, insert, insert_len); + written += insert_len; + } + + if (*format) + Con_DPrintf("LOC_Format: overflow\n"); + + out[written] = 0; + + return written; +} diff --git a/Quake/common.h b/Quake/common.h index eff6b649..9aaa8b2c 100644 --- a/Quake/common.h +++ b/Quake/common.h @@ -190,6 +190,15 @@ void COM_CreatePath (char *path); char *va (const char *format, ...) FUNC_PRINTF(1,2); // does a varargs printf into a temp buffer +unsigned COM_HashString (const char *str); + +// localization support for 2021 rerelease version: +void LOC_Init (void); +void LOC_Shutdown (void); +const char* LOC_GetRawString (const char *key); +const char* LOC_GetString (const char *key); +qboolean LOC_HasPlaceholders (const char *str); +size_t LOC_Format (const char *format, const char* (*getarg_fn)(int idx, void* userdata), void* userdata, char* out, size_t len); //============================================================================ diff --git a/Quake/host.c b/Quake/host.c index b1028e43..d0bf1c96 100644 --- a/Quake/host.c +++ b/Quake/host.c @@ -873,6 +873,8 @@ void Host_Init (void) CL_Init (); } + LOC_Init (); // for 2021 rerelease support. + Hunk_AllocName (0, "-HOST_HUNKLEVEL-"); host_hunklevel = Hunk_LowMark (); @@ -936,5 +938,7 @@ void Host_Shutdown(void) } LOG_Close (); + + LOC_Shutdown (); } diff --git a/Quake/pr_cmds.c b/Quake/pr_cmds.c index 94462451..cd62e52d 100644 --- a/Quake/pr_cmds.c +++ b/Quake/pr_cmds.c @@ -47,21 +47,44 @@ static char *PR_GetTempString (void) =============================================================================== */ +static const char* PF_GetStringArg(int idx, void* userdata) +{ + if (userdata) + idx += *(int*)userdata; + if (idx < 0 || idx >= pr_argc) + return ""; + return LOC_GetString(G_STRING(OFS_PARM0 + idx * 3)); +} + static char *PF_VarString (int first) { int i; static char out[1024]; + const char *format; size_t s; out[0] = 0; s = 0; - for (i = first; i < pr_argc; i++) + + if (first >= pr_argc) + return out; + + format = LOC_GetString(G_STRING((OFS_PARM0 + first * 3))); + if (LOC_HasPlaceholders(format)) { - s = q_strlcat(out, G_STRING((OFS_PARM0+i*3)), sizeof(out)); - if (s >= sizeof(out)) + int offset = first + 1; + s = LOC_Format(format, PF_GetStringArg, &offset, out, sizeof(out)); + } + else + { + for (i = first; i < pr_argc; i++) { - Con_Warning("PF_VarString: overflow (string truncated)\n"); - return out; + s = q_strlcat(out, LOC_GetString(G_STRING(OFS_PARM0+i*3)), sizeof(out)); + if (s >= sizeof(out)) + { + Con_Warning("PF_VarString: overflow (string truncated)\n"); + return out; + } } } if (s > 255) @@ -1540,7 +1563,7 @@ static void PF_WriteCoord (void) static void PF_WriteString (void) { - MSG_WriteString (WriteDest(), G_STRING(OFS_PARM1)); + MSG_WriteString (WriteDest(), LOC_GetString(G_STRING(OFS_PARM1))); } static void PF_WriteEntity (void)