diff --git a/codxe.vcxproj b/codxe.vcxproj
index 0c208ef1..f2437208 100644
--- a/codxe.vcxproj
+++ b/codxe.vcxproj
@@ -87,6 +87,7 @@
+
@@ -102,7 +103,6 @@
-
@@ -201,6 +201,8 @@
+
+
@@ -214,6 +216,7 @@
+
@@ -224,7 +227,6 @@
-
diff --git a/src/game/iw3/mp/components/assets.cpp b/src/game/iw3/mp/components/assets.cpp
new file mode 100644
index 00000000..5077f43d
--- /dev/null
+++ b/src/game/iw3/mp/components/assets.cpp
@@ -0,0 +1,877 @@
+#include
+
+#include "pch.h"
+#include "assets.h"
+#include "third_party/aria_csv/csv_parser.hpp"
+
+namespace iw3
+{
+namespace mp
+{
+
+namespace
+{
+Detour DB_LinkXAssetEntry_Detour;
+
+const size_t MAX_OVERRIDE_CACHE_ENTRIES = 512;
+const size_t STRINGTABLE_VALUE_ALIGNMENT = sizeof(const char *);
+
+enum OverrideCacheState
+{
+ CACHE_EMPTY,
+ CACHE_LOADED
+};
+
+struct OverrideCacheEntry
+{
+ OverrideCacheState state;
+ XAssetType type;
+ char name[MAX_PATH];
+ void *storage;
+ DWORD storage_size;
+ DWORD storage_process_type;
+ DWORD payload_size;
+ StringTable stringtable;
+};
+
+OverrideCacheEntry *s_override_cache = nullptr;
+size_t s_override_cache_capacity = 0;
+
+size_t AlignSize(size_t value, size_t alignment)
+{
+ return (value + alignment - 1) & ~(alignment - 1);
+}
+
+void *AllocVirtualBlock(size_t size, const char *reason)
+{
+ void *storage = VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
+ UNREFERENCED_PARAMETER(reason);
+ return storage;
+}
+
+bool FreeVirtualBlock(void *storage, DWORD size, const char *reason)
+{
+ if (storage == nullptr)
+ {
+ return true;
+ }
+
+ const BOOL result = VirtualFree(storage, 0, MEM_RELEASE);
+ UNREFERENCED_PARAMETER(size);
+ UNREFERENCED_PARAMETER(reason);
+ return result != FALSE;
+}
+
+void ResetCacheEntry(OverrideCacheEntry &entry)
+{
+ if (entry.storage != nullptr)
+ {
+ const DWORD current_process_type = KeGetCurrentProcessType();
+ if (entry.storage_process_type == 0 || entry.storage_process_type == current_process_type)
+ {
+ FreeVirtualBlock(entry.storage, entry.storage_size, entry.name);
+ }
+ }
+
+ memset(&entry, 0, sizeof(entry));
+}
+
+void ClearOverrideCache()
+{
+ if (s_override_cache == nullptr)
+ {
+ return;
+ }
+
+ for (size_t i = 0; i < s_override_cache_capacity; ++i)
+ {
+ ResetCacheEntry(s_override_cache[i]);
+ }
+}
+
+bool InitializeOverrideCache()
+{
+ if (s_override_cache != nullptr)
+ {
+ ClearOverrideCache();
+ return true;
+ }
+
+ const size_t cache_size = sizeof(OverrideCacheEntry) * MAX_OVERRIDE_CACHE_ENTRIES;
+ s_override_cache = static_cast(AllocVirtualBlock(cache_size, "override cache metadata"));
+ if (s_override_cache == nullptr)
+ {
+ Com_Printf(CON_CHANNEL_FILES,
+ "[codxe][assets] Failed to allocate override cache. entries=%u bytes=%u error=0x%08X\n",
+ static_cast(MAX_OVERRIDE_CACHE_ENTRIES), static_cast(cache_size),
+ GetLastError());
+ s_override_cache_capacity = 0;
+ return false;
+ }
+
+ s_override_cache_capacity = MAX_OVERRIDE_CACHE_ENTRIES;
+ memset(s_override_cache, 0, cache_size);
+ return true;
+}
+
+void ShutdownOverrideCache()
+{
+ const DWORD cache_bytes = static_cast(sizeof(OverrideCacheEntry) * s_override_cache_capacity);
+ ClearOverrideCache();
+
+ if (s_override_cache != nullptr)
+ {
+ if (!FreeVirtualBlock(s_override_cache, cache_bytes, "override cache metadata"))
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] Failed to free override cache metadata. bytes=%u\n",
+ cache_bytes);
+ }
+ }
+
+ s_override_cache = nullptr;
+ s_override_cache_capacity = 0;
+}
+
+bool CopyAssetName(char *dest, size_t dest_size, const char *name)
+{
+ if (dest == nullptr || dest_size == 0 || name == nullptr || name[0] == '\0')
+ {
+ return false;
+ }
+
+ size_t i = 0;
+ for (; name[i] != '\0'; ++i)
+ {
+ if (i + 1 >= dest_size)
+ {
+ dest[0] = '\0';
+ return false;
+ }
+
+ dest[i] = name[i];
+ }
+
+ dest[i] = '\0';
+ return true;
+}
+
+bool AppendPathPart(char *path, size_t path_size, const char *text, bool normalize_slashes)
+{
+ if (path == nullptr || path_size == 0 || text == nullptr)
+ {
+ return false;
+ }
+
+ size_t length = strlen(path);
+ for (size_t i = 0; text[i] != '\0'; ++i)
+ {
+ if (length + 1 >= path_size)
+ {
+ return false;
+ }
+
+ char c = text[i];
+ if (normalize_slashes && c == '/')
+ {
+ c = '\\';
+ }
+
+ path[length++] = c;
+ }
+
+ path[length] = '\0';
+ return true;
+}
+
+bool BuildAssetPath(char *path, size_t path_size, const char *asset_name, const char *extension = nullptr)
+{
+ if (path == nullptr || path_size == 0)
+ {
+ return false;
+ }
+
+ path[0] = '\0';
+
+ const char *base_path = Config::GetModBasePathCStr();
+ if (base_path == nullptr || base_path[0] == '\0' || asset_name == nullptr || asset_name[0] == '\0')
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, base_path, false))
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, "\\", false))
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, asset_name, true))
+ {
+ return false;
+ }
+
+ if (extension != nullptr && !AppendPathPart(path, path_size, extension, false))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool BuildSourceDisplayPath(char *path, size_t path_size, const char *asset_name, const char *extension = nullptr)
+{
+ if (path == nullptr || path_size == 0)
+ {
+ return false;
+ }
+
+ path[0] = '\0';
+ if (Config::active_mod.empty() || asset_name == nullptr || asset_name[0] == '\0')
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, "mods\\", false))
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, Config::active_mod.c_str(), false))
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, "\\", false))
+ {
+ return false;
+ }
+
+ if (!AppendPathPart(path, path_size, asset_name, true))
+ {
+ return false;
+ }
+
+ if (extension != nullptr && !AppendPathPart(path, path_size, extension, false))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+void PrintOverrideApplied(const char *type, const char *asset_name, const char *source)
+{
+ char display_name[MAX_PATH];
+ display_name[0] = '\0';
+ const char *name = asset_name;
+ if (AppendPathPart(display_name, sizeof(display_name), asset_name, true))
+ {
+ name = display_name;
+ }
+
+ Com_Printf(CON_CHANNEL_FILES, "^2codxe^7: %s \"%s\" -> \"%s\"\n", type, name, source);
+}
+
+OverrideCacheEntry *FindCacheEntry(XAssetType type, const char *name)
+{
+ if (name == nullptr || name[0] == '\0')
+ {
+ return nullptr;
+ }
+
+ if (s_override_cache == nullptr)
+ {
+ return nullptr;
+ }
+
+ for (size_t i = 0; i < s_override_cache_capacity; ++i)
+ {
+ OverrideCacheEntry &entry = s_override_cache[i];
+ if (entry.state == CACHE_LOADED && entry.type == type && strcmp(entry.name, name) == 0)
+ {
+ return &entry;
+ }
+ }
+
+ return nullptr;
+}
+
+OverrideCacheEntry *AllocateCacheEntry(XAssetType type, const char *name)
+{
+ if (s_override_cache == nullptr)
+ {
+ return nullptr;
+ }
+
+ for (size_t i = 0; i < s_override_cache_capacity; ++i)
+ {
+ if (s_override_cache[i].state == CACHE_EMPTY)
+ {
+ OverrideCacheEntry *entry = &s_override_cache[i];
+ memset(entry, 0, sizeof(*entry));
+ entry->type = type;
+ if (!CopyAssetName(entry->name, sizeof(entry->name), name))
+ {
+ memset(entry, 0, sizeof(*entry));
+ return nullptr;
+ }
+
+ return entry;
+ }
+ }
+
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] Override cache full. type=%d name='%s'\n", type, name);
+ return nullptr;
+}
+
+void *AllocStorage(size_t size, const char *reason)
+{
+ if (size == 0 || size > 0x7FFFFFFF)
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] VirtualAlloc rejected for '%s'. size=%u\n",
+ reason ? reason : "", static_cast(size));
+ return nullptr;
+ }
+
+ void *storage = AllocVirtualBlock(size, reason);
+ if (storage == nullptr)
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] VirtualAlloc failed for '%s'. size=%u error=0x%08X\n",
+ reason ? reason : "", static_cast(size), GetLastError());
+ }
+
+ return storage;
+}
+
+bool FileExistsFast(const char *path)
+{
+ const DWORD attrs = GetFileAttributesA(path);
+ return attrs != INVALID_FILE_SIZE && (attrs & FILE_ATTRIBUTE_DIRECTORY) == 0;
+}
+
+bool LoadFileBuffer(const char *path, bool null_terminate, void *&storage, DWORD &storage_size, DWORD &payload_size)
+{
+ storage = nullptr;
+ storage_size = 0;
+ payload_size = 0;
+
+ HANDLE file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (file == INVALID_HANDLE_VALUE)
+ {
+ return false;
+ }
+
+ const DWORD file_size = GetFileSize(file, nullptr);
+ if (file_size == INVALID_FILE_SIZE || file_size > 0x7FFFFFFF)
+ {
+ CloseHandle(file);
+ return false;
+ }
+
+ const DWORD extra_byte = null_terminate ? 1 : 0;
+ const size_t alloc_size = static_cast(file_size) + extra_byte;
+ storage = AllocStorage(alloc_size == 0 ? 1 : alloc_size, path);
+ if (storage == nullptr)
+ {
+ CloseHandle(file);
+ return false;
+ }
+
+ if (file_size > 0)
+ {
+ DWORD bytes_read = 0;
+ if (!ReadFile(file, storage, file_size, &bytes_read, nullptr) || bytes_read != file_size)
+ {
+ CloseHandle(file);
+ FreeVirtualBlock(storage, static_cast(alloc_size == 0 ? 1 : alloc_size), path);
+ storage = nullptr;
+ return false;
+ }
+ }
+
+ CloseHandle(file);
+
+ if (null_terminate)
+ {
+ static_cast(storage)[file_size] = '\0';
+ }
+
+ storage_size = static_cast(alloc_size == 0 ? 1 : alloc_size);
+ payload_size = file_size;
+ return true;
+}
+
+struct CsvStats
+{
+ size_t row_count;
+ size_t column_count;
+ size_t field_count;
+ size_t string_bytes;
+};
+
+class MemoryCsvStreamBuf : public std::streambuf
+{
+ public:
+ MemoryCsvStreamBuf(const char *data, size_t size)
+ {
+ char *begin = const_cast(data);
+ setg(begin, begin, begin + size);
+ }
+};
+
+bool AddCsvFieldToStats(CsvStats &stats, size_t ¤t_columns, const std::string &field)
+{
+ const size_t field_length = field.size();
+ if (stats.field_count >= 0x7FFFFFFF || field_length > 0x7FFFFFFF ||
+ stats.string_bytes > 0x7FFFFFFF - (field_length + 1))
+ {
+ return false;
+ }
+
+ ++current_columns;
+ ++stats.field_count;
+ stats.string_bytes += field_length + 1;
+ return true;
+}
+
+bool FinishCsvStatsRow(CsvStats &stats, size_t ¤t_columns)
+{
+ if (stats.row_count >= 0x7FFFFFFF || current_columns > 0x7FFFFFFF)
+ {
+ return false;
+ }
+
+ ++stats.row_count;
+ if (current_columns > stats.column_count)
+ {
+ stats.column_count = current_columns;
+ }
+
+ current_columns = 0;
+ return true;
+}
+
+bool FinalizeCsvStats(CsvStats &stats)
+{
+ const size_t cell_count = stats.row_count * stats.column_count;
+ if (stats.column_count != 0 && cell_count / stats.column_count != stats.row_count)
+ {
+ return false;
+ }
+
+ if (stats.field_count > cell_count)
+ {
+ return false;
+ }
+
+ const size_t padded_empty_fields = cell_count - stats.field_count;
+ if (stats.string_bytes > 0x7FFFFFFF - padded_empty_fields)
+ {
+ return false;
+ }
+
+ stats.string_bytes += padded_empty_fields;
+ return true;
+}
+
+bool ScanStringTableCsvBuffer(const char *data, size_t data_size, CsvStats &stats)
+{
+ memset(&stats, 0, sizeof(stats));
+
+ MemoryCsvStreamBuf streambuf(data, data_size);
+ std::istream input(&streambuf);
+
+ try
+ {
+ aria::csv::CsvParser parser(input);
+ size_t current_columns = 0;
+
+ for (;;)
+ {
+ const aria::csv::Field field = parser.next_field();
+ switch (field.type)
+ {
+ case aria::csv::FieldType::CSV_END:
+ if (current_columns != 0 && !FinishCsvStatsRow(stats, current_columns))
+ {
+ return false;
+ }
+
+ return FinalizeCsvStats(stats);
+
+ case aria::csv::FieldType::ROW_END:
+ if (!FinishCsvStatsRow(stats, current_columns))
+ {
+ return false;
+ }
+
+ break;
+
+ case aria::csv::FieldType::DATA:
+ if (field.data == nullptr || !AddCsvFieldToStats(stats, current_columns, *field.data))
+ {
+ return false;
+ }
+
+ break;
+ }
+ }
+ }
+ catch (const std::exception &e)
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] aria csv scan exception: %s\n", e.what());
+ return false;
+ }
+}
+
+bool WriteEmptyStringCell(const char **values, size_t cell_index, char *&string_cursor, char *string_end)
+{
+ if (string_cursor >= string_end)
+ {
+ return false;
+ }
+
+ values[cell_index] = string_cursor;
+ *string_cursor++ = '\0';
+ return true;
+}
+
+bool FinishStringTableRow(const char **values, size_t row_count, size_t column_count, size_t &row_index,
+ size_t ¤t_column, char *&string_cursor, char *string_end)
+{
+ if (row_index >= row_count)
+ {
+ return false;
+ }
+
+ while (current_column < column_count)
+ {
+ if (!WriteEmptyStringCell(values, row_index * column_count + current_column, string_cursor, string_end))
+ {
+ return false;
+ }
+
+ ++current_column;
+ }
+
+ ++row_index;
+ current_column = 0;
+ return true;
+}
+
+bool WriteStringTableField(const std::string &field, const char **values, size_t row_count, size_t column_count,
+ size_t row_index, size_t ¤t_column, char *&string_cursor, char *string_end)
+{
+ if (row_index >= row_count || current_column >= column_count)
+ {
+ return false;
+ }
+
+ const size_t field_length = field.size();
+ if (field_length > 0x7FFFFFFF)
+ {
+ return false;
+ }
+
+ const size_t remaining = static_cast(string_end - string_cursor);
+ if (remaining < field_length + 1)
+ {
+ return false;
+ }
+
+ values[row_index * column_count + current_column] = string_cursor;
+ if (field_length > 0)
+ {
+ memcpy(string_cursor, field.c_str(), field_length);
+ }
+
+ string_cursor[field_length] = '\0';
+ string_cursor += field_length + 1;
+ ++current_column;
+ return true;
+}
+
+bool FillStringTableCsvBuffer(const char *data, size_t data_size, void *storage, size_t row_count, size_t column_count,
+ size_t values_bytes_aligned, size_t string_bytes)
+{
+ const size_t cell_count = row_count * column_count;
+ if (cell_count == 0)
+ {
+ return row_count == 0 && column_count == 0;
+ }
+
+ const char **values = reinterpret_cast(storage);
+ char *string_cursor = static_cast(storage) + values_bytes_aligned;
+ char *string_end = string_cursor + string_bytes;
+ MemoryCsvStreamBuf streambuf(data, data_size);
+ std::istream input(&streambuf);
+ size_t row_index = 0;
+ size_t current_column = 0;
+
+ try
+ {
+ aria::csv::CsvParser parser(input);
+
+ for (;;)
+ {
+ const aria::csv::Field field = parser.next_field();
+ switch (field.type)
+ {
+ case aria::csv::FieldType::CSV_END:
+ if (current_column != 0 && !FinishStringTableRow(values, row_count, column_count, row_index,
+ current_column, string_cursor, string_end))
+ {
+ return false;
+ }
+
+ return row_index == row_count;
+
+ case aria::csv::FieldType::ROW_END:
+ if (!FinishStringTableRow(values, row_count, column_count, row_index, current_column, string_cursor,
+ string_end))
+ {
+ return false;
+ }
+
+ break;
+
+ case aria::csv::FieldType::DATA:
+ if (field.data == nullptr ||
+ !WriteStringTableField(*field.data, values, row_count, column_count, row_index, current_column,
+ string_cursor, string_end))
+ {
+ return false;
+ }
+
+ break;
+ }
+ }
+ }
+ catch (const std::exception &e)
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] aria csv fill exception: %s\n", e.what());
+ return false;
+ }
+}
+
+StringTable *LoadStringTableOverride(const char *asset_name)
+{
+ OverrideCacheEntry *entry = FindCacheEntry(ASSET_TYPE_STRINGTABLE, asset_name);
+ if (entry != nullptr)
+ {
+ return &entry->stringtable;
+ }
+
+ char path[MAX_PATH];
+ if (!BuildAssetPath(path, sizeof(path), asset_name))
+ {
+ return nullptr;
+ }
+
+ if (!FileExistsFast(path))
+ {
+ return nullptr;
+ }
+
+ void *file_storage = nullptr;
+ DWORD file_storage_size = 0;
+ DWORD file_size = 0;
+ if (!LoadFileBuffer(path, true, file_storage, file_storage_size, file_size))
+ {
+ return nullptr;
+ }
+
+ CsvStats stats;
+ if (!ScanStringTableCsvBuffer(static_cast(file_storage), file_size, stats))
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] Failed to parse stringtable csv '%s'\n", path);
+ FreeVirtualBlock(file_storage, file_storage_size, path);
+ return nullptr;
+ }
+
+ const size_t cell_count = stats.row_count * stats.column_count;
+ const size_t values_bytes = cell_count * sizeof(const char *);
+ const size_t values_bytes_aligned = AlignSize(values_bytes, STRINGTABLE_VALUE_ALIGNMENT);
+ if (cell_count > 0x7FFFFFFF || values_bytes_aligned > 0x7FFFFFFF ||
+ stats.string_bytes > 0x7FFFFFFF - values_bytes_aligned)
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] Stringtable csv too large '%s'\n", path);
+ FreeVirtualBlock(file_storage, file_storage_size, path);
+ return nullptr;
+ }
+
+ void *storage = nullptr;
+ const size_t storage_size = values_bytes_aligned + stats.string_bytes;
+ if (storage_size > 0)
+ {
+ storage = AllocStorage(storage_size, path);
+ if (storage == nullptr)
+ {
+ FreeVirtualBlock(file_storage, file_storage_size, path);
+ return nullptr;
+ }
+
+ memset(storage, 0, storage_size);
+ }
+
+ if (storage_size > 0 &&
+ !FillStringTableCsvBuffer(static_cast(file_storage), file_size, storage, stats.row_count,
+ stats.column_count, values_bytes_aligned, stats.string_bytes))
+ {
+ Com_Printf(CON_CHANNEL_FILES, "[codxe][assets] Failed to build stringtable csv '%s'\n", path);
+ FreeVirtualBlock(file_storage, file_storage_size, path);
+ FreeVirtualBlock(storage, static_cast(storage_size), path);
+ return nullptr;
+ }
+
+ FreeVirtualBlock(file_storage, file_storage_size, path);
+
+ entry = AllocateCacheEntry(ASSET_TYPE_STRINGTABLE, asset_name);
+ if (entry == nullptr)
+ {
+ if (storage != nullptr)
+ {
+ FreeVirtualBlock(storage, static_cast(storage_size), path);
+ }
+
+ return nullptr;
+ }
+
+ entry->storage = storage;
+ entry->storage_size = static_cast(storage_size);
+ entry->storage_process_type = KeGetCurrentProcessType();
+ entry->payload_size = static_cast(storage_size);
+ entry->stringtable.name = entry->name;
+ entry->stringtable.columnCount = static_cast(stats.column_count);
+ entry->stringtable.rowCount = static_cast(stats.row_count);
+ entry->stringtable.values = cell_count == 0 ? nullptr : reinterpret_cast(storage);
+ entry->state = CACHE_LOADED;
+
+ return &entry->stringtable;
+}
+
+OverrideCacheEntry *LoadMapEntsOverride(const char *asset_name)
+{
+ OverrideCacheEntry *entry = FindCacheEntry(ASSET_TYPE_MAP_ENTS, asset_name);
+ if (entry != nullptr)
+ {
+ return entry;
+ }
+
+ char path[MAX_PATH];
+ if (!BuildAssetPath(path, sizeof(path), asset_name, ".ents"))
+ {
+ return nullptr;
+ }
+
+ void *storage = nullptr;
+ DWORD storage_size = 0;
+ DWORD payload_size = 0;
+ if (!LoadFileBuffer(path, true, storage, storage_size, payload_size))
+ {
+ return nullptr;
+ }
+
+ entry = AllocateCacheEntry(ASSET_TYPE_MAP_ENTS, asset_name);
+ if (entry == nullptr)
+ {
+ FreeVirtualBlock(storage, storage_size, path);
+ return nullptr;
+ }
+
+ entry->storage = storage;
+ entry->storage_size = storage_size;
+ entry->storage_process_type = KeGetCurrentProcessType();
+ entry->payload_size = payload_size;
+ entry->state = CACHE_LOADED;
+
+ return entry;
+}
+
+void OverrideMapEnts(MapEnts *asset)
+{
+ if (asset == nullptr || asset->name == nullptr || asset->name[0] == '\0')
+ {
+ return;
+ }
+
+ OverrideCacheEntry *entry = LoadMapEntsOverride(asset->name);
+ if (entry == nullptr || entry->state != CACHE_LOADED)
+ {
+ return;
+ }
+
+ asset->entityString = static_cast(entry->storage);
+ asset->numEntityChars = static_cast(entry->payload_size);
+
+ char source[MAX_PATH];
+ if (BuildSourceDisplayPath(source, sizeof(source), asset->name, ".ents"))
+ {
+ PrintOverrideApplied("mapents", asset->name, source);
+ }
+}
+
+void OverrideStringTable(StringTable *asset)
+{
+ if (asset == nullptr || asset->name == nullptr || asset->name[0] == '\0')
+ {
+ return;
+ }
+
+ StringTable *override_asset = LoadStringTableOverride(asset->name);
+ if (override_asset == nullptr)
+ {
+ return;
+ }
+
+ asset->columnCount = override_asset->columnCount;
+ asset->rowCount = override_asset->rowCount;
+ asset->values = override_asset->values;
+
+ char source[MAX_PATH];
+ if (BuildSourceDisplayPath(source, sizeof(source), asset->name))
+ {
+ PrintOverrideApplied("stringtable", asset->name, source);
+ }
+}
+
+XAssetEntry *DB_LinkXAssetEntry_Hook(XAssetEntry *newEntry, int allowOverride)
+{
+ if (newEntry != nullptr)
+ {
+ switch (newEntry->asset.type)
+ {
+ case ASSET_TYPE_MAP_ENTS:
+ OverrideMapEnts(newEntry->asset.header.mapEnts);
+ break;
+ case ASSET_TYPE_STRINGTABLE:
+ OverrideStringTable(newEntry->asset.header.stringTable);
+ break;
+ }
+ }
+
+ return DB_LinkXAssetEntry_Detour.GetOriginal()(newEntry, allowOverride);
+}
+
+} // namespace
+
+assets::assets()
+{
+ InitializeOverrideCache();
+
+ DB_LinkXAssetEntry_Detour = Detour(DB_LinkXAssetEntry, DB_LinkXAssetEntry_Hook);
+ DB_LinkXAssetEntry_Detour.Install();
+}
+
+assets::~assets()
+{
+ DB_LinkXAssetEntry_Detour.Remove();
+
+ ShutdownOverrideCache();
+}
+} // namespace mp
+} // namespace iw3
diff --git a/src/game/iw3/mp/components/scr_parser.h b/src/game/iw3/mp/components/assets.h
similarity index 62%
rename from src/game/iw3/mp/components/scr_parser.h
rename to src/game/iw3/mp/components/assets.h
index 460c157d..67564c10 100644
--- a/src/game/iw3/mp/components/scr_parser.h
+++ b/src/game/iw3/mp/components/assets.h
@@ -6,15 +6,15 @@ namespace iw3
{
namespace mp
{
-class scr_parser : public Module
+class assets : public Module
{
public:
- scr_parser();
- ~scr_parser();
+ assets();
+ ~assets();
const char *get_name() override
{
- return "scr_parser";
+ return "assets";
};
};
} // namespace mp
diff --git a/src/game/iw3/mp/components/cmds.cpp b/src/game/iw3/mp/components/cmds.cpp
index e633b616..3daea549 100644
--- a/src/game/iw3/mp/components/cmds.cpp
+++ b/src/game/iw3/mp/components/cmds.cpp
@@ -111,46 +111,17 @@ void ClientCommand_Hook(int clientNum)
ClientCommand_Detour.GetOriginal()(clientNum);
}
-Detour Cmd_ExecFromFastFile_Detour;
-
-bool Cmd_ExecFromFastFile_Hook(int localClientNum, int controllerIndex, const char *filename)
-{
- auto callOriginal = [&]()
- {
- return Cmd_ExecFromFastFile_Detour.GetOriginal()(localClientNum,
- controllerIndex, filename);
- };
-
- // Check if mod is active
- std::string modBasePath = Config::GetModBasePath();
- if (modBasePath.empty())
- return callOriginal();
-
- std::string contents = filesystem::read_file_to_string(modBasePath + "\\" + filename);
- if (contents.empty())
- return callOriginal();
-
- Com_Printf(CON_CHANNEL_SYSTEM, "execing %s from raw:\\\n", filename);
- Cbuf_ExecuteBuffer(localClientNum, controllerIndex, contents.c_str());
- return true;
-}
-
cmds::cmds()
{
ClientCommand_Detour = Detour(ClientCommand, ClientCommand_Hook);
ClientCommand_Detour.Install();
- Cmd_ExecFromFastFile_Detour = Detour(Cmd_ExecFromFastFile, Cmd_ExecFromFastFile_Hook);
- Cmd_ExecFromFastFile_Detour.Install();
-
command::add("dumpraw", Cmd_Dumpraw_f);
}
cmds::~cmds()
{
ClientCommand_Detour.Remove();
-
- Cmd_ExecFromFastFile_Detour.Remove();
}
} // namespace mp
} // namespace iw3
diff --git a/src/game/iw3/mp/components/mpsp.cpp b/src/game/iw3/mp/components/mpsp.cpp
index 31747abf..90394e95 100644
--- a/src/game/iw3/mp/components/mpsp.cpp
+++ b/src/game/iw3/mp/components/mpsp.cpp
@@ -438,52 +438,6 @@ const ZoneOverride ZONE_OVERRIDES[] = {
int g_zoneOverrideIndex = -1;
-Detour DB_LinkXAssetEntry_Detour;
-XAssetEntry *DB_LinkXAssetEntry_Hook(XAssetEntry *newEntry, int allowOverride)
-{
- XAsset xasset;
- xasset.type = newEntry->asset.type;
- xasset.header = newEntry->asset.header;
-
- if (mpsp::is_sp_map)
- {
- switch (newEntry->asset.type)
- {
- case ASSET_TYPE_MAP_ENTS:
- {
- Asset::MapEnts_::override_(newEntry->asset.header.mapEnts);
- break;
- }
- case ASSET_TYPE_RAWFILE:
- {
- Asset::RawFile_::override_(newEntry->asset.header.rawfile);
- break;
- }
- case ASSET_TYPE_GAMEWORLD_SP:
- {
- newEntry->asset.type = ASSET_TYPE_GAMEWORLD_MP;
- break;
- }
- // Hijack the reference asset ',' mechanism to avoid reaching asset limits.
- case ASSET_TYPE_WEAPON:
- {
- static const std::string weapon_default_reference_name =
- std::string(",") + g_defaultAssetName[ASSET_TYPE_WEAPON];
- DB_SetXAssetName(&xasset, weapon_default_reference_name.c_str());
- break;
- }
- case ASSET_TYPE_FX:
- {
- static const std::string fx_default_reference_name = std::string(",") + g_defaultAssetName[ASSET_TYPE_FX];
- DB_SetXAssetName(&xasset, fx_default_reference_name.c_str());
- break;
- }
- }
- }
-
- return DB_LinkXAssetEntry_Detour.GetOriginal()(newEntry, allowOverride);
-}
-
Detour Com_sprintf_Detour;
int Com_sprintf_Hook(char *dest, unsigned int size, const char *fmt...)
{
@@ -778,10 +732,6 @@ mpsp::mpsp()
DB_AuthLoad_Inflate_Detour = Detour(DB_AuthLoad_Inflate, DB_AuthLoad_Inflate_Hook);
DB_AuthLoad_Inflate_Detour.Install();
- // Rewrite some assets before linking
- DB_LinkXAssetEntry_Detour = Detour(DB_LinkXAssetEntry, DB_LinkXAssetEntry_Hook);
- DB_LinkXAssetEntry_Detour.Install();
-
Load_XAssetArrayCustom_Detour = Detour(Load_XAssetArrayCustom, Load_XAssetArrayCustom_Stub);
Load_XAssetArrayCustom_Detour.Install();
@@ -799,8 +749,6 @@ mpsp::~mpsp()
DB_AuthLoad_Inflate_Detour.Remove();
- DB_LinkXAssetEntry_Detour.Remove();
-
Load_XAssetArrayCustom_Detour.Remove();
SV_AddEntitiesVisibleFromPoint_Detour.Remove();
diff --git a/src/game/iw3/mp/components/scr_parser.cpp b/src/game/iw3/mp/components/scr_parser.cpp
deleted file mode 100644
index e7d6dbc0..00000000
--- a/src/game/iw3/mp/components/scr_parser.cpp
+++ /dev/null
@@ -1,148 +0,0 @@
-#include "pch.h"
-#include "scr_parser.h"
-
-namespace iw3
-{
-namespace mp
-{
-namespace
-{
-const size_t MAX_SCRIPT_PATH = 512;
-
-void NormalizePath(char *path)
-{
- if (path == nullptr)
- return;
-
- for (char *cursor = path; *cursor != '\0'; ++cursor)
- {
- if (*cursor == '/')
- *cursor = '\\';
- }
-}
-
-bool BuildScriptPath(char *path, size_t path_size, const char *base_path, const char *script_path)
-{
- if (path == nullptr || path_size == 0 || base_path == nullptr || base_path[0] == '\0' || script_path == nullptr ||
- script_path[0] == '\0')
- {
- return false;
- }
-
- const int written = _snprintf_s(path, path_size, _TRUNCATE, "%s\\%s", base_path, script_path);
- path[path_size - 1] = '\0';
-
- if (written < 0 || static_cast(written) >= path_size)
- return false;
-
- NormalizePath(path);
- return true;
-}
-
-char *ReadFileToGameTempBuffer(const char *path)
-{
- HANDLE file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE)
- return nullptr;
-
- const DWORD file_size = GetFileSize(file, nullptr);
- if (file_size == INVALID_FILE_SIZE || file_size == 0)
- {
- CloseHandle(file);
- return nullptr;
- }
-
- char *buffer = static_cast(Hunk_AllocateTempMemoryHighInternal(file_size + 1));
- if (buffer == nullptr)
- {
- CloseHandle(file);
- return nullptr;
- }
-
- DWORD bytes_read = 0;
- if (!ReadFile(file, buffer, file_size, &bytes_read, nullptr) || bytes_read != file_size)
- {
- CloseHandle(file);
- return nullptr;
- }
-
- CloseHandle(file);
- buffer[file_size] = '\0';
- return buffer;
-}
-
-void WriteScriptDump(const char *script_path, const char *contents)
-{
- if (script_path == nullptr || contents == nullptr)
- return;
-
- char dump_path[MAX_SCRIPT_PATH];
- if (!BuildScriptPath(dump_path, sizeof(dump_path), DUMP_DIR, script_path))
- return;
-
- char dir_path[MAX_SCRIPT_PATH];
- strncpy(dir_path, dump_path, sizeof(dir_path) - 1);
- dir_path[sizeof(dir_path) - 1] = '\0';
-
- char *last_slash = strrchr(dir_path, '\\');
- if (last_slash != nullptr)
- {
- *last_slash = '\0';
- filesystem::create_nested_dirs(dir_path);
- }
-
- HANDLE file = CreateFileA(dump_path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE)
- return;
-
- DWORD bytes_written = 0;
- const DWORD content_size = static_cast(std::strlen(contents));
- WriteFile(file, contents, content_size, &bytes_written, nullptr);
- CloseHandle(file);
-}
-} // namespace
-
-Detour Scr_AddSourceBuffer_Detour;
-
-char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, const char *codePos, bool archive)
-{
- auto callOriginal = [&]()
- {
- return Scr_AddSourceBuffer_Detour.GetOriginal()(filename, extFilename, codePos,
- archive);
- };
-
- if (Config::dump_rawfile)
- {
- char *contents = callOriginal();
- if (contents != nullptr)
- WriteScriptDump(extFilename, contents);
-
- return contents;
- }
-
- char override_path[MAX_SCRIPT_PATH];
- if (!BuildScriptPath(override_path, sizeof(override_path), Config::GetModBasePathCStr(), extFilename))
- return callOriginal();
-
- char *buffer = ReadFileToGameTempBuffer(override_path);
- if (buffer == nullptr)
- return callOriginal();
-
- DbgPrint("GSCLoader: Loaded override script: %s\n", override_path);
- return buffer;
-}
-
-scr_parser::scr_parser()
-{
- Scr_AddSourceBuffer_Detour = Detour(Scr_AddSourceBuffer, Scr_AddSourceBuffer_Hook);
- Scr_AddSourceBuffer_Detour.Install();
-}
-
-scr_parser::~scr_parser()
-{
- Scr_AddSourceBuffer_Detour.Remove();
-}
-} // namespace mp
-} // namespace iw3
diff --git a/src/game/iw3/mp/main.cpp b/src/game/iw3/mp/main.cpp
index 7205c8e3..b1fd4e51 100644
--- a/src/game/iw3/mp/main.cpp
+++ b/src/game/iw3/mp/main.cpp
@@ -1,4 +1,5 @@
#include "pch.h"
+#include "components/assets.h"
#include "components/cg.h"
#include "components/cj_tas.h"
#include "components/clipmap.h"
@@ -14,7 +15,6 @@
#include "components/image_loader.h"
#include "components/mpsp.h"
#include "components/pm.h"
-#include "components/scr_parser.h"
#include "components/stats.h"
#include "components/sv_bots.h"
#include "common/config.h"
@@ -24,78 +24,6 @@ namespace iw3
{
namespace mp
{
-Detour Load_MapEntsPtr_Detour;
-
-void Load_MapEntsPtr_Hook()
-{
- // TODO: don't write null byte to file
- // and add null byte to entityString when reading from file
-
- DbgPrint("Load_MapEntsPtr_Hook\n");
-
- // TODO: write comment what this is ***
- // Get pointer to pointer stored at 0x82475914
- MapEnts **varMapEntsPtr = *(MapEnts ***)0x82475914;
-
- Load_MapEntsPtr_Detour.GetOriginal()();
-
- // Validate pointer before dereferencing
- if (varMapEntsPtr && *varMapEntsPtr)
- {
- MapEnts *mapEnts = *varMapEntsPtr;
-
- // Write stock map ents to disk
- std::string file_path = DUMP_DIR;
- file_path += std::string("\\") + mapEnts->name;
- file_path += ".ents"; // iw4x naming convention
- std::replace(file_path.begin(), file_path.end(), '/', '\\'); // Replace forward slashes with backslashes
- filesystem::write_file_to_disk(file_path.c_str(), mapEnts->entityString, mapEnts->numEntityChars);
-
- // Use new ConfigModule API
- // Load map ents from file
- // Path to check for existing entity file
- std::string raw_file_path = Config::GetModBasePath();
-
- raw_file_path += std::string("\\") + mapEnts->name;
- raw_file_path += ".ents"; // IW4x naming convention
- std::replace(raw_file_path.begin(), raw_file_path.end(), '/', '\\'); // Replace forward slashes with backslashes
-
- // If the file exists, replace entityString
- if (filesystem::file_exists(raw_file_path))
- {
- DbgPrint("Found entity file: %s\n", raw_file_path.c_str());
- std::string new_entity_string = filesystem::read_file_to_string(raw_file_path);
- if (!new_entity_string.empty())
- {
- // Allocate new memory and copy the data
- size_t new_size = new_entity_string.size() + 1; // Include null terminator
- char *new_memory = static_cast(malloc(new_size));
-
- if (new_memory)
- {
- memcpy(new_memory, new_entity_string.c_str(), new_size); // Copy with null terminator
-
- // Update the entityString pointer to point to the new memory
- mapEnts->entityString = new_memory;
-
- // // Update numEntityChars
- // mapEnts->numEntityChars = static_cast(new_entity_string.size()); // unnecessary
-
- DbgPrint("Replaced entityString from file: %s\n", raw_file_path.c_str());
- }
- else
- {
- DbgPrint("Failed to allocate memory for entityString replacement.\n");
- }
- }
- }
- }
- else
- {
- DbgPrint("Hooked Load_MapEntsPtr: varMapEntsPtr is NULL or invalid.\n");
- }
-}
-
/**
* Patch out the signature checks used during fastfile authentication.
* Signature data must still be present in
@@ -125,6 +53,7 @@ IW3_MP_Plugin::IW3_MP_Plugin()
RegisterModule(new command());
RegisterModule(new cg());
+ RegisterModule(new assets());
RegisterModule(new cj_tas());
RegisterModule(new clipmap());
RegisterModule(new cmds());
@@ -137,17 +66,12 @@ IW3_MP_Plugin::IW3_MP_Plugin()
RegisterModule(new image_loader());
RegisterModule(new pm());
RegisterModule(new mpsp());
- RegisterModule(new scr_parser());
RegisterModule(new stats());
RegisterModule(new sv_bots());
-
- Load_MapEntsPtr_Detour = Detour(Load_MapEntsPtr, Load_MapEntsPtr_Hook);
- Load_MapEntsPtr_Detour.Install();
}
IW3_MP_Plugin::~IW3_MP_Plugin()
{
- Load_MapEntsPtr_Detour.Remove();
}
} // namespace mp
diff --git a/src/third_party/aria_csv/csv_parser.hpp b/src/third_party/aria_csv/csv_parser.hpp
new file mode 100644
index 00000000..ba117a2d
--- /dev/null
+++ b/src/third_party/aria_csv/csv_parser.hpp
@@ -0,0 +1,445 @@
+#ifndef ARIA_CSV_H
+#define ARIA_CSV_H
+
+// Local vendored copy modified for the Xbox 360 compiler used by this project.
+// https://github.com/AriaFallah/csv-parser
+
+#include
+#include
+#include
+#include
+#include
+
+namespace aria
+{
+namespace csv
+{
+
+namespace Term
+{
+enum Enum
+{
+ CRLF = -2
+};
+} // namespace Term
+
+namespace FieldType
+{
+enum Enum
+{
+ DATA,
+ ROW_END,
+ CSV_END
+};
+} // namespace FieldType
+
+typedef std::vector> CSV;
+
+// Checking for '\n', '\r', and '\r\n' by default
+inline bool operator==(const char c, const Term::Enum t)
+{
+ switch (t)
+ {
+ case Term::CRLF:
+ return c == '\r' || c == '\n';
+ default:
+ return static_cast(t) == c;
+ }
+}
+
+inline bool operator!=(const char c, const Term::Enum t)
+{
+ return !(c == t);
+}
+
+// Wraps returned fields so we can also indicate
+// that we hit row endings or the end of the csv itself
+struct Field
+{
+ explicit Field(FieldType::Enum t) : type(t), data(nullptr)
+ {
+ }
+ explicit Field(const std::string &str) : type(FieldType::DATA), data(&str)
+ {
+ }
+
+ FieldType::Enum type;
+ const std::string *data;
+};
+
+// Reads and parses lines from a csv file
+class CsvParser
+{
+ private:
+ // CSV state for state machine
+ struct State
+ {
+ enum Enum
+ {
+ START_OF_FIELD,
+ IN_FIELD,
+ IN_QUOTED_FIELD,
+ IN_ESCAPED_QUOTE,
+ END_OF_ROW,
+ EMPTY
+ };
+ };
+ State::Enum m_state;
+
+ // Configurable attributes
+ char m_quote;
+ char m_delimiter;
+ Term::Enum m_terminator;
+ std::istream &m_input;
+
+ // Buffer capacities
+ static const int INPUTBUF_CAP = 4096;
+
+ // Buffers
+ std::string m_fieldbuf;
+ char m_inputbuf[INPUTBUF_CAP];
+
+ // Misc
+ bool m_eof;
+ size_t m_cursor;
+ size_t m_inputbuf_size;
+ std::streamoff m_scanposition;
+
+ CsvParser(const CsvParser &);
+ CsvParser &operator=(const CsvParser &);
+
+ public:
+ // Creates the CSV parser which by default, splits on commas,
+ // uses quotes to escape, and handles CSV files that end in either
+ // '\r', '\n', or '\r\n'.
+ explicit CsvParser(std::istream &input)
+ : m_state(State::START_OF_FIELD), m_quote('"'), m_delimiter(','), m_terminator(Term::CRLF), m_input(input),
+ m_eof(false), m_cursor(INPUTBUF_CAP), m_inputbuf_size(INPUTBUF_CAP), m_scanposition(-INPUTBUF_CAP)
+ {
+ if (!m_input.good())
+ {
+ throw std::runtime_error("Something is wrong with input stream");
+ }
+ }
+
+ // Change the quote character
+ CsvParser &"e(char c)
+ {
+ m_quote = c;
+ return std::move(*this);
+ }
+
+ // Change the delimiter character
+ CsvParser &&delimiter(char c)
+ {
+ m_delimiter = c;
+ return std::move(*this);
+ }
+
+ // Change the terminator character
+ CsvParser &&terminator(char c)
+ {
+ m_terminator = static_cast(c);
+ return std::move(*this);
+ }
+
+ // The parser is in the empty state when there are
+ // no more tokens left to read from the input buffer
+ bool empty()
+ {
+ return m_state == State::EMPTY;
+ }
+
+ // Not the actual position in the stream (its buffered) just the
+ // position up to last availiable token
+ std::streamoff position() const
+ {
+ return m_scanposition + static_cast(m_cursor);
+ }
+
+ // Reads a single field from the CSV
+ Field next_field()
+ {
+ if (empty())
+ {
+ return Field(FieldType::CSV_END);
+ }
+ m_fieldbuf.clear();
+
+ // This loop runs until either the parser has
+ // read a full field or until there's no tokens left to read
+ for (;;)
+ {
+ char *maybe_token = top_token();
+
+ // If we're out of tokens to read return whatever's left in the
+ // field and row buffers. If there's nothing left, return null.
+ if (!maybe_token)
+ {
+ m_state = State::EMPTY;
+ return !m_fieldbuf.empty() ? Field(m_fieldbuf) : Field(FieldType::CSV_END);
+ }
+
+ // Parsing the CSV is done using a finite state machine
+ char c = *maybe_token;
+ switch (m_state)
+ {
+ case State::START_OF_FIELD:
+ m_cursor++;
+ if (c == m_terminator)
+ {
+ handle_crlf(c);
+ m_state = State::END_OF_ROW;
+ return Field(m_fieldbuf);
+ }
+
+ if (c == m_quote)
+ {
+ m_state = State::IN_QUOTED_FIELD;
+ }
+ else if (c == m_delimiter)
+ {
+ return Field(m_fieldbuf);
+ }
+ else
+ {
+ m_state = State::IN_FIELD;
+ m_fieldbuf += c;
+ }
+
+ break;
+
+ case State::IN_FIELD:
+ m_cursor++;
+ if (c == m_terminator)
+ {
+ handle_crlf(c);
+ m_state = State::END_OF_ROW;
+ return Field(m_fieldbuf);
+ }
+
+ if (c == m_delimiter)
+ {
+ m_state = State::START_OF_FIELD;
+ return Field(m_fieldbuf);
+ }
+ else
+ {
+ m_fieldbuf += c;
+ }
+
+ break;
+
+ case State::IN_QUOTED_FIELD:
+ m_cursor++;
+ if (c == m_quote)
+ {
+ m_state = State::IN_ESCAPED_QUOTE;
+ }
+ else
+ {
+ m_fieldbuf += c;
+ }
+
+ break;
+
+ case State::IN_ESCAPED_QUOTE:
+ m_cursor++;
+ if (c == m_terminator)
+ {
+ handle_crlf(c);
+ m_state = State::END_OF_ROW;
+ return Field(m_fieldbuf);
+ }
+
+ if (c == m_quote)
+ {
+ m_state = State::IN_QUOTED_FIELD;
+ m_fieldbuf += c;
+ }
+ else if (c == m_delimiter)
+ {
+ m_state = State::START_OF_FIELD;
+ return Field(m_fieldbuf);
+ }
+ else
+ {
+ m_state = State::IN_FIELD;
+ m_fieldbuf += c;
+ }
+
+ break;
+
+ case State::END_OF_ROW:
+ m_state = State::START_OF_FIELD;
+ return Field(FieldType::ROW_END);
+
+ case State::EMPTY:
+ throw std::logic_error("You goofed");
+ }
+ }
+ }
+
+ private:
+ // When the parser hits the end of a line it needs
+ // to check the special case of '\r\n' as a terminator.
+ // If it finds that the previous token was a '\r', and
+ // the next token will be a '\n', it skips the '\n'.
+ void handle_crlf(const char c)
+ {
+ if (m_terminator != Term::CRLF || c != '\r')
+ {
+ return;
+ }
+
+ char *token = top_token();
+ if (token && *token == '\n')
+ {
+ m_cursor++;
+ }
+ }
+
+ // Pulls the next token from the input buffer, but does not move
+ // the cursor forward. If the stream is empty and the input buffer
+ // is also empty return a nullptr.
+ char *top_token()
+ {
+ // Return null if there's nothing left to read
+ if (m_eof && m_cursor == m_inputbuf_size)
+ {
+ return nullptr;
+ }
+
+ // Refill the input buffer if it's been fully read
+ if (m_cursor == m_inputbuf_size)
+ {
+ m_scanposition += static_cast(m_cursor);
+ m_cursor = 0;
+ m_input.read(m_inputbuf, INPUTBUF_CAP);
+
+ // Indicate we hit end of file, and resize
+ // input buffer to show that it's not at full capacity
+ if (m_input.eof())
+ {
+ m_eof = true;
+ m_inputbuf_size = static_cast(m_input.gcount());
+
+ // Return null if there's nothing left to read
+ if (m_inputbuf_size == 0)
+ {
+ return nullptr;
+ }
+ }
+ }
+
+ return &m_inputbuf[m_cursor];
+ }
+
+ public:
+ // Iterator implementation for the CSV parser, which reads
+ // from the CSV row by row in the form of a vector of strings
+ class iterator
+ {
+ public:
+ typedef std::ptrdiff_t difference_type;
+ typedef std::vector value_type;
+ typedef const std::vector *pointer;
+ typedef const std::vector &reference;
+ typedef std::input_iterator_tag iterator_category;
+
+ explicit iterator(CsvParser *p, bool end = false) : m_parser(p), m_current_row(end ? -1 : 0)
+ {
+ if (!end)
+ {
+ m_row.reserve(50);
+ next();
+ }
+ }
+
+ iterator &operator++()
+ {
+ next();
+ return *this;
+ }
+
+ iterator operator++(int)
+ {
+ iterator i = (*this);
+ ++(*this);
+ return i;
+ }
+
+ bool operator==(const iterator &other) const
+ {
+ return m_current_row == other.m_current_row && m_row.size() == other.m_row.size();
+ }
+
+ bool operator!=(const iterator &other) const
+ {
+ return !(*this == other);
+ }
+
+ reference operator*() const
+ {
+ return m_row;
+ }
+
+ pointer operator->() const
+ {
+ return &m_row;
+ }
+
+ private:
+ value_type m_row;
+ CsvParser *m_parser;
+ int m_current_row;
+
+ void next()
+ {
+ value_type::size_type num_fields = 0;
+ for (;;)
+ {
+ auto field = m_parser->next_field();
+ switch (field.type)
+ {
+ case FieldType::CSV_END:
+ if (num_fields < m_row.size())
+ {
+ m_row.resize(num_fields);
+ }
+ m_current_row = -1;
+ return;
+ case FieldType::ROW_END:
+ if (num_fields < m_row.size())
+ {
+ m_row.resize(num_fields);
+ }
+ m_current_row++;
+ return;
+ case FieldType::DATA:
+ if (num_fields < m_row.size())
+ {
+ m_row[num_fields] = std::move(*field.data);
+ }
+ else
+ {
+ m_row.push_back(std::move(*field.data));
+ }
+ num_fields++;
+ }
+ }
+ }
+ };
+
+ iterator begin()
+ {
+ return iterator(this);
+ };
+ iterator end()
+ {
+ return iterator(this, true);
+ };
+};
+} // namespace csv
+} // namespace aria
+#endif