#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 #include #include #include #include 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 Datatable* (*SQ_GetDatatableInternal)(HSquirrelVM* sqvm); struct CSVData { std::string m_sAssetName; std::string m_sCSVString; char* m_pDataBuf; size_t m_nDataBufSize; std::vector columns; std::vector> dataPointers; }; std::unordered_map CSVCache; // var function GetDataTable( asset path ) REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) { const char* pAssetName; g_pSquirrel->getasset(sqvm, 2, &pAssetName); if (strncmp(pAssetName, "datatable/", 10)) { g_pSquirrel->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->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->template createuserdata(sqvm, sizeof(CSVData*)); g_pSquirrel->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->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 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->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->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->template createuserdata(sqvm, sizeof(CSVData*)); g_pSquirrel->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->m_funcOriginals["GetDataTable"](sqvm); // the file doesn't exist at all, error else { g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableColumnByName"](sqvm); CSVData* csv = *pData; const char* pColumnName = g_pSquirrel->getstring(sqvm, 2); for (int i = 0; i < csv->columns.size(); i++) { if (!strcmp(csv->columns[i], pColumnName)) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } // column not found g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDatatableRowCount"](sqvm); CSVData* csv = *pData; g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableString"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableAsset"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableInt"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableFloat"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableBool"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableVector"](sqvm); CSVData* csv = *pData; const int nRow = g_pSquirrel->getinteger(sqvm, 2); const int nCol = g_pSquirrel->getinteger(sqvm, 3); if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) { g_pSquirrel->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->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingStringValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const char* pStringVal = g_pSquirrel->getstring(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingAssetValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const char* pStringVal; g_pSquirrel->getasset(sqvm, 3, &pStringVal); for (int i = 0; i < csv->dataPointers.size(); i++) { if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingFloatValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (flFloatVal == std::stof(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingIntValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (nIntVal == std::stoi(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingVectorValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const Vector3 vVectorVal = g_pSquirrel->getvector(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (vVectorVal == StringToVector(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToIntValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const int nIntVal = g_pSquirrel->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->pushinteger(sqvm, 1); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToIntValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (nIntVal <= std::stoi(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToFloatValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (flFloatVal >= std::stof(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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->getuserdata(sqvm, 2, &pData, &typeId); if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToFloatValue"](sqvm); CSVData* csv = *pData; int nCol = g_pSquirrel->getinteger(sqvm, 2); const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); for (int i = 0; i < csv->dataPointers.size(); i++) { if (flFloatVal <= std::stof(csv->dataPointers[i][nCol])) { g_pSquirrel->pushinteger(sqvm, i); return SQRESULT_NOTNULL; } } g_pSquirrel->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) { NOTE_UNUSED(args); // likely not a comprehensive list, might be missing a couple? static const std::vector 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 = module.Offset(0x1250f0).RCast(); } ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module)) { SQ_GetDatatableInternal = module.Offset(0x1C9070).RCast(); SQ_GetDatatableInternal = SQ_GetDatatableInternal; } 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); }