910 lines
28 KiB
C++
910 lines
28 KiB
C++
#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, (SQInteger)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);
|
|
}
|