aboutsummaryrefslogtreecommitdiff
path: root/primedev/scripts/scriptdatatables.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'primedev/scripts/scriptdatatables.cpp')
-rw-r--r--primedev/scripts/scriptdatatables.cpp909
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);
+}