Structureddata stuff. Not the best implementation, but better than our old one :P
This commit is contained in:
parent
b4be34c8a8
commit
c1ef716e5c
@ -13,8 +13,8 @@ namespace Components
|
||||
virtual void Load(Game::XAssetHeader* header, std::string name, ZoneBuilder::Zone* builder) { /*ErrorTypeNotSupported(this);*/ };
|
||||
};
|
||||
|
||||
typedef Game::XAssetHeader(*Callback)(Game::XAssetType, const char*);
|
||||
typedef bool(*RestrictCallback)(Game::XAssetType type, Game::XAssetHeader asset, const char* name);
|
||||
typedef Game::XAssetHeader(*Callback)(Game::XAssetType type, std::string name);
|
||||
typedef bool(*RestrictCallback)(Game::XAssetType type, Game::XAssetHeader asset, std::string name);
|
||||
|
||||
AssetHandler();
|
||||
~AssetHandler();
|
||||
|
@ -10,6 +10,11 @@ namespace Components
|
||||
std::vector<Game::XZoneInfo> data;
|
||||
Utils::Merge(&data, zoneInfo, zoneCount);
|
||||
|
||||
if (FastFiles::Exists("patch_iw4x"))
|
||||
{
|
||||
data.push_back({ "patch_iw4x", 1, 0 });
|
||||
}
|
||||
|
||||
// Load custom weapons, if present (force that later on)
|
||||
if (FastFiles::Exists("weapons_mp"))
|
||||
{
|
||||
|
@ -116,7 +116,7 @@ namespace Components
|
||||
|
||||
Localization::Localization()
|
||||
{
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_LOCALIZE, [] (Game::XAssetType, const char* filename)
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_LOCALIZE, [] (Game::XAssetType, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
|
@ -50,7 +50,7 @@ namespace Components
|
||||
return FastFiles::LoadLocalizeZones(data.data(), data.size(), sync);
|
||||
}
|
||||
|
||||
bool Maps::LoadAssetRestrict(Game::XAssetType type, Game::XAssetHeader asset, const char* name)
|
||||
bool Maps::LoadAssetRestrict(Game::XAssetType type, Game::XAssetHeader asset, std::string name)
|
||||
{
|
||||
if (std::find(Maps::CurrentDependencies.begin(), Maps::CurrentDependencies.end(), FastFiles::Current()) != Maps::CurrentDependencies.end())
|
||||
{
|
||||
@ -63,7 +63,7 @@ namespace Components
|
||||
if (type == Game::XAssetType::ASSET_TYPE_MAP_ENTS)
|
||||
{
|
||||
static std::string mapEntities;
|
||||
FileSystem::File ents(Utils::VA("%s.ents", name));
|
||||
FileSystem::File ents(name + ".ents");
|
||||
if (ents.Exists())
|
||||
{
|
||||
mapEntities = ents.GetBuffer();
|
||||
|
@ -19,7 +19,7 @@ namespace Components
|
||||
static std::vector<std::string> CurrentDependencies;
|
||||
|
||||
static void GetBSPName(char* buffer, size_t size, const char* format, const char* mapname);
|
||||
static bool LoadAssetRestrict(Game::XAssetType type, Game::XAssetHeader asset, const char* name);
|
||||
static bool LoadAssetRestrict(Game::XAssetType type, Game::XAssetHeader asset, std::string name);
|
||||
static void LoadMapZones(Game::XZoneInfo *zoneInfo, unsigned int zoneCount, int sync);
|
||||
|
||||
void ReallocateEntryPool();
|
||||
|
@ -506,16 +506,16 @@ namespace Components
|
||||
Menus::MenuList.clear();
|
||||
}
|
||||
|
||||
Game::XAssetHeader Menus::MenuLoad(Game::XAssetType type, const char* filename)
|
||||
Game::XAssetHeader Menus::MenuLoad(Game::XAssetType type, std::string filename)
|
||||
{
|
||||
return { Game::Menus_FindByName(Game::uiContext, filename) };
|
||||
return { Game::Menus_FindByName(Game::uiContext, filename.data()) };
|
||||
}
|
||||
|
||||
Game::XAssetHeader Menus::MenuFileLoad(Game::XAssetType type, const char* filename)
|
||||
Game::XAssetHeader Menus::MenuFileLoad(Game::XAssetType type, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
Game::MenuList* menuList = Game::DB_FindXAssetHeader(type, filename).menuList;
|
||||
Game::MenuList* menuList = Game::DB_FindXAssetHeader(type, filename.data()).menuList;
|
||||
header.menuList = menuList;
|
||||
|
||||
// Free the last menulist and ui context, as we have to rebuild it with the new menus
|
||||
@ -538,7 +538,7 @@ namespace Components
|
||||
{
|
||||
if (FileSystem::File(filename).Exists())
|
||||
{
|
||||
header.menuList = Menus::LoadScriptMenu(filename);
|
||||
header.menuList = Menus::LoadScriptMenu(filename.data());
|
||||
|
||||
// Reset, if we didn't find scriptmenus
|
||||
if (!header.menuList)
|
||||
|
@ -19,8 +19,8 @@ namespace Components
|
||||
static std::map<std::string, Game::MenuList*> MenuListList;
|
||||
static std::vector<std::string> CustomMenus;
|
||||
|
||||
static Game::XAssetHeader MenuLoad(Game::XAssetType type, const char* filename);
|
||||
static Game::XAssetHeader MenuFileLoad(Game::XAssetType type, const char* filename);
|
||||
static Game::XAssetHeader MenuLoad(Game::XAssetType type, std::string filename);
|
||||
static Game::XAssetHeader MenuFileLoad(Game::XAssetType type, std::string filename);
|
||||
|
||||
static Game::MenuList* LoadMenuList(Game::MenuList* menuList);
|
||||
static Game::MenuList* LoadScriptMenu(const char* menu);
|
||||
|
@ -9,13 +9,13 @@ namespace Components
|
||||
MusicalTalent::SoundAliasList[Utils::StrToLower(sound)] = file;
|
||||
}
|
||||
|
||||
Game::XAssetHeader MusicalTalent::ManipulateAliases(Game::XAssetType type, const char* filename)
|
||||
Game::XAssetHeader MusicalTalent::ModifyAliases(Game::XAssetType type, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
if (MusicalTalent::SoundAliasList.find(Utils::StrToLower(filename)) != MusicalTalent::SoundAliasList.end())
|
||||
{
|
||||
Game::snd_alias_list_t* aliases = Game::DB_FindXAssetHeader(type, filename).aliasList;
|
||||
Game::snd_alias_list_t* aliases = Game::DB_FindXAssetHeader(type, filename.data()).aliasList;
|
||||
|
||||
if (aliases)
|
||||
{
|
||||
@ -33,7 +33,7 @@ namespace Components
|
||||
|
||||
MusicalTalent::MusicalTalent()
|
||||
{
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_SOUND, MusicalTalent::ManipulateAliases);
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_SOUND, MusicalTalent::ModifyAliases);
|
||||
|
||||
MusicalTalent::Replace("music_mainmenu_mp", "hz_boneyard_intro_LR_1.mp3");
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ namespace Components
|
||||
|
||||
private:
|
||||
static std::map<std::string, const char*> SoundAliasList;
|
||||
static Game::XAssetHeader ManipulateAliases(Game::XAssetType type, const char* filename);
|
||||
static Game::XAssetHeader ModifyAliases(Game::XAssetType type, std::string filename);
|
||||
};
|
||||
}
|
||||
|
@ -18,14 +18,14 @@ namespace Components
|
||||
return hash;
|
||||
}
|
||||
|
||||
Game::StringTable* StringTable::LoadObject(const char* filename)
|
||||
Game::StringTable* StringTable::LoadObject(std::string filename)
|
||||
{
|
||||
Game::StringTable* table = nullptr;
|
||||
FileSystem::File rawTable(filename);
|
||||
|
||||
if (rawTable.Exists())
|
||||
{
|
||||
Utils::CSV parsedTable(rawTable.GetBuffer(), false);
|
||||
Utils::CSV parsedTable(rawTable.GetBuffer(), false, false);
|
||||
|
||||
table = Utils::Memory::AllocateArray<Game::StringTable>(1);
|
||||
|
||||
@ -67,10 +67,7 @@ namespace Components
|
||||
|
||||
StringTable::StringTable()
|
||||
{
|
||||
// Disable StringTable loading until our StructuredData handler is finished!
|
||||
#ifdef ENABLE_STRINGTABLES
|
||||
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_STRINGTABLE, [] (Game::XAssetType, const char* filename)
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_STRINGTABLE, [] (Game::XAssetType, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
@ -85,7 +82,6 @@ namespace Components
|
||||
|
||||
return header;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
StringTable::~StringTable()
|
||||
|
@ -11,6 +11,6 @@ namespace Components
|
||||
static std::map<std::string, Game::StringTable*> StringTableMap;
|
||||
|
||||
static int Hash(const char* data);
|
||||
static Game::StringTable* LoadObject(const char* filename);
|
||||
static Game::StringTable* LoadObject(std::string filename);
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,92 @@
|
||||
|
||||
namespace Components
|
||||
{
|
||||
StructuredData* StructuredData::Singleton = nullptr;
|
||||
|
||||
int StructuredData::IndexCount[StructuredData::ENUM_MAX];
|
||||
Game::structuredDataEnumIndex_t* StructuredData::Indices[StructuredData::ENUM_MAX];
|
||||
std::vector<StructuredData::EnumEntry> StructuredData::Entries[StructuredData::ENUM_MAX];
|
||||
|
||||
void StructuredData::AddPlayerDataEntry(StructuredData::PlayerDataType type, int index, std::string name)
|
||||
{
|
||||
if (type >= StructuredData::ENUM_MAX) return;
|
||||
|
||||
// Check for duplications
|
||||
for (auto entry : StructuredData::Entries[type])
|
||||
{
|
||||
if (entry.name == name || entry.statOffset == index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StructuredData::Entries[type].push_back({ name, index });
|
||||
}
|
||||
|
||||
void StructuredData::PatchPlayerDataEnum(Game::structuredDataDef_t* data, StructuredData::PlayerDataType type, std::vector<StructuredData::EnumEntry>& entries)
|
||||
{
|
||||
if (!data || !data->data) return;
|
||||
|
||||
Game::structuredDataEnum_t* dataEnum = &data->data->enums[type];
|
||||
|
||||
if (StructuredData::IndexCount[type])
|
||||
{
|
||||
dataEnum->numIndices = StructuredData::IndexCount[type];
|
||||
dataEnum->indices = StructuredData::Indices[type];
|
||||
return;
|
||||
}
|
||||
|
||||
// Find last index so we can add our offset to it.
|
||||
// This whole procedure is potentially unsafe.
|
||||
// If any index changes, everything gets shifted and the stats are fucked.
|
||||
int lastIndex = 0;
|
||||
for (int i = 0; i < dataEnum->numIndices; i++)
|
||||
{
|
||||
if (dataEnum->indices[i].index > lastIndex)
|
||||
{
|
||||
lastIndex = dataEnum->indices[i].index;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate new count
|
||||
StructuredData::IndexCount[type] = dataEnum->numIndices + entries.size();
|
||||
|
||||
// Allocate new entries
|
||||
StructuredData::Indices[type] = StructuredData::GetSingleton()->MemAllocator.AllocateArray<Game::structuredDataEnumIndex_t>(StructuredData::IndexCount[type]);
|
||||
memcpy(StructuredData::Indices[type], dataEnum->indices, sizeof(Game::structuredDataEnumIndex_t) * dataEnum->numIndices);
|
||||
|
||||
for (unsigned int i = 0; i < entries.size(); i++)
|
||||
{
|
||||
unsigned int pos = 0;
|
||||
|
||||
for (; pos < (dataEnum->numIndices + i); pos++)
|
||||
{
|
||||
if (StructuredData::Indices[type][pos].key == entries[i].name)
|
||||
{
|
||||
Logger::Error("Duplicate playerdatadef entry found: %s", entries[i].name.data());
|
||||
}
|
||||
|
||||
// We found our position
|
||||
if (entries[i].name < StructuredData::Indices[type][pos].key)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int j = dataEnum->numIndices + i; j > pos; j--)
|
||||
{
|
||||
StructuredData::Indices[type][j] = StructuredData::Indices[type][j - 1];
|
||||
}
|
||||
|
||||
StructuredData::Indices[type][pos].index = entries[i].statOffset + lastIndex;
|
||||
StructuredData::Indices[type][pos].key = StructuredData::GetSingleton()->MemAllocator.DuplicateString(entries[i].name);
|
||||
}
|
||||
|
||||
// Apply our patches
|
||||
dataEnum->numIndices = StructuredData::IndexCount[type];
|
||||
dataEnum->indices = StructuredData::Indices[type];
|
||||
}
|
||||
|
||||
void StructuredData::DumpDataDef(Game::structuredDataDef_t* dataDef)
|
||||
{
|
||||
if (!dataDef || !dataDef->data) return;
|
||||
@ -16,17 +102,72 @@ namespace Components
|
||||
Utils::WriteFile(Utils::VA("raw/%s.json", dataDef->name), definition.dump());
|
||||
}
|
||||
|
||||
StructuredData* StructuredData::GetSingleton()
|
||||
{
|
||||
if (!StructuredData::Singleton)
|
||||
{
|
||||
Logger::Error("StructuredData singleton is null!");
|
||||
}
|
||||
|
||||
return StructuredData::Singleton;
|
||||
}
|
||||
|
||||
StructuredData::StructuredData()
|
||||
{
|
||||
StructuredData::Singleton = this;
|
||||
ZeroMemory(StructuredData::IndexCount, sizeof(StructuredData));
|
||||
|
||||
AssetHandler::OnFind(Game::XAssetType::ASSET_TYPE_STRUCTUREDDATADEF, [] (Game::XAssetType type, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
if (filename == "mp/playerdata.def")
|
||||
{
|
||||
Game::structuredDataDef_t* data = AssetHandler::FindOriginalAsset(Game::XAssetType::ASSET_TYPE_STRUCTUREDDATADEF, filename.data()).structuredData;
|
||||
header.structuredData = data;
|
||||
|
||||
if (data)
|
||||
{
|
||||
for (int i = 0; i < ARR_SIZE(StructuredData::Entries); i++)
|
||||
{
|
||||
if (StructuredData::Entries[i].size())
|
||||
{
|
||||
StructuredData::PatchPlayerDataEnum(data, static_cast<StructuredData::PlayerDataType>(i), StructuredData::Entries[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return header;
|
||||
});
|
||||
|
||||
Command::Add("dumpDataDef", [] (Command::Params params)
|
||||
{
|
||||
if (params.Length() < 2) return;
|
||||
StructuredData::DumpDataDef(Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_STRUCTUREDDATADEF, params[1]).structuredData);
|
||||
});
|
||||
|
||||
// ---------- Weapons ----------
|
||||
StructuredData::AddPlayerDataEntry(StructuredData::ENUM_WEAPONS, 1, "m40a3");
|
||||
StructuredData::AddPlayerDataEntry(StructuredData::ENUM_WEAPONS, 2, "ak47classic");
|
||||
//StructuredData::AddPlayerDataEntry(StructuredData::ENUM_WEAPONS, 3, "ak74u");
|
||||
//StructuredData::AddPlayerDataEntry(StructuredData::ENUM_WEAPONS, 4, "peacekeeper");
|
||||
|
||||
// ---------- Cardicons ----------
|
||||
StructuredData::AddPlayerDataEntry(StructuredData::ENUM_CARDICONS, 1, "cardicon_rtrolling");
|
||||
|
||||
// ---------- Cardtitles ----------
|
||||
StructuredData::AddPlayerDataEntry(StructuredData::ENUM_CARDTITLES, 1, "cardtitle_evilchicken");
|
||||
StructuredData::AddPlayerDataEntry(StructuredData::ENUM_CARDTITLES, 2, "cardtitle_nolaststand");
|
||||
}
|
||||
|
||||
StructuredData::~StructuredData()
|
||||
{
|
||||
for (int i = 0; i < ARR_SIZE(StructuredData::Entries); i++)
|
||||
{
|
||||
StructuredData::Entries[i].clear();
|
||||
}
|
||||
|
||||
StructuredData::Singleton = nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,47 @@ namespace Components
|
||||
class StructuredData : public Component
|
||||
{
|
||||
public:
|
||||
enum PlayerDataType
|
||||
{
|
||||
ENUM_FEATURES,
|
||||
ENUM_WEAPONS,
|
||||
ENUM_ATTACHEMENTS,
|
||||
ENUM_CHALLENGES,
|
||||
ENUM_CAMOS,
|
||||
ENUM_PERKS,
|
||||
ENUM_KILLSTREAKS,
|
||||
ENUM_ACCOLADES,
|
||||
ENUM_CARDICONS,
|
||||
ENUM_CARDTITLES,
|
||||
ENUM_CARDNAMEPLATES,
|
||||
ENUM_TEAMS,
|
||||
ENUM_GAMETYPES,
|
||||
ENUM_MAX
|
||||
};
|
||||
|
||||
StructuredData();
|
||||
~StructuredData();
|
||||
const char* GetName() { return "StructuredData"; };
|
||||
|
||||
void AddPlayerDataEntry(PlayerDataType type, int index, std::string name);
|
||||
|
||||
private:
|
||||
struct EnumEntry
|
||||
{
|
||||
std::string name;
|
||||
int statOffset;
|
||||
};
|
||||
|
||||
static void DumpDataDef(Game::structuredDataDef_t* dataDef);
|
||||
static void PatchPlayerDataEnum(Game::structuredDataDef_t* data, PlayerDataType type, std::vector<EnumEntry>& entries);
|
||||
static StructuredData* GetSingleton();
|
||||
|
||||
Utils::Memory::Allocator MemAllocator;
|
||||
|
||||
static int IndexCount[ENUM_MAX];
|
||||
static Game::structuredDataEnumIndex_t* Indices[ENUM_MAX];
|
||||
static std::vector<EnumEntry> Entries[ENUM_MAX];
|
||||
|
||||
static StructuredData* Singleton;
|
||||
};
|
||||
}
|
||||
|
@ -2,14 +2,14 @@
|
||||
|
||||
namespace Components
|
||||
{
|
||||
Game::XAssetHeader Weapon::WeaponFileLoad(Game::XAssetType type, const char* filename)
|
||||
Game::XAssetHeader Weapon::WeaponFileLoad(Game::XAssetType type, std::string filename)
|
||||
{
|
||||
Game::XAssetHeader header = { 0 };
|
||||
|
||||
// Try loading raw weapon
|
||||
if (FileSystem::File(Utils::VA("weapons/mp/%s", filename)).Exists())
|
||||
if (FileSystem::File(Utils::VA("weapons/mp/%s", filename.data())).Exists())
|
||||
{
|
||||
header.data = Game::BG_LoadWeaponDef_LoadObj(filename);
|
||||
header.data = Game::BG_LoadWeaponDef_LoadObj(filename.data());
|
||||
}
|
||||
|
||||
return header;
|
||||
|
@ -7,6 +7,6 @@ namespace Components
|
||||
const char* GetName() { return "Weapon"; };
|
||||
|
||||
private:
|
||||
static Game::XAssetHeader WeaponFileLoad(Game::XAssetType type, const char* filename);
|
||||
static Game::XAssetHeader WeaponFileLoad(Game::XAssetType type, std::string filename);
|
||||
};
|
||||
}
|
||||
|
@ -447,7 +447,7 @@ namespace Components
|
||||
static_assert(sizeof(Game::XFile) == 40, "Invalid XFile structure!");
|
||||
static_assert(Game::MAX_XFILE_COUNT == 8, "XFile block enum is invalid!");
|
||||
|
||||
AssetHandler::OnLoad([] (Game::XAssetType type, Game::XAssetHeader asset, const char* name)
|
||||
AssetHandler::OnLoad([] (Game::XAssetType type, Game::XAssetHeader asset, std::string name)
|
||||
{
|
||||
// static void* blocTable = 0;
|
||||
//
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
CSV::CSV(std::string file, bool isFile)
|
||||
CSV::CSV(std::string file, bool isFile, bool allowComments)
|
||||
{
|
||||
CSV::Parse(file, isFile);
|
||||
CSV::Parse(file, isFile, allowComments);
|
||||
}
|
||||
|
||||
CSV::~CSV()
|
||||
@ -64,7 +64,7 @@ namespace Utils
|
||||
return "";
|
||||
}
|
||||
|
||||
void CSV::Parse(std::string file, bool isFile)
|
||||
void CSV::Parse(std::string file, bool isFile, bool allowComments)
|
||||
{
|
||||
std::string buffer;
|
||||
|
||||
@ -84,12 +84,12 @@ namespace Utils
|
||||
|
||||
for (auto row : rows)
|
||||
{
|
||||
CSV::ParseRow(row);
|
||||
CSV::ParseRow(row, allowComments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CSV::ParseRow(std::string row)
|
||||
void CSV::ParseRow(std::string row, bool allowComments)
|
||||
{
|
||||
bool isString = false;
|
||||
std::string element;
|
||||
@ -119,7 +119,7 @@ namespace Utils
|
||||
//++i;
|
||||
continue;
|
||||
}
|
||||
else if (!isString && row[i] == '#') // Skip comments. I know CSVs usually don't have comments, but in this case it's useful
|
||||
else if (!isString && row[i] == '#' && allowComments) // Skip comments. I know CSVs usually don't have comments, but in this case it's useful
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ namespace Utils
|
||||
class CSV
|
||||
{
|
||||
public:
|
||||
CSV(std::string file, bool isFile = true);
|
||||
CSV(std::string file, bool isFile = true, bool allowComments = true);
|
||||
~CSV();
|
||||
|
||||
int GetRows();
|
||||
@ -14,8 +14,8 @@ namespace Utils
|
||||
|
||||
private:
|
||||
|
||||
void Parse(std::string file, bool isFile = true);
|
||||
void ParseRow(std::string row);
|
||||
void Parse(std::string file, bool isFile = true, bool allowComments = true);
|
||||
void ParseRow(std::string row, bool allowComments = true);
|
||||
std::vector<std::vector<std::string>> DataMap;
|
||||
};
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ namespace Utils
|
||||
return input;
|
||||
}
|
||||
|
||||
bool EndsWith(const char* haystack, const char* needle)
|
||||
bool EndsWith(std::string haystack, std::string needle)
|
||||
{
|
||||
return (strstr(haystack, needle) == (haystack + strlen(haystack) - strlen(needle)));
|
||||
return (strstr(haystack.data(), needle.data()) == (haystack.data() + haystack.size() - needle.size()));
|
||||
}
|
||||
|
||||
std::vector<std::string> Explode(const std::string& str, char delim)
|
||||
|
@ -4,7 +4,7 @@ namespace Utils
|
||||
{
|
||||
const char *VA(const char *fmt, ...);
|
||||
std::string StrToLower(std::string input);
|
||||
bool EndsWith(const char* haystack, const char* needle);
|
||||
bool EndsWith(std::string haystack, std::string needle);
|
||||
std::vector<std::string> Explode(const std::string& str, char delim);
|
||||
void Replace(std::string &string, std::string find, std::string replace);
|
||||
bool StartsWith(std::string haystack, std::string needle);
|
||||
|
Loading…
Reference in New Issue
Block a user