diff options
Diffstat (limited to 'primedev/scripts/scriptdatatables.cpp')
-rw-r--r-- | primedev/scripts/scriptdatatables.cpp | 909 |
1 files changed, 909 insertions, 0 deletions
diff --git a/primedev/scripts/scriptdatatables.cpp b/primedev/scripts/scriptdatatables.cpp new file mode 100644 index 00000000..87a26dca --- /dev/null +++ b/primedev/scripts/scriptdatatables.cpp @@ -0,0 +1,909 @@ +#include "squirrel/squirrel.h" +#include "core/filesystem/rpakfilesystem.h" +#include "core/convar/convar.h" +#include "dedicated/dedicated.h" +#include "core/filesystem/filesystem.h" +#include "core/math/vector.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include <iostream> +#include <sstream> +#include <map> +#include <fstream> +#include <filesystem> + +const uint64_t USERDATA_TYPE_DATATABLE = 0xFFF7FFF700000004; +const uint64_t USERDATA_TYPE_DATATABLE_CUSTOM = 0xFFFCFFFC12345678; + +enum class DatatableType : int +{ + BOOL = 0, + INT, + FLOAT, + VECTOR, + STRING, + ASSET, + UNK_STRING // unknown but deffo a string type +}; + +struct ColumnInfo +{ + char* name; + DatatableType type; + int offset; +}; + +struct Datatable +{ + int numColumns; + int numRows; + ColumnInfo* columnInfo; + char* data; // actually data pointer + int rowInfo; +}; + +ConVar* Cvar_ns_prefer_datatable_from_disk; + +template <ScriptContext context> Datatable* (*SQ_GetDatatableInternal)(HSquirrelVM* sqvm); + +struct CSVData +{ + std::string m_sAssetName; + std::string m_sCSVString; + char* m_pDataBuf; + size_t m_nDataBufSize; + + std::vector<char*> columns; + std::vector<std::vector<char*>> dataPointers; +}; + +std::unordered_map<std::string, CSVData> CSVCache; + +Vector3 StringToVector(char* pString) +{ + Vector3 vRet; + + int length = 0; + while (pString[length]) + { + if ((pString[length] == '<') || (pString[length] == '>')) + pString[length] = '\0'; + length++; + } + + int startOfFloat = 1; + int currentIndex = 1; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.x = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.y = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.z = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + return vRet; +} + +// var function GetDataTable( asset path ) +REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + const char* pAssetName; + g_pSquirrel<context>->getasset(sqvm, 2, &pAssetName); + + if (strncmp(pAssetName, "datatable/", 10)) + { + g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Asset \"{}\" doesn't start with \"datatable/\"", pAssetName).c_str()); + return SQRESULT_ERROR; + } + else if (!Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) + return g_pSquirrel<context>->m_funcOriginals["GetDataTable"](sqvm); + // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak + else + { + std::string sAssetPath(fmt::format("scripts/{}", pAssetName)); + + // first, check the cache + if (CSVCache.find(pAssetName) != CSVCache.end()) + { + CSVData** pUserdata = g_pSquirrel<context>->template createuserdata<CSVData*>(sqvm, sizeof(CSVData*)); + g_pSquirrel<context>->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); + *pUserdata = &CSVCache[pAssetName]; + + return SQRESULT_NOTNULL; + } + + // check files on disk + // we don't use .rpak as the extension for on-disk datatables, so we need to replace .rpak with .csv in the filename we're reading + fs::path diskAssetPath("scripts"); + if (fs::path(pAssetName).extension() == ".rpak") + diskAssetPath /= fs::path(pAssetName).remove_filename() / (fs::path(pAssetName).stem().string() + ".csv"); + else + diskAssetPath /= fs::path(pAssetName); + + std::string sDiskAssetPath(diskAssetPath.string()); + if ((*g_pFilesystem)->m_vtable2->FileExists(&(*g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME")) + { + std::string sTableCSV = ReadVPKFile(sDiskAssetPath.c_str()); + if (!sTableCSV.size()) + { + g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Datatable \"{}\" is empty", pAssetName).c_str()); + return SQRESULT_ERROR; + } + + // somewhat shit, but ensure we end with a newline to make parsing easier + if (sTableCSV[sTableCSV.length() - 1] != '\n') + sTableCSV += '\n'; + + CSVData csv; + csv.m_sAssetName = pAssetName; + csv.m_sCSVString = sTableCSV; + csv.m_nDataBufSize = sTableCSV.size(); + csv.m_pDataBuf = new char[csv.m_nDataBufSize]; + memcpy(csv.m_pDataBuf, &sTableCSV[0], csv.m_nDataBufSize); + + // parse the csv + // csvs are essentially comma and newline-deliniated sets of strings for parsing, only thing we need to worry about is quoted + // entries when we parse an element of the csv, rather than allocating an entry for it, we just convert that element to a + // null-terminated string i.e., store the ptr to the first char of it, then make the comma that delinates it a nullchar + + bool bHasColumns = false; + bool bInQuotes = false; + + std::vector<char*> vCurrentRow; + char* pElemStart = csv.m_pDataBuf; + char* pElemEnd = nullptr; + + for (int i = 0; i < csv.m_nDataBufSize; i++) + { + if (csv.m_pDataBuf[i] == '\r' && csv.m_pDataBuf[i + 1] == '\n') + { + if (!pElemEnd) + pElemEnd = csv.m_pDataBuf + i; + + continue; // next iteration can handle the \n + } + + // newline, end of a row + if (csv.m_pDataBuf[i] == '\n') + { + // shouldn't have newline in string + if (bInQuotes) + { + g_pSquirrel<context>->raiseerror(sqvm, "Unexpected \\n in string"); + return SQRESULT_ERROR; + } + + // push last entry to current row + if (pElemEnd) + *pElemEnd = '\0'; + else + csv.m_pDataBuf[i] = '\0'; + + vCurrentRow.push_back(pElemStart); + + // newline, push last line to csv data and go from there + if (!bHasColumns) + { + bHasColumns = true; + csv.columns = vCurrentRow; + } + else + csv.dataPointers.push_back(vCurrentRow); + + vCurrentRow.clear(); + // put start of current element at char after newline + pElemStart = csv.m_pDataBuf + i + 1; + pElemEnd = nullptr; + } + // we're starting or ending a quoted string + else if (csv.m_pDataBuf[i] == '"') + { + // start quoted string + if (!bInQuotes) + { + // shouldn't have quoted strings in column names + if (!bHasColumns) + { + g_pSquirrel<context>->raiseerror(sqvm, "Unexpected \" in column name"); + return SQRESULT_ERROR; + } + + bInQuotes = true; + // put start of current element at char after string begin + pElemStart = csv.m_pDataBuf + i + 1; + } + // end quoted string + else + { + pElemEnd = csv.m_pDataBuf + i; + bInQuotes = false; + } + } + // don't parse commas in quotes + else if (bInQuotes) + { + continue; + } + // comma, push new entry to current row + else if (csv.m_pDataBuf[i] == ',') + { + if (pElemEnd) + *pElemEnd = '\0'; + else + csv.m_pDataBuf[i] = '\0'; + + vCurrentRow.push_back(pElemStart); + // put start of next element at char after comma + pElemStart = csv.m_pDataBuf + i + 1; + pElemEnd = nullptr; + } + } + + // add to cache and return + CSVData** pUserdata = g_pSquirrel<context>->template createuserdata<CSVData*>(sqvm, sizeof(CSVData*)); + g_pSquirrel<context>->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); + CSVCache[pAssetName] = csv; + *pUserdata = &CSVCache[pAssetName]; + + return SQRESULT_NOTNULL; + } + // the file doesn't exist on disk, check rpak if we haven't already + else if (Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) + return g_pSquirrel<context>->m_funcOriginals["GetDataTable"](sqvm); + // the file doesn't exist at all, error + else + { + g_pSquirrel<context>->raiseerror(sqvm, fmt::format("Datatable {} not found", pAssetName).c_str()); + return SQRESULT_ERROR; + } + } +} + +// int function GetDataTableColumnByName( var datatable, string columnName ) +REPLACE_SQFUNC(GetDataTableColumnByName, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableColumnByName"](sqvm); + + CSVData* csv = *pData; + const char* pColumnName = g_pSquirrel<context>->getstring(sqvm, 2); + + for (int i = 0; i < csv->columns.size(); i++) + { + if (!strcmp(csv->columns[i], pColumnName)) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + // column not found + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowCount( var datatable ) +REPLACE_SQFUNC(GetDataTableRowCount, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDatatableRowCount"](sqvm); + + CSVData* csv = *pData; + g_pSquirrel<context>->pushinteger(sqvm, csv->dataPointers.size()); + return SQRESULT_NOTNULL; +} + +// string function GetDataTableString( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableString, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableString"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushstring(sqvm, csv->dataPointers[nRow][nCol], -1); + return SQRESULT_NOTNULL; +} + +// asset function GetDataTableAsset( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableAsset, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableAsset"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushasset(sqvm, csv->dataPointers[nRow][nCol], -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableInt( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableInt, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableInt"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushinteger(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// float function GetDataTableFloat( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableFloat, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableFloat"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushfloat(sqvm, std::stof(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// bool function GetDataTableBool( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableBool, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableBool"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushbool(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// vector function GetDataTableVector( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableVector, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableVector"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nCol = g_pSquirrel<context>->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel<context>->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel<context>->pushvector(sqvm, StringToVector(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingStringValue( var datatable, int col, string value ) +REPLACE_SQFUNC(GetDataTableRowMatchingStringValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingStringValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const char* pStringVal = g_pSquirrel<context>->getstring(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingAssetValue( var datatable, int col, asset value ) +REPLACE_SQFUNC(GetDataTableMatchingAssetValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingAssetValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const char* pStringVal; + g_pSquirrel<context>->getasset(sqvm, 3, &pStringVal); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowMatchingFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal == std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowMatchingIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal == std::stoi(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingVectorValue( var datatable, int col, vector value ) +REPLACE_SQFUNC(GetDataTableRowMatchingVectorValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowMatchingVectorValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const Vector3 vVectorVal = g_pSquirrel<context>->getvector(sqvm, 3); + + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (vVectorVal == StringToVector(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowGreaterThanOrEqualToIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal >= std::stoi(csv->dataPointers[i][nCol])) + { + spdlog::info("datatable not loaded"); + g_pSquirrel<context>->pushinteger(sqvm, 1); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowLessThanOrEqualToIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowLessThanOrEqualToIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel<context>->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal <= std::stoi(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowGreaterThanOrEqualToFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal >= std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowLessThanOrEqualToFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel<context>->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel<context>->m_funcOriginals["GetDataTableRowLessThanOrEqualToFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel<context>->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel<context>->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal <= std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel<context>->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +std::string DataTableToString(Datatable* datatable) +{ + std::string sCSVString; + + // write columns + bool bShouldComma = false; + for (int i = 0; i < datatable->numColumns; i++) + { + if (bShouldComma) + sCSVString += ','; + else + bShouldComma = true; + + sCSVString += datatable->columnInfo[i].name; + } + + // write rows + for (int row = 0; row < datatable->numRows; row++) + { + sCSVString += '\n'; + + bool bShouldComma = false; + for (int col = 0; col < datatable->numColumns; col++) + { + if (bShouldComma) + sCSVString += ','; + else + bShouldComma = true; + + // output typed data + ColumnInfo column = datatable->columnInfo[col]; + const void* pUntypedVal = datatable->data + column.offset + row * datatable->rowInfo; + switch (column.type) + { + case DatatableType::BOOL: + { + sCSVString += *(bool*)pUntypedVal ? '1' : '0'; + break; + } + + case DatatableType::INT: + { + sCSVString += std::to_string(*(int*)pUntypedVal); + break; + } + + case DatatableType::FLOAT: + { + sCSVString += std::to_string(*(float*)pUntypedVal); + break; + } + + case DatatableType::VECTOR: + { + Vector3* pVector = (Vector3*)(pUntypedVal); + sCSVString += fmt::format("<{},{},{}>", pVector->x, pVector->y, pVector->z); + break; + } + + case DatatableType::STRING: + case DatatableType::ASSET: + case DatatableType::UNK_STRING: + { + sCSVString += fmt::format("\"{}\"", *(char**)pUntypedVal); + break; + } + } + } + } + + return sCSVString; +} + +void DumpDatatable(const char* pDatatablePath) +{ + Datatable* pDatatable = (Datatable*)g_pPakLoadManager->LoadFile(pDatatablePath); + if (!pDatatable) + { + spdlog::error("couldn't load datatable {} (rpak containing it may not be loaded?)", pDatatablePath); + return; + } + + std::string sOutputPath(fmt::format("{}/scripts/datatable/{}.csv", g_pModName, fs::path(pDatatablePath).stem().string())); + std::string sDatatableContents(DataTableToString(pDatatable)); + + fs::create_directories(fs::path(sOutputPath).remove_filename()); + std::ofstream outputStream(sOutputPath); + outputStream.write(sDatatableContents.c_str(), sDatatableContents.size()); + outputStream.close(); + + spdlog::info("dumped datatable {} {} to {}", pDatatablePath, (void*)pDatatable, sOutputPath); +} + +void ConCommand_dump_datatable(const CCommand& args) +{ + if (args.ArgC() < 2) + { + spdlog::info("usage: dump_datatable datatable/tablename.rpak"); + return; + } + + DumpDatatable(args.Arg(1)); +} + +void ConCommand_dump_datatables(const CCommand& args) +{ + // likely not a comprehensive list, might be missing a couple? + static const std::vector<const char*> VANILLA_DATATABLE_PATHS = { + "datatable/burn_meter_rewards.rpak", + "datatable/burn_meter_store.rpak", + "datatable/calling_cards.rpak", + "datatable/callsign_icons.rpak", + "datatable/camo_skins.rpak", + "datatable/default_pilot_loadouts.rpak", + "datatable/default_titan_loadouts.rpak", + "datatable/faction_leaders.rpak", + "datatable/fd_awards.rpak", + "datatable/features_mp.rpak", + "datatable/non_loadout_weapons.rpak", + "datatable/pilot_abilities.rpak", + "datatable/pilot_executions.rpak", + "datatable/pilot_passives.rpak", + "datatable/pilot_properties.rpak", + "datatable/pilot_weapons.rpak", + "datatable/pilot_weapon_features.rpak", + "datatable/pilot_weapon_mods.rpak", + "datatable/pilot_weapon_mods_common.rpak", + "datatable/playlist_items.rpak", + "datatable/titans_mp.rpak", + "datatable/titan_abilities.rpak", + "datatable/titan_executions.rpak", + "datatable/titan_fd_upgrades.rpak", + "datatable/titan_nose_art.rpak", + "datatable/titan_passives.rpak", + "datatable/titan_primary_mods.rpak", + "datatable/titan_primary_mods_common.rpak", + "datatable/titan_primary_weapons.rpak", + "datatable/titan_properties.rpak", + "datatable/titan_skins.rpak", + "datatable/titan_voices.rpak", + "datatable/unlocks_faction_level.rpak", + "datatable/unlocks_fd_titan_level.rpak", + "datatable/unlocks_player_level.rpak", + "datatable/unlocks_random.rpak", + "datatable/unlocks_titan_level.rpak", + "datatable/unlocks_weapon_level_pilot.rpak", + "datatable/weapon_skins.rpak", + "datatable/xp_per_faction_level.rpak", + "datatable/xp_per_fd_titan_level.rpak", + "datatable/xp_per_player_level.rpak", + "datatable/xp_per_titan_level.rpak", + "datatable/xp_per_weapon_level.rpak", + "datatable/faction_leaders_dropship_anims.rpak", + "datatable/score_events.rpak", + "datatable/startpoints.rpak", + "datatable/sp_levels.rpak", + "datatable/community_entries.rpak", + "datatable/spotlight_images.rpak", + "datatable/death_hints_mp.rpak", + "datatable/flightpath_assets.rpak", + "datatable/earn_meter_mp.rpak", + "datatable/battle_chatter_voices.rpak", + "datatable/battle_chatter.rpak", + "datatable/titan_os_conversations.rpak", + "datatable/faction_dialogue.rpak", + "datatable/grunt_chatter_mp.rpak", + "datatable/spectre_chatter_mp.rpak", + "datatable/pain_death_sounds.rpak", + "datatable/caller_ids_mp.rpak"}; + + for (const char* datatable : VANILLA_DATATABLE_PATHS) + DumpDatatable(datatable); +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerScriptDatatables, ServerSquirrel, (CModule module)) +{ + SQ_GetDatatableInternal<ScriptContext::SERVER> = module.Offset(0x1250f0).RCast<Datatable* (*)(HSquirrelVM*)>(); +} + +ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module)) +{ + SQ_GetDatatableInternal<ScriptContext::CLIENT> = module.Offset(0x1C9070).RCast<Datatable* (*)(HSquirrelVM*)>(); + SQ_GetDatatableInternal<ScriptContext::UI> = SQ_GetDatatableInternal<ScriptContext::CLIENT>; +} + +ON_DLL_LOAD_RELIESON("engine.dll", SharedScriptDataTables, ConVar, (CModule module)) +{ + Cvar_ns_prefer_datatable_from_disk = new ConVar( + "ns_prefer_datatable_from_disk", + IsDedicatedServer() && CommandLine()->CheckParm("-nopakdedi") ? "1" : "0", + FCVAR_NONE, + "whether to prefer loading datatables from disk, rather than rpak"); + + RegisterConCommand("dump_datatables", ConCommand_dump_datatables, "dumps all datatables from a hardcoded list", FCVAR_NONE); + RegisterConCommand("dump_datatable", ConCommand_dump_datatable, "dump a datatable", FCVAR_NONE); +} |