og-odamex/common/stringtable.cpp
2026-01-05 01:52:47 +00:00

431 lines
9.5 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id$
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// String Abstraction Layer (StringTable)
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include "stringtable.h"
#include "cmdlib.h"
#include "i_system.h"
#include "oscanner.h"
#include "stringenums.h"
#include "w_wad.h"
/**
* @brief Map a ZDoom game name to Odamex's internals and returns true if
* the current game is the passed string.
*
* @param str String to check against.
* @return True if the game matches the passed string, otherwise false.
*/
static bool IfGameZDoom(const std::string& str)
{
// TODO: should this account for rekkr too? uzdoom seems to just have
// doom, strife, heretic, hexen, and chex here
if (!stricmp(str.c_str(), "doom") && !IsChexMission(::gamemission) &&
::gamemode != undetermined && ::gamemission != commercial_hacx)
{
return true;
}
if (!stricmp(str.c_str(), "chex") && IsChexMission(::gamemission))
{
return true;
}
if (!stricmp(str.c_str(), "hacx") && ::gamemission == commercial_hacx)
{
return true;
}
// We don't support anything else.
return false;
}
bool StringTable::canSetPassString(int pass, const std::string& name) const
{
StringHash::const_iterator it = _stringHash.find(name);
// New string?
if (it == _stringHash.end())
return true;
// Found an entry, does the string exist?
if ((*it).second.string.first == false)
return true;
// Was the string set with a less exact pass?
if ((*it).second.pass >= pass)
return true;
return false;
}
void StringTable::clearStrings()
{
_stringHash.clear();
}
//
// Loads a language
//
void StringTable::loadLanguage(const char* code, bool exactMatch, int pass, char* lump,
size_t lumpLen)
{
OScannerConfig config = {
"LANGUAGE", // lumpName
false, // semiComments
true, // cComments
};
OScanner os = OScanner::openBuffer(config, lump, lump + lumpLen);
while (os.scan())
{
// Parse a language section.
bool shouldParseSection = false;
os.assertTokenIs("[");
while (os.scan())
{
// Code to check against.
char checkCode[4] = {'\0', '\0', '\0', '\0'};
if (os.compareToken("]"))
{
break;
}
else if (os.compareToken("default"))
{
// Default has a speical ID.
strncpy(checkCode, "**", 3);
}
else
{
// Turn the language into an ID.
const std::string& lang = os.getToken();
if (lang.length() == 2 || lang.length() == 3)
{
strncpy(checkCode, lang.c_str(), lang.length());
}
else
{
os.error("Language identifier must be 2 or 3 characters");
}
}
if (exactMatch && strncmp(code, checkCode, 3) == 0)
{
shouldParseSection = true;
}
else if (!exactMatch && strncmp(code, checkCode, 2) == 0)
{
shouldParseSection = true;
}
}
if (shouldParseSection)
{
// Parse all of the strings in this section.
while (os.scan())
{
if (os.compareToken("["))
{
// We reached the end of the section.
os.unScan();
break;
}
// $ifgame() does not appear to be documented in the wiki,
// but it causes the next string to only be set it the game
// matches up.
bool skip = false;
if (os.compareToken("$"))
{
os.scan();
os.assertTokenIs("ifgame");
os.scan();
os.assertTokenIs("(");
os.scan();
skip = !IfGameZDoom(os.getToken());
os.scan();
os.assertTokenIs(")");
os.scan();
}
// String name
const std::string& name = os.getToken();
// If we can find the token, skip past the string
if (!canSetPassString(pass, name))
{
while (os.scan())
{
if (os.compareToken(";"))
break;
}
continue;
}
os.scan();
os.assertTokenIs("=");
// Grab the string value.
std::string value;
while (os.scan())
{
const std::string piece = os.getToken();
if (piece.compare(";") == 0)
{
// Found the end of the string, next batter up.
break;
}
value += piece;
}
replaceEscapes(value);
if (skip)
{
continue;
}
setPassString(pass, name, value);
}
}
else
{
// Skip past all of the strings in this section.
while (os.scan())
{
if (os.compareToken("["))
{
// Found another section, parse it.
os.unScan();
break;
}
}
}
}
}
void StringTable::loadStringsLump(const int lump, const char* lumpname, const bool engOnly)
{
// Can't use Z_Malloc this early, so we use raw new/delete.
size_t len = W_LumpLength(lump);
char* languageLump = new char[len + 1];
W_ReadLump(lump, languageLump);
languageLump[len] = '\0';
// String replacement pass. Strings in an later pass can be replaced
// by a string in an earlier pass from another lump.
int pass = 1;
if (!engOnly)
{
// Load language-specific strings.
for (size_t i = 0; i < ARRAY_LENGTH(::LanguageIDs); i++)
{
// Deconstruct code into something less confusing.
char code[4];
UNMAKE_ID(code, ::LanguageIDs[i]);
// Language codes are up to three letters long.
code[3] = '\0';
// Try the full language code (enu).
loadLanguage(code, true, pass++, languageLump, len);
// Try the partial language code (en).
code[2] = '\0';
loadLanguage(code, true, pass++, languageLump, len);
// Try an inexact match for all languages in the same family (en_).
loadLanguage(code, false, pass++, languageLump, len);
}
}
// Load string defaults.
loadLanguage("**", true, pass++, languageLump, len);
delete[] languageLump;
}
void StringTable::prepareIndexes()
{
// All of the default strings have index numbers that represent their
// position in the now-removed enumeration. This function simply sets
// them all up.
for (size_t i = 0; i < ARRAY_LENGTH(::stringIndexes); i++)
{
OString name = *(::stringIndexes[i]);
StringHash::iterator it = _stringHash.find(name);
if (it == _stringHash.end())
{
TableEntry entry = {std::make_pair(false, ""), 0xFF, static_cast<int>(i)};
_stringHash.emplace(name, entry);
}
}
}
void StringTable::replaceEscapes(std::string& str)
{
size_t index = 0;
for (;;)
{
// Find the initial slash.
index = str.find("\\", index);
if (index == std::string::npos || index == str.length() - 1)
break;
// Substitute the escape string.
switch (str.at(index + 1))
{
case 'n':
str.replace(index, 2, "\n");
break;
case '\\':
str.replace(index, 2, "\\");
break;
}
index += 1;
}
}
//
// Dump all strings to the console.
//
// Sometimes a blunt instrument is what is necessary.
//
void StringTable::dumpStrings()
{
for (const auto& [first, second] : _stringHash)
{
PrintFmt(PRINT_HIGH, "{} (pass: {}, index: {}) = {}\n", first,
second.pass, second.index, second.string.second);
}
}
//
// See if a string exists in the table.
//
bool StringTable::hasString(const OString& name) const
{
StringHash::const_iterator it = _stringHash.find(name);
if (it == _stringHash.end())
return false;
if ((*it).second.string.first == false)
return false;
return true;
}
//
// Load strings from all LANGUAGE lumps in all loaded WAD files.
//
void StringTable::loadStrings(const bool engOnly)
{
clearStrings();
prepareIndexes();
int lump = -1;
lump = -1;
while ((lump = W_FindLump("LANGUAGE", lump)) != -1)
{
loadStringsLump(lump, "LANGUAGE", engOnly);
}
}
//
// Find a string with the same text.
//
const OString& StringTable::matchString(const OString& string) const
{
for (const auto& [first, second] : _stringHash)
{
if (second.string.first == false)
continue;
if (second.string.second == string)
return first;
}
static OString empty = "";
return empty;
}
//
// Set a string to something specific by name.
//
// Overrides the existing string, if it exists.
//
void StringTable::setString(const OString& name, const OString& string)
{
StringHash::iterator it = _stringHash.find(name);
if (it == _stringHash.end())
{
// Stringtable entry does not exist, insert it.
TableEntry entry = {std::make_pair(true, string), 0, -1};
_stringHash.emplace(name, entry);
}
else
{
// Stringtable entry exists, update it.
(*it).second.string.first = true;
(*it).second.string.second = string;
}
}
//
// Set a string to something specific by name.
//
// Does not set the string if it already exists.
//
void StringTable::setPassString(int pass, const OString& name, const OString& string)
{
StringHash::iterator it = _stringHash.find(name);
if (it == _stringHash.end())
{
// Stringtable entry does not exist.
TableEntry entry = {std::make_pair(true, string), pass, -1};
_stringHash.emplace(name, entry);
}
else
{
// Stringtable entry exists, but has not been set yet.
(*it).second.string.first = true;
(*it).second.string.second = string;
(*it).second.pass = pass;
}
}
//
// Number of entries in the stringtable.
//
size_t StringTable::size() const
{
return _stringHash.size();
}
VERSION_CONTROL(stringtable_cpp, "$Id$")