From cdf7d52214658820c32c6e28e5c5cba8665900d3 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Sun, 9 Aug 2026 02:48:01 -0700 Subject: [PATCH] Added AAS2File code. --- source/engine/aas2file/aas2debugareamodel.cpp | 50 + source/engine/aas2file/aas2debugareamodel.h | 37 +- .../aas2file/aas2debugareamodeldata.cpp | 224 ++++ .../engine/aas2file/aas2debugareamodeldata.h | 70 +- .../aas2file/aas2debugareamodelgenerator.cpp | 109 ++ .../aas2file/aas2debugareamodelgenerator.h | 16 +- source/engine/aas2file/aas2file.cpp | 1119 +++++++++++++++++ source/engine/aas2file/aas2file.h | 539 ++++++-- source/engine/aas2file/aas2file_sample.cpp | 971 ++++++++++++++ source/engine/aas2file/aas2file_text.cpp | 485 +++++++ .../engine/aas2file/aastraversalchaindata.h | 130 +- 11 files changed, 3560 insertions(+), 190 deletions(-) create mode 100644 source/engine/aas2file/aas2debugareamodel.cpp create mode 100644 source/engine/aas2file/aas2debugareamodeldata.cpp create mode 100644 source/engine/aas2file/aas2debugareamodelgenerator.cpp create mode 100644 source/engine/aas2file/aas2file.cpp create mode 100644 source/engine/aas2file/aas2file_sample.cpp create mode 100644 source/engine/aas2file/aas2file_text.cpp diff --git a/source/engine/aas2file/aas2debugareamodel.cpp b/source/engine/aas2file/aas2debugareamodel.cpp new file mode 100644 index 0000000..dba8ec2 --- /dev/null +++ b/source/engine/aas2file/aas2debugareamodel.cpp @@ -0,0 +1,50 @@ +#include "aas2file/aas2debugareamodel.h" + +#include "aas2file/aas2debugareamodelgenerator.h" +#include "framework/resourcelist.h" +#include "idlib/filesystem/filesystem.h" + +idResourceList idAAS2DebugAreaModel::resourceList("aasDebugAreaModel"); + +idAAS2DebugAreaModel::idAAS2DebugAreaModel() + : data(), sourceTimestamp(std::uint32_t(-1)) { +} + +idAAS2DebugAreaModel::~idAAS2DebugAreaModel() = default; + +idResourceList* idAAS2DebugAreaModel::GetResourceList() { + return &resourceList; +} + +void idAAS2DebugAreaModel::LoadResource() { + data.Free(); + sourceTimestamp = std::uint32_t(-1); + char binaryPath[256] = {}; + fileSystem->FixLongFilename("generated", + idAAS2DebugAreaModelData::BINARY_FILE_EXTENSION, GetName(), + binaryPath, sizeof(binaryPath)); + idStr error; + const idResource::resourceError_t loadError = + data.LoadBinary(binaryPath, error); + if (loadError != idResource::RESOURCE_ERROR_NONE) { + resourceError = "Could not load generated AAS debug area model"; + } +} + +bool idAAS2DebugAreaModel::ReloadIfStale() { + if (fileSystem->GetTimestamp(GetName(), false) == sourceTimestamp) { + return false; + } + char binaryPath[256] = {}; + fileSystem->FixLongFilename("generated", + idAAS2DebugAreaModelData::BINARY_FILE_EXTENSION, GetName(), + binaryPath, sizeof(binaryPath)); + fileSystem->RemoveFile(binaryPath, FSPATH_BASE); + LoadResource(); + return true; +} + +void idAAS2DebugAreaModel::WriteResourceFile() { + idAAS2DebugAreaModelGenerator::WriteToBinary(GetName(), GetName(), + sourceTimestamp, data); +} diff --git a/source/engine/aas2file/aas2debugareamodel.h b/source/engine/aas2file/aas2debugareamodel.h index b04a883..ae722d4 100644 --- a/source/engine/aas2file/aas2debugareamodel.h +++ b/source/engine/aas2file/aas2debugareamodel.h @@ -1,24 +1,27 @@ #pragma once -// Reconstructed C++ declarations from IDA Local Types and PDB/DIA metadata. -// Original PDB header: w:\tech5\engine\aas2file\aas2debugareamodel.h -// Recovered logical types: 1 -// Signatures retain Xbox 360 ABI evidence and may still require manual review. +#include "aas2file/aas2debugareamodeldata.h" +#include "framework/resource.h" +#include -// IDA Local Type ordinal 23519; PDB kind: class. -class idAAS2DebugAreaModel : public idResource -{ +class idAAS2DebugAreaModel : public idResource { public: - // Recovered virtual interface; IDA vtable ordinal 23520. - virtual ~idAAS2DebugAreaModel(); - virtual void LoadResource(); - virtual bool ReloadIfStale(); - virtual void WriteResourceFile(); - virtual idResourceList *GetResourceList(); - virtual void Print(); - virtual void List(); + idAAS2DebugAreaModel(); + ~idAAS2DebugAreaModel() override; - idAAS2DebugAreaModelData data; - unsigned int sourceTimestamp; + void LoadResource() override; + bool ReloadIfStale() override; + void WriteResourceFile() override; + idResourceList* GetResourceList() override; + + static idResourceList resourceList; + + idAAS2DebugAreaModelData data; + std::uint32_t sourceTimestamp; }; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idAAS2DebugAreaModel) == 72, + "Recovered idAAS2DebugAreaModel ABI changed"); +#endif diff --git a/source/engine/aas2file/aas2debugareamodeldata.cpp b/source/engine/aas2file/aas2debugareamodeldata.cpp new file mode 100644 index 0000000..bfc8d73 --- /dev/null +++ b/source/engine/aas2file/aas2debugareamodeldata.cpp @@ -0,0 +1,224 @@ +#include "aas2file/aas2debugareamodeldata.h" + +#include "framework/resource.h" +#include "idlib/filesystem/file.h" +#include "idlib/filesystem/filesystem.h" + +#include + +namespace { + +constexpr std::uint32_t DEBUG_AAS_RESOURCE_ID = 0x41415344u; + +#pragma pack(push, 4) +struct DebugResourceHeader { + std::uint16_t headerVersionHi; + std::uint16_t headerVersionLo; + std::uint32_t resourceId; + std::uint16_t versionHi; + std::uint16_t versionLo; + std::uint32_t sourceTimestamp; + std::uint16_t sourceFileNameLen; + std::uint16_t uniqueIdNameLen; + std::uint8_t platform; + std::uint8_t pad[3]; + std::uint64_t uniqueId; + std::uint64_t hash; + std::uint64_t dataOffset; + std::uint64_t totalSize; +}; +#pragma pack(pop) + +static_assert(sizeof(DebugResourceHeader) == 56, + "Recovered debug resource header layout changed"); + +idAAS2DebugAreaModelData::drawTriangleCallback_t drawTriangleCallback = nullptr; + +bool ReadExact(idFile& file, void* data, const unsigned int bytes) { + return bytes == 0 || file.Read(data, bytes) == bytes; +} + +std::uint16_t ByteSwap16(const std::uint16_t value) { + return static_cast((value << 8) | (value >> 8)); +} + +std::uint32_t ByteSwap32(const std::uint32_t value) { + return (value << 24) | ((value << 8) & 0x00FF0000u) + | ((value >> 8) & 0x0000FF00u) | (value >> 24); +} + +std::uint64_t ByteSwap64(const std::uint64_t value) { + return (static_cast(ByteSwap32( + static_cast(value))) << 32) + | ByteSwap32(static_cast(value >> 32)); +} + +void SwapHeader(DebugResourceHeader& header) { + header.headerVersionHi = ByteSwap16(header.headerVersionHi); + header.headerVersionLo = ByteSwap16(header.headerVersionLo); + header.resourceId = ByteSwap32(header.resourceId); + header.versionHi = ByteSwap16(header.versionHi); + header.versionLo = ByteSwap16(header.versionLo); + header.sourceTimestamp = ByteSwap32(header.sourceTimestamp); + header.sourceFileNameLen = ByteSwap16(header.sourceFileNameLen); + header.uniqueIdNameLen = ByteSwap16(header.uniqueIdNameLen); + header.uniqueId = ByteSwap64(header.uniqueId); + header.hash = ByteSwap64(header.hash); + header.dataOffset = ByteSwap64(header.dataOffset); + header.totalSize = ByteSwap64(header.totalSize); +} + +void SwapWord(void* const pointer) { + std::uint32_t value = 0; + std::memcpy(&value, pointer, sizeof(value)); + value = ByteSwap32(value); + std::memcpy(pointer, &value, sizeof(value)); +} + +} // namespace + +idAAS2DebugAreaModelData::idAAS2DebugAreaModelData() + : vertices(0), areas(0) { +} + +idAAS2DebugAreaModelData::~idAAS2DebugAreaModelData() = default; + +void idAAS2DebugAreaModelData::SetDrawTriangleCallback( + const drawTriangleCallback_t callback) { + drawTriangleCallback = callback; +} + +void idAAS2DebugAreaModelData::Draw(idRenderWorld* const renderWorld, + const idList& visibleAreas) const { + if (renderWorld == nullptr || drawTriangleCallback == nullptr) return; + const float color[4] = { 0.0f, 1.0f, 0.0f, 0.5f }; + for (int visibleIndex = 0; visibleIndex < visibleAreas.Num(); ++visibleIndex) { + const int areaNum = visibleAreas[visibleIndex]; + if (areaNum < 0 || areaNum >= areas.Num()) continue; + const debugAreaInfo_t& area = areas[areaNum]; + for (int triangleIndex = 0; triangleIndex < area.tris.Num(); ++triangleIndex) { + const areaTri_t& triangle = area.tris[triangleIndex]; + if (triangle.v0 < 0 || triangle.v0 >= vertices.Num() + || triangle.v1 < 0 || triangle.v1 >= vertices.Num() + || triangle.v2 < 0 || triangle.v2 >= vertices.Num()) continue; + drawTriangleCallback(renderWorld, vertices[triangle.v0], + vertices[triangle.v1], vertices[triangle.v2], color); + } + } +} + +void idAAS2DebugAreaModelData::BuildResourceNameFromAASName( + const char* const aasName, idStr& resourceName) { + resourceName = aasName; + resourceName.Append(TEXT_FILE_EXTENSION); +} + +void idAAS2DebugAreaModelData::Free() { + vertices.Clear(); + areas.ClearFree(); +} + +idResource::resourceError_t idAAS2DebugAreaModelData::LoadBinary( + const char* const binaryFileName, + idStr& errorMessage) { + Free(); + idFileLocal file(fileSystem->OpenFileRead(binaryFileName, true, false)); + if (file.file == nullptr) { + errorMessage = "file not found"; + return idResource::RESOURCE_ERROR_FILE_NOT_FOUND; + } + DebugResourceHeader header{}; + if (!ReadExact(*file.file, &header, sizeof(header))) { + errorMessage = "truncated resource header"; + return idResource::RESOURCE_ERROR_TRUNCATED; + } + const bool byteSwap = header.headerVersionHi != 1 + && ByteSwap16(header.headerVersionHi) == 1; + if (byteSwap) SwapHeader(header); + if (header.headerVersionHi != 1 || header.headerVersionLo != 1) { + errorMessage = "resource header version mismatch"; + return idResource::RESOURCE_ERROR_HEADER_VERSION_MISMATCH; + } + if (header.resourceId != DEBUG_AAS_RESOURCE_ID) { + errorMessage = "resource type mismatch"; + return idResource::RESOURCE_ERROR_WRONG_TYPE; + } + if (header.versionHi != BINARY_VERSION_HI + || header.versionLo != BINARY_VERSION_LO) { + errorMessage = "debug AAS resource version mismatch"; + return idResource::RESOURCE_ERROR_RESOURCE_VERSION_MISMATCH; + } + if (header.sourceFileNameLen >= 256 || header.uniqueIdNameLen >= 256) { + errorMessage = "invalid resource name"; + return idResource::RESOURCE_ERROR_INVALID_NAME; + } + if (header.dataOffset < sizeof(header) + || header.totalSize < header.dataOffset + || header.totalSize > static_cast(file.file->Length())) { + errorMessage = "invalid debug AAS resource offsets"; + return idResource::RESOURCE_ERROR_TRUNCATED; + } + if (file.file->Seek(static_cast(header.dataOffset), + FS_SEEK_SET) != 0) { + errorMessage = "could not seek debug AAS resource data"; + return idResource::RESOURCE_ERROR_TRUNCATED; + } + int vertexCount = 0; + if (!ReadExact(*file.file, &vertexCount, 4)) { + errorMessage = "Bad number of vertices"; + return idResource::RESOURCE_ERROR_FATAL; + } + if (byteSwap) SwapWord(&vertexCount); + if (vertexCount <= 0 + || !vertices.SetNum(vertexCount) + || !ReadExact(*file.file, vertices.Ptr(), vertexCount * 12u)) { + errorMessage = "Bad number of vertices"; + Free(); + return idResource::RESOURCE_ERROR_FATAL; + } + if (byteSwap) { + for (int vertex = 0; vertex < vertexCount; ++vertex) { + SwapWord(&vertices[vertex].x); SwapWord(&vertices[vertex].y); + SwapWord(&vertices[vertex].z); + } + } + int areaCount = 0; + if (!ReadExact(*file.file, &areaCount, 4)) { + errorMessage = "Bad number of areas"; + Free(); + return idResource::RESOURCE_ERROR_FATAL; + } + if (byteSwap) SwapWord(&areaCount); + if (areaCount <= 0 + || !areas.SetNum(areaCount)) { + errorMessage = "Bad number of areas"; + Free(); + return idResource::RESOURCE_ERROR_FATAL; + } + for (int areaNum = 0; areaNum < areaCount; ++areaNum) { + int triangleCount = 0; + if (!ReadExact(*file.file, &triangleCount, 4)) { + errorMessage = "Bad number of area tris"; + Free(); + return idResource::RESOURCE_ERROR_FATAL; + } + if (byteSwap) SwapWord(&triangleCount); + if (triangleCount < 0 + || !areas[areaNum].tris.SetNum(triangleCount) + || !ReadExact(*file.file, areas[areaNum].tris.Ptr(), + triangleCount * 12u)) { + errorMessage = "Bad number of area tris"; + Free(); + return idResource::RESOURCE_ERROR_FATAL; + } + if (byteSwap) { + for (int triangle = 0; triangle < triangleCount; ++triangle) { + SwapWord(&areas[areaNum].tris[triangle].v0); + SwapWord(&areas[areaNum].tris[triangle].v1); + SwapWord(&areas[areaNum].tris[triangle].v2); + } + } + } + errorMessage.Clear(); + return idResource::RESOURCE_ERROR_NONE; +} diff --git a/source/engine/aas2file/aas2debugareamodeldata.h b/source/engine/aas2file/aas2debugareamodeldata.h index a116896..876f826 100644 --- a/source/engine/aas2file/aas2debugareamodeldata.h +++ b/source/engine/aas2file/aas2debugareamodeldata.h @@ -1,37 +1,47 @@ #pragma once -// Reconstructed C++ declarations from IDA Local Types and PDB/DIA metadata. -// Original PDB header: w:\tech5\engine\aas2file\aas2debugareamodeldata.h -// Recovered logical types: 4 -// Signatures retain Xbox 360 ABI evidence and may still require manual review. +#include "framework/resource.h" +#include "idlib/containers/list.h" +#include "idlib/math/vector.h" +#include "idlib/text/str.h" +class idRenderWorld; -// IDA Local Type ordinal 23514; PDB kind: struct. -struct idAAS2DebugAreaModelData::areaTri_t -{ - int v0; - int v1; - int v2; -}; - -// IDA Local Type ordinal 23516; PDB kind: struct. -struct idAAS2DebugAreaModelData::debugAreaInfo_t -{ - idList tris; -}; - -// IDA Local Type ordinal 23518; PDB kind: class. -class idAAS2DebugAreaModelData -{ +class idAAS2DebugAreaModelData { public: - idList vertices; - idList areas; + struct areaTri_t { int v0; int v1; int v2; }; + struct debugAreaInfo_t { idList tris; }; + + idAAS2DebugAreaModelData(); + ~idAAS2DebugAreaModelData(); + + using drawTriangleCallback_t = void (*)(idRenderWorld* renderWorld, + const idVec3& v0, const idVec3& v1, const idVec3& v2, + const float color[4]); + static void SetDrawTriangleCallback(drawTriangleCallback_t callback); + + void Draw(idRenderWorld* renderWorld, + const idList& visibleAreas) const; + static void BuildResourceNameFromAASName(const char* aasName, + idStr& resourceName); + void Free(); + idResource::resourceError_t LoadBinary(const char* binaryFileName, + idStr& errorMessage); + + static constexpr int BINARY_VERSION_HI = 1; + static constexpr int BINARY_VERSION_LO = 0; + static constexpr const char* BINARY_FILE_EXTENSION = "baasd"; + static constexpr const char* TEXT_FILE_EXTENSION = ".aasd"; + + idList vertices; + idList areas; }; -// IDA Local Type ordinal 23883; PDB kind: class. -class idAAS2DebugAreaModelData::LoadBinary::__l2::idLocalFileBuffer -{ -public: - unsigned int dataLen; - char *data; -}; +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idAAS2DebugAreaModelData::areaTri_t) == 12, + "Recovered debug area triangle ABI changed"); +static_assert(sizeof(idAAS2DebugAreaModelData::debugAreaInfo_t) == 16, + "Recovered debug area info ABI changed"); +static_assert(sizeof(idAAS2DebugAreaModelData) == 32, + "Recovered debug area data ABI changed"); +#endif diff --git a/source/engine/aas2file/aas2debugareamodelgenerator.cpp b/source/engine/aas2file/aas2debugareamodelgenerator.cpp new file mode 100644 index 0000000..f83a969 --- /dev/null +++ b/source/engine/aas2file/aas2debugareamodelgenerator.cpp @@ -0,0 +1,109 @@ +#include "aas2file/aas2debugareamodelgenerator.h" + +#include "aas2file/aas2debugareamodeldata.h" +#include "idlib/filesystem/file.h" +#include "idlib/filesystem/filesystem.h" +#include "idlib/hashing/murmur.h" + +#include +#include + +namespace { + +constexpr std::uint32_t DEBUG_AAS_RESOURCE_ID = 0x41415344u; + +#pragma pack(push, 4) +struct DebugResourceHeader { + std::uint16_t headerVersionHi; + std::uint16_t headerVersionLo; + std::uint32_t resourceId; + std::uint16_t versionHi; + std::uint16_t versionLo; + std::uint32_t sourceTimestamp; + std::uint16_t sourceFileNameLen; + std::uint16_t uniqueIdNameLen; + std::uint8_t platform; + std::uint8_t pad[3]; + std::uint64_t uniqueId; + std::uint64_t hash; + std::uint64_t dataOffset; + std::uint64_t totalSize; +}; +#pragma pack(pop) + +bool WriteExact(idFile& file, const void* data, const unsigned int bytes) { + return bytes == 0 || file.Write(data, bytes) == bytes; +} + +} // namespace + +bool idAAS2DebugAreaModelGenerator::WriteToBinary( + const char* const uniqueIdName, const char* const fileName, + const std::uint32_t sourceTimestamp, + const idAAS2DebugAreaModelData& data) { + char binaryPath[256] = {}; + fileSystem->FixLongFilename("generated", + idAAS2DebugAreaModelData::BINARY_FILE_EXTENSION, fileName, + binaryPath, sizeof(binaryPath)); + const char* const sourceName = fileName != nullptr ? fileName : ""; + const char* const uniqueName = uniqueIdName != nullptr ? uniqueIdName : ""; + const std::uint16_t sourceLength = static_cast( + (std::min)(std::size_t(255), std::strlen(sourceName))); + const std::uint16_t uniqueLength = static_cast( + (std::min)(std::size_t(255), std::strlen(uniqueName))); + + std::uint64_t payloadSize = 8u + data.vertices.Num() * 12ull; + for (int area = 0; area < data.areas.Num(); ++area) { + payloadSize += 4 + data.areas[area].tris.Num() * 12ull; + } + if (payloadSize > static_cast(UINT_MAX)) return false; + + idFile_Memory payload; + const int vertexCount = data.vertices.Num(); + const int areaCount = data.areas.Num(); + if (!WriteExact(payload, &vertexCount, 4) + || !WriteExact(payload, data.vertices.Ptr(), vertexCount * 12u) + || !WriteExact(payload, &areaCount, 4)) return false; + for (int area = 0; area < areaCount; ++area) { + const int triangleCount = data.areas[area].tris.Num(); + if (!WriteExact(payload, &triangleCount, 4) + || !WriteExact(payload, data.areas[area].tris.Ptr(), + triangleCount * 12u)) return false; + } + + idFileLocal file(fileSystem->OpenFileWrite(binaryPath, FSPATH_BASE)); + if (file.file == nullptr) return false; + DebugResourceHeader header{}; + header.headerVersionHi = 1; + header.headerVersionLo = 1; + header.resourceId = DEBUG_AAS_RESOURCE_ID; + header.versionHi = idAAS2DebugAreaModelData::BINARY_VERSION_HI; + header.versionLo = idAAS2DebugAreaModelData::BINARY_VERSION_LO; + header.sourceTimestamp = sourceTimestamp; + header.sourceFileNameLen = sourceLength; + header.uniqueIdNameLen = uniqueLength; + // The recovered writer stores RESOURCEPLATFORM_360. This is the PC + // port's equivalent resource and therefore advertises Windows (0). + header.platform = 0; + header.uniqueId = 0; + header.hash = MurMur64_HashData(payload.GetDataPtr(), + static_cast(payload.Length()), 0); + header.dataOffset = (sizeof(header) + sourceLength + uniqueLength + 15u) + & ~std::uint64_t(15u); + header.totalSize = header.dataOffset + payloadSize; + + if (!WriteExact(*file.file, &header, sizeof(header)) + || !WriteExact(*file.file, sourceName, sourceLength) + || !WriteExact(*file.file, uniqueName, uniqueLength)) return false; + const std::uint8_t zeroBytes[16] = {}; + const std::uint64_t writtenHeaderBytes = + sizeof(header) + sourceLength + uniqueLength; + const unsigned int padding = static_cast( + header.dataOffset - writtenHeaderBytes); + if (!WriteExact(*file.file, zeroBytes, padding) + || !WriteExact(*file.file, payload.GetDataPtr(), + static_cast(payload.Length()))) { + return false; + } + return true; +} diff --git a/source/engine/aas2file/aas2debugareamodelgenerator.h b/source/engine/aas2file/aas2debugareamodelgenerator.h index 4569e66..e188cad 100644 --- a/source/engine/aas2file/aas2debugareamodelgenerator.h +++ b/source/engine/aas2file/aas2debugareamodelgenerator.h @@ -1,13 +1,15 @@ #pragma once -// Reconstructed C++ declarations from IDA Local Types and PDB/DIA metadata. -// Original PDB header: w:\tech5\engine\aas2file\aas2debugareamodelgenerator.h -// Recovered logical types: 1 -// Signatures retain Xbox 360 ABI evidence and may still require manual review. +#include +class idAAS2DebugAreaModelData; -// IDA Local Type ordinal 23855; PDB kind: class. -class idAAS2DebugAreaModelGenerator -{ +class idAAS2DebugAreaModelGenerator { public: + static bool WriteToBinary(const char* uniqueIdName, const char* fileName, + std::uint32_t sourceTimestamp, + const idAAS2DebugAreaModelData& data); }; + +static_assert(sizeof(idAAS2DebugAreaModelGenerator) == 1, + "Recovered stateless AAS2 debug generator ABI changed"); diff --git a/source/engine/aas2file/aas2file.cpp b/source/engine/aas2file/aas2file.cpp new file mode 100644 index 0000000..8a05413 --- /dev/null +++ b/source/engine/aas2file/aas2file.cpp @@ -0,0 +1,1119 @@ +#include "aas2file/aas2file.h" + +#include "framework/resourcelist.h" +#include "idlib/filesystem/file.h" +#include "idlib/filesystem/filesystem.h" +#include "idlib/hashing/crc32.h" +#include "idlib/lib_print.h" +#include "idlib/text/lexer.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t AAS2_FILE_ID_BINARY = 0x32534141u; // "AAS2" +constexpr std::uint8_t AAS2_BINARY_MAJOR = 3; +constexpr std::uint8_t AAS2_BINARY_MINOR = 18; +constexpr std::uint8_t AAS2_BINARY_REVISION = 97; + +bool ReadExact(idFile& file, void* data, const unsigned int bytes) { + return bytes == 0 || file.Read(data, bytes) == bytes; +} + +bool WriteExact(idFile& file, const void* data, const unsigned int bytes) { + return bytes == 0 || file.Write(data, bytes) == bytes; +} + +std::uint16_t ByteSwap16(const std::uint16_t value) { + return static_cast((value << 8) | (value >> 8)); +} + +std::uint32_t ByteSwap32(const std::uint32_t value) { + return (value << 24) | ((value << 8) & 0x00FF0000u) + | ((value >> 8) & 0x0000FF00u) | (value >> 24); +} + +void Swap(std::uint16_t& value) { value = ByteSwap16(value); } +void Swap(std::int16_t& value) { + value = static_cast( + ByteSwap16(static_cast(value))); +} +void Swap(std::uint32_t& value) { value = ByteSwap32(value); } +void Swap(int& value) { + value = static_cast(ByteSwap32(static_cast(value))); +} +void Swap(float& value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + bits = ByteSwap32(bits); + std::memcpy(&value, &bits, sizeof(value)); +} +void Swap(std::uint8_t&) {} + +template +void Swap(idIndex& index) { + valueType value = index.Get(); + Swap(value); + index = idIndex(value); +} + +void Swap(idVec3& value) { + Swap(value.x); Swap(value.y); Swap(value.z); +} + +void Swap(idBounds& value) { Swap(value[0]); Swap(value[1]); } + +void Swap(idPlane& value) { + Swap(value.a); Swap(value.b); Swap(value.c); Swap(value.d); +} + +void Swap(idQuantizedVec3& value) { + Swap(value.x); Swap(value.y); Swap(value.z); +} + +void Swap(aas2Edge_t& value) { + Swap(value.vertexNum[0]); Swap(value.vertexNum[1]); Swap(value.flags); +} + +void Swap(aas2Reachability_t& value) { + Swap(value.travelFlags); Swap(value.travelTime); + Swap(value.fromAreaNum); Swap(value.toAreaNum); Swap(value.padding); + for (int axis = 0; axis < 3; ++axis) Swap(value.start[axis]); + for (int axis = 0; axis < 3; ++axis) Swap(value.end[axis]); + Swap(value.areaTTOfsAndNumber); Swap(value.next); Swap(value.rev_next); +} + +void Swap(aas2Area_t& value) { + Swap(value.travelFlags); Swap(value.flags); Swap(value.numEdges); + Swap(value.firstEdge); Swap(value.cluster); Swap(value.clusterAreaNum); + Swap(value.obstaclePVSOffset); Swap(value.reach); Swap(value.rev_reach); + Swap(value.firstChokePoint); Swap(value.numChokePoints); + Swap(value.firstCover); Swap(value.numCover); Swap(value.firstTraversal); + Swap(value.numTraversals); Swap(value.firstHintNode); + Swap(value.numHintNodes); +} + +void Swap(aas2Node_t& value) { + Swap(value.planeNum); Swap(value.flags); + Swap(value.children[0]); Swap(value.children[1]); +} + +void Swap(aas2Portal_t& value) { + Swap(value.areaNum); Swap(value.clusters[0]); Swap(value.clusters[1]); + Swap(value.clusterAreaNum[0]); Swap(value.clusterAreaNum[1]); + Swap(value.maxAreaTravelTime); +} + +void Swap(aas2Cluster_t& value) { + Swap(value.numAreas); Swap(value.numReachableAreas); + Swap(value.numPortals); Swap(value.firstPortal); +} + +void Swap(aas2Name_t& value) { Swap(value.index); } +void Swap(aas2AnimName_t&) {} +void Swap(aas2DependencyName_t&) {} +void Swap(aas2InteractionEntityName_t&) {} +void Swap(aas2TraversalEntityName_t&) {} + +void Swap(aas2Cover_t& value) { + Swap(value.origin); Swap(value.dir); Swap(value.areaNum); + Swap(value.flags); Swap(value.numTouching); Swap(value.firstTouching); + Swap(value.durationSec); Swap(value.minRange); Swap(value.maxRange); + Swap(value.reservedBy); Swap(value.usableTime); +} + +void Swap(aas2Traversal_t& value) { + Swap(value.startPoint); Swap(value.endPoint); Swap(value.orientationFwd); + Swap(value.extrusionFwd); Swap(value.animIndex); + Swap(value.reachabilityIndex); Swap(value.dependencyIndex); + Swap(value.interactionEntIndex); Swap(value.extrusionDistance); + Swap(value.startAreaNum); Swap(value.endAreaNum); + Swap(value.traversalNameIndex); Swap(value.flags); +} + +void Swap(aas2HintNode_t& value) { + Swap(value.origin); Swap(value.areaNum); Swap(value.radius); + Swap(value.hintData); +} + +void Swap(idAAS2File::bspTree_t& value) { + Swap(value.floorNormal); Swap(value.headNode); + Swap(value.firstArea); Swap(value.lastArea); +} + +void Swap(aas2AreaBounds_t& value) { + for (int axis = 0; axis < 3; ++axis) Swap(value.min[axis]); + for (int axis = 0; axis < 3; ++axis) Swap(value.max[axis]); +} + +bool ReadEndianString(idFile& file, idStr& string, + const bool byteSwap) { + std::uint32_t count = 0; + if (!ReadExact(file, &count, sizeof(count))) return false; + if (byteSwap) Swap(count); + if (count > 16u * 1024u * 1024u) return false; + if (count == 0) { + string.Clear(); + return true; + } + char* text = static_cast(std::malloc(count + 1u)); + if (text == nullptr) return false; + const bool result = ReadExact(file, text, count); + if (result) { + text[count] = '\0'; + string = text; + } + std::free(text); + return result; +} + +bool ReadBinarySettings(idFile& file, idAAS2Settings& settings, + const bool byteSwap) { + int storedType = 0; + if (!ReadExact(file, &storedType, 4)) return false; + if (byteSwap) Swap(storedType); + if (!ReadEndianString(file, settings.fileExtensionAAS, byteSwap) + || !ReadEndianString(file, settings.groupName, byteSwap) + || !ReadEndianString(file, settings.explicitGroupName, byteSwap)) { + return false; + } + settings.type = static_cast(storedType); + void* blocks[] = { + &settings.boundingBox, &settings.primitiveModeBrush, + &settings.primitiveModePatch, &settings.primitiveModeModel, + &settings.gravityDir, &settings.gravityValue, + &settings.maxStepHeight, &settings.maxBarrierHeight, + &settings.maxWaterJumpHeight, &settings.maxFallHeight, + &settings.minFloorCos, &settings.minHighCeiling, + &settings.groundSpeed, &settings.waterSpeed, &settings.ladderSpeed, + &settings.wallCornerEdgeRadius, &settings.ledgeCornerEdgeRadius, + &settings.obstaclePVSRadius, &settings.minCrouchingCoverHeight, + &settings.minStandingCoverHeight, &settings.obstaclePVSRadius, + &settings.standingFireHeight, &settings.minWallWidth, + &settings.maxWallWidth, &settings.minDoorWidth, + &settings.maxDoorWidth, &settings.coverCornerDistance, + &settings.coverWallDistance, &settings.chokePointWidth, + &settings.tt_barrierJump, &settings.tt_waterJump, + &settings.tt_startWalkOffLedge, &settings.tt_startLadderClimb + }; + const unsigned int sizes[] = { + 24, 4, 4, 4, 12, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 + }; + for (unsigned int index = 0; + index < sizeof(sizes) / sizeof(sizes[0]); ++index) { + if (!ReadExact(file, blocks[index], sizes[index])) return false; + if (byteSwap) { + std::uint32_t* words = static_cast(blocks[index]); + for (unsigned int word = 0; word < sizes[index] / 4; ++word) { + words[word] = ByteSwap32(words[word]); + } + } + } + return true; +} + +template +bool ReadList(idFile& file, idList& list, + const bool byteSwap = false) { + int count = 0; + if (!ReadExact(file, &count, sizeof(count))) return false; + if (byteSwap) Swap(count); + if (count < 0 + || count > 0x10000000 / (std::max)(1, int(sizeof(type)))) { + return false; + } + if (!list.SetNum(count)) { + return false; + } + if (!ReadExact(file, list.Ptr(), + static_cast(count * sizeof(type)))) return false; + if (byteSwap) { + for (int index = 0; index < count; ++index) Swap(list[index]); + } + return true; +} + +template +bool WriteList(idFile& file, const idList& list) { + const int count = list.Num(); + return WriteExact(file, &count, sizeof(count)) + && WriteExact(file, list.Ptr(), + static_cast(count * sizeof(type))); +} + +template +indexType FindNameIndex(const idList& names, + const char* name) { + if (name == nullptr) { + return indexType(); + } + for (int index = 0; index < names.Num(); ++index) { + if (_stricmp(name, names[index].name) == 0) { + return indexType(static_cast(index)); + } + } + return indexType(); +} + +bool IsValidArea(const idList& areas, const int areaNum) { + return areaNum > 0 && areaNum < areas.Num(); +} + +idBounds ClearedBounds() { + idBounds result; + const float maximum = (std::numeric_limits::max)(); + result[0].Set(maximum, maximum, maximum); + result[1].Set(-maximum, -maximum, -maximum); + return result; +} + +void AddPoint(idBounds& bounds, const idVec3& point) { + for (int axis = 0; axis < 3; ++axis) { + bounds[0][axis] = (std::min)(bounds[0][axis], point[axis]); + bounds[1][axis] = (std::max)(bounds[1][axis], point[axis]); + } +} + +bool Intersects(const idBounds& left, const idBounds& right) { + for (int axis = 0; axis < 3; ++axis) { + if (left[1][axis] < right[0][axis] + || left[0][axis] > right[1][axis]) { + return false; + } + } + return true; +} + +bool Contains(const idBounds& bounds, const idVec3& point) { + for (int axis = 0; axis < 3; ++axis) { + if (point[axis] < bounds[0][axis] || point[axis] > bounds[1][axis]) { + return false; + } + } + return true; +} + +std::uint32_t CanonicalXboxAreaChecksum( + const idList& areas) { + if (areas.Num() == 0) return CRC32_BlockChecksum(nullptr, 0); + std::vector bigEndianAreas(areas.Ptr(), + areas.Ptr() + areas.Num()); + for (aas2Area_t& area : bigEndianAreas) Swap(area); + return CRC32_BlockChecksum(bigEndianAreas.data(), + static_cast(bigEndianAreas.size() * sizeof(aas2Area_t))); +} + +} // namespace + +idResourceList idAAS2File::resourceList("aas"); + +aas2Cover_t::aas2Cover_t() + : origin(), dir(), areaNum(0), flags(0), numTouching(0), + firstTouching(0), durationSec(-1.0f), minRange(0.0f), maxRange(0.0f), + reservedBy(0x1FFF), usableTime(0) { + origin.Zero(); + dir.Zero(); +} + +aas2Traversal_t::aas2Traversal_t() + : startPoint(), endPoint(), orientationFwd(), extrusionFwd(), + animIndex(), reachabilityIndex(), dependencyIndex(), + interactionEntIndex(), extrusionDistance(0), startAreaNum(0), + endAreaNum(-1), traversalNameIndex(0), flags(0) { + startPoint.Zero(); + endPoint.Zero(); + const idVec3 zero(0.0f, 0.0f, 0.0f); + orientationFwd.Set(zero); + extrusionFwd.Set(zero); + animIndex.Invalidate(); + reachabilityIndex.Invalidate(); + dependencyIndex = idIndex(0); + interactionEntIndex.Invalidate(); +} + +void aas2Traversal_t::Clear() { + startPoint.Zero(); + endPoint.Zero(); + const idVec3 zero(0.0f, 0.0f, 0.0f); + orientationFwd.Set(zero); + extrusionFwd.Set(zero); + animIndex.Invalidate(); + reachabilityIndex.Invalidate(); + dependencyIndex.Invalidate(); + interactionEntIndex.Invalidate(); + extrusionDistance = 0; + startAreaNum = 0; + endAreaNum = 0; + traversalNameIndex.Invalidate(); + flags = 0; +} + +void aas2Traversal_t::CalcExtrusionPoint(idVec3& extrusionPoint, + const idVec3&) const { + extrusionPoint = startPoint; + if (extrusionDistance > 0) { + extrusionPoint = extrusionPoint + + extrusionFwd.ToVec3() * static_cast(extrusionDistance); + } +} + +aas2HintNode_t::aas2HintNode_t() + : origin(), areaNum(0), radius(128), hintType(HINT_NODE_TYPE_SEARCH_AUTO), + orientation(0), dirFlags(0), grouping(0), hintData(0) { + origin.Zero(); +} + +void aas2HintNode_t::GetOrientationVector(idVec3& direction) const { + const float yaw = static_cast(orientation) * 1.4117647f; + const float radians = yaw * 0.01745329251994329577f; + direction.Set(std::cos(radians), std::sin(radians), 0.0f); +} + +aas2Area_t::aas2Area_t() + : travelFlags(0), flags(0), numEdges(0), firstEdge(0), cluster(0), + clusterAreaNum(0), obstaclePVSOffset(0), reach(), rev_reach(), + firstChokePoint(0), numChokePoints(0), firstCover(0), numCover(0), + firstTraversal(0), numTraversals(0), firstHintNode(0), numHintNodes(0) { + // The textual loader builds these chains explicitly; zero is the exact + // value materialized by the recovered constructor. + reach = idIndex(0); + rev_reach = idIndex(0); +} + +idAAS2Settings::idAAS2Settings() + : type(AAS_MAX), fileExtensionAAS("defaulted"), groupName("aas"), + explicitGroupName("defaulted"), boundingBox(), + primitiveModeBrush(AAS_PRIMITIVE_MODE_DEFAULT), + primitiveModePatch(AAS_PRIMITIVE_MODE_NEVER), + primitiveModeModel(AAS_PRIMITIVE_MODE_NEVER), + gravityDir(0.0f, 0.0f, -1.0f), gravityValue(1066.0f), + maxStepHeight(14.0f), maxBarrierHeight(32.0f), + maxWaterJumpHeight(20.0f), maxFallHeight(64.0f), minFloorCos(0.7f), + minHighCeiling(80.0f), groundSpeed(250.0f), waterSpeed(150.0f), + ladderSpeed(50.0f), wallCornerEdgeRadius(16.0f), + ledgeCornerEdgeRadius(16.0f), obstaclePVSRadius(1024.0f), + wallCornerReachabilityBackoff(0.0f), + highQualityReachabilityBackoff(0.0f), + minCrouchingCoverHeight(32.0f), minStandingCoverHeight(64.0f), + crouchingFireHeight(48.0f), standingFireHeight(72.0f), + minWallWidth(8.0f), maxWallWidth(32.0f), minDoorWidth(32.0f), + maxDoorWidth(80.0f), coverCornerDistance(8.0f), + coverWallDistance(8.0f), chokePointWidth(96.0f), + tt_barrierJump(100), tt_waterJump(100), + tt_startWalkOffLedge(100), tt_startLadderClimb(100) { + boundingBox[0].Set(-16.0f, -16.0f, 0.0f); + boundingBox[1].Set(16.0f, 16.0f, 96.0f); +} + +bool idAAS2Settings::ParseInt(idLexer& source, int& value) { + if (!source.ExpectTokenString("=")) return false; + value = source.ParseInt(); + return !source.HadError(); +} + +bool idAAS2Settings::ParseFloat(idLexer& source, float& value) { + if (!source.ExpectTokenString("=")) return false; + value = source.ParseFloat(); + return !source.HadError(); +} + +bool idAAS2Settings::ParseVector(idLexer& source, idVec3& vector) { + return source.ExpectTokenString("=") + && source.Parse1DMatrix(3, &vector.x, false); +} + +bool idAAS2Settings::ParseBounds(idLexer& source, idBounds& bounds) { + return source.ExpectTokenString("=") + && source.Parse1DMatrix(3, &bounds[0].x, false) + && source.ExpectTokenString("-") + && source.Parse1DMatrix(3, &bounds[1].x, false); +} + +bool idAAS2Settings::ValidForBounds(const idBounds& bounds) const { + for (int axis = 0; axis < 3; ++axis) { + if (bounds[0][axis] < boundingBox[0][axis] + || bounds[1][axis] > boundingBox[1][axis]) { + return false; + } + } + return true; +} + +bool idAAS2Settings::ReadFromFile(idLexer& source) { + if (!source.ExpectTokenString("{")) return false; + idToken token; + while (source.ReadToken(token)) { + const char* const key = token.c_str(); + if (std::strcmp(key, "}") == 0) return true; + + if (std::strcmp(key, "type") == 0) { + int parsed = static_cast(type); + if (!ParseInt(source, parsed)) return false; + type = static_cast(parsed); + } else if (std::strcmp(key, "mMaterial") == 0) { + idToken ignored; + if (!source.ExpectTokenString("=") || !source.ReadToken(ignored)) return false; + } else if (std::strcmp(key, "groupName") == 0 + || std::strcmp(key, "explicitGroupName") == 0) { + idToken text; + if (!source.ExpectTokenString("=") || !source.ReadToken(text)) return false; + (key[0] == 'g' ? groupName : explicitGroupName) = text.c_str(); + } else if (std::strcmp(key, "bbox") == 0) { + if (!ParseBounds(source, boundingBox)) return false; + } else if (std::strcmp(key, "primitiveModeBrush") == 0) { + if (!ParseInt(source, primitiveModeBrush)) return false; + } else if (std::strcmp(key, "primitiveModePatch") == 0) { + if (!ParseInt(source, primitiveModePatch)) return false; + } else if (std::strcmp(key, "primitiveModeModel") == 0) { + if (!ParseInt(source, primitiveModeModel)) return false; + } else if (std::strcmp(key, "gravity") == 0) { + if (!ParseVector(source, gravityDir)) return false; + } else if (std::strcmp(key, "gravityValue") == 0) { + if (!ParseFloat(source, gravityValue)) return false; + } else if (std::strcmp(key, "maxStepHeight") == 0) { + if (!ParseFloat(source, maxStepHeight)) return false; + } else if (std::strcmp(key, "maxBarrierHeight") == 0) { + if (!ParseFloat(source, maxBarrierHeight)) return false; + } else if (std::strcmp(key, "maxWaterJumpHeight") == 0) { + if (!ParseFloat(source, maxWaterJumpHeight)) return false; + } else if (std::strcmp(key, "maxFallHeight") == 0) { + if (!ParseFloat(source, maxFallHeight)) return false; + } else if (std::strcmp(key, "minFloorCos") == 0) { + if (!ParseFloat(source, minFloorCos)) return false; + } else if (std::strcmp(key, "minHighCeiling") == 0) { + if (!ParseFloat(source, minHighCeiling)) return false; + } else if (std::strcmp(key, "groundSpeed") == 0) { + if (!ParseFloat(source, groundSpeed)) return false; + } else if (std::strcmp(key, "waterSpeed") == 0) { + if (!ParseFloat(source, waterSpeed)) return false; + } else if (std::strcmp(key, "ladderSpeed") == 0) { + if (!ParseFloat(source, ladderSpeed)) return false; + } else if (std::strcmp(key, "wallCornerEdgeRadius") == 0) { + if (!ParseFloat(source, wallCornerEdgeRadius)) return false; + } else if (std::strcmp(key, "ledgeCornerEdgeRadius") == 0) { + if (!ParseFloat(source, ledgeCornerEdgeRadius)) return false; + } else if (std::strcmp(key, "obstaclePVSRadius") == 0) { + if (!ParseFloat(source, obstaclePVSRadius)) return false; + } else if (std::strcmp(key, "wallCornerReachabilityBackoff") == 0) { + if (!ParseFloat(source, wallCornerReachabilityBackoff)) return false; + } else if (std::strcmp(key, "highQualityReachabilityBackoff") == 0) { + if (!ParseFloat(source, highQualityReachabilityBackoff)) return false; + } else if (std::strcmp(key, "minCrouchingCoverHeight") == 0) { + if (!ParseFloat(source, minCrouchingCoverHeight)) return false; + } else if (std::strcmp(key, "minStandingCoverHeight") == 0) { + if (!ParseFloat(source, minStandingCoverHeight)) return false; + } else if (std::strcmp(key, "crouchingFireHeight") == 0) { + if (!ParseFloat(source, crouchingFireHeight)) return false; + } else if (std::strcmp(key, "standingFireHeight") == 0) { + if (!ParseFloat(source, standingFireHeight)) return false; + } else if (std::strcmp(key, "minWallWidth") == 0) { + if (!ParseFloat(source, minWallWidth)) return false; + } else if (std::strcmp(key, "maxWallWidth") == 0) { + if (!ParseFloat(source, maxWallWidth)) return false; + } else if (std::strcmp(key, "minDoorWidth") == 0) { + if (!ParseFloat(source, minDoorWidth)) return false; + } else if (std::strcmp(key, "maxDoorWidth") == 0) { + if (!ParseFloat(source, maxDoorWidth)) return false; + } else if (std::strcmp(key, "coverCornerDistance") == 0) { + if (!ParseFloat(source, coverCornerDistance)) return false; + } else if (std::strcmp(key, "coverWallDistance") == 0) { + if (!ParseFloat(source, coverWallDistance)) return false; + } else if (std::strcmp(key, "chokePointWidth") == 0) { + if (!ParseFloat(source, chokePointWidth)) return false; + } else if (std::strcmp(key, "tt_barrierJump") == 0) { + if (!ParseInt(source, tt_barrierJump)) return false; + } else if (std::strcmp(key, "tt_waterJump") == 0) { + if (!ParseInt(source, tt_waterJump)) return false; + } else if (std::strcmp(key, "tt_startWalkOffLedge") == 0) { + if (!ParseInt(source, tt_startWalkOffLedge)) return false; + } else if (std::strcmp(key, "tt_startLadderClimb") == 0) { + if (!ParseInt(source, tt_startLadderClimb)) return false; + } else { + source.Error("invalid token '%s'", key); + return false; + } + } + return false; +} + +bool idAAS2Settings::WriteToFileBinary(idFile& file) const { + const int storedType = static_cast(type); + if (!WriteExact(file, &storedType, 4) + || file.WriteString(fileExtensionAAS) == 0 + || file.WriteString(groupName) == 0 + || file.WriteString(explicitGroupName) == 0) { + return false; + } + const void* blocks[] = { + &boundingBox, &primitiveModeBrush, &primitiveModePatch, + &primitiveModeModel, &gravityDir, &gravityValue, &maxStepHeight, + &maxBarrierHeight, &maxWaterJumpHeight, &maxFallHeight, &minFloorCos, + &minHighCeiling, &groundSpeed, &waterSpeed, &ladderSpeed, + &wallCornerEdgeRadius, &ledgeCornerEdgeRadius, &obstaclePVSRadius, + &minCrouchingCoverHeight, &minStandingCoverHeight, + &obstaclePVSRadius, &standingFireHeight, &minWallWidth, &maxWallWidth, + &minDoorWidth, &maxDoorWidth, &coverCornerDistance, + &coverWallDistance, &chokePointWidth, &tt_barrierJump, &tt_waterJump, + &tt_startWalkOffLedge, &tt_startLadderClimb + }; + const unsigned int sizes[] = { + 24, 4, 4, 4, 12, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 + }; + for (unsigned int index = 0; index < sizeof(sizes) / sizeof(sizes[0]); ++index) { + if (!WriteExact(file, blocks[index], sizes[index])) return false; + } + return true; +} + +bool idAAS2Settings::ReadFromFileBinary(idFile& file) { + return ReadBinarySettings(file, *this, false); +} + +idAAS2File::idAAS2File() + : crc(0), timestamp(0), visitedAreas(0), settings(), major(0), minor(0), + firstFakeVertex(0), firstFakeEdge(0), firstFakeEdgeIndex(0), + firstFakeArea(0), trees(0), planes(0), vertices(0), edges(0), + edgeIndex(0), reachabilities(0), areas(0), nodes(0), portals(0), + portalIndex(0), clusters(0), obstaclePVS(0), reachabilityNames(0), + animNames(0), dependencyNames(0), interactionEntityNames(0), + traversalEntityNames(0), cover(0), areaCoverIndex(0), + touchingCoverIndex(0), chokePoints(0), traversalPoints(0), + hintNodes(0), areaBounds(0) { +} + +idAAS2File::~idAAS2File() = default; + +idResourceList* idAAS2File::GetResourceList() { return &resourceList; } + +void idAAS2File::Clear() { + crc = timestamp = 0; + firstFakeVertex = firstFakeEdge = firstFakeEdgeIndex = firstFakeArea = 0; + visitedAreas.Clear(); + trees.Clear(); planes.Clear(); vertices.Clear(); edges.Clear(); + edgeIndex.Clear(); reachabilities.Clear(); areas.Clear(); nodes.Clear(); + portals.Clear(); portalIndex.Clear(); clusters.Clear(); obstaclePVS.Clear(); + reachabilityNames.Clear(); animNames.Clear(); dependencyNames.Clear(); + interactionEntityNames.Clear(); cover.Clear(); + areaCoverIndex.Clear(); touchingCoverIndex.Clear(); chokePoints.Clear(); + traversalPoints.Clear(); hintNodes.Clear(); +} + +void idAAS2File::MakeDefault() { + Clear(); + + // This is the exact five-plane, one-area fallback BSP materialized by + // the recovered executable. Index zero is deliberately reserved in + // the edge, area, node, portal, cluster, cover, and traversal tables. + planes.Append(idPlane(0.0f, -1.0f, 0.0f, 32768.0f)); + planes.Append(idPlane(1.0f, 0.0f, 0.0f, 32768.0f)); + planes.Append(idPlane(1.0f, 0.0f, 0.0f, -32768.0f)); + planes.Append(idPlane(0.0f, -1.0f, 0.0f, -32768.0f)); + planes.Append(idPlane(0.0f, 0.0f, 1.0f, 32768.0f)); + + vertices.Append(idVec3(-32768.0f, -32768.0f, -32768.0f)); + vertices.Append(idVec3(-32768.0f, 32768.0f, -32768.0f)); + vertices.Append(idVec3(32768.0f, 32768.0f, -32768.0f)); + vertices.Append(idVec3(32768.0f, -32768.0f, -32768.0f)); + + edges.Append(aas2Edge_t{ { 0, 0 }, 0 }); + edges.Append(aas2Edge_t{ { 0, 1 }, AAS_EDGE_WALL }); + edges.Append(aas2Edge_t{ { 1, 2 }, AAS_EDGE_WALL }); + edges.Append(aas2Edge_t{ { 2, 3 }, AAS_EDGE_WALL }); + edges.Append(aas2Edge_t{ { 3, 0 }, AAS_EDGE_WALL }); + edgeIndex.Append(1); + edgeIndex.Append(2); + edgeIndex.Append(3); + edgeIndex.Append(4); + + areas.SetNum(2); + areas[0] = aas2Area_t(); + areas[1].numEdges = 4; + areas[1].firstEdge = 0; + areas[1].travelFlags = AAS_TFL_AIR; + areas[1].flags = AAS_AREA_REACHABLE_WALK | AAS_AREA_OUTSIDE + | AAS_AREA_HIGH_CEILING; + areas[1].cluster = 1; + areas[1].clusterAreaNum = 0; + areas[1].obstaclePVSOffset = 1; + areas[1].reach.Invalidate(); + areas[1].rev_reach.Invalidate(); + + nodes.Append(aas2Node_t{ 0, 0, { 0, 0 } }); + nodes.Append(aas2Node_t{ 0, 0, { 2, 0 } }); + nodes.Append(aas2Node_t{ 1, 0, { 3, 0 } }); + nodes.Append(aas2Node_t{ 2, 0, { 0, 4 } }); + nodes.Append(aas2Node_t{ 3, 0, { 0, 5 } }); + nodes.Append(aas2Node_t{ 4, 0xFFFFFFFFu, { -1, 0 } }); + + portals.Append(aas2Portal_t{}); + clusters.Append(aas2Cluster_t{ 0, 0, 0, 0 }); + clusters.Append(aas2Cluster_t{ 1, 1, 0, 0 }); + obstaclePVS.Append(1); + obstaclePVS.Append(2); + + trees.SetNum(1); + trees[0].floorNormal.Set(0.0f, 0.0f, 1.0f); + trees[0].headNode = 1; + trees[0].firstArea = 1; + trees[0].lastArea = 2; + + cover.Alloc(); + traversalPoints.Alloc(); +} + +unsigned int idAAS2File::MemorySize() const { + return 4u * (4u * (2u * reachabilities.NumAllocated() + + clusters.NumAllocated() + nodes.NumAllocated() + + planes.NumAllocated()) + + 3u * (portals.NumAllocated() + edges.NumAllocated() + + vertices.NumAllocated()) + + 14u * cover.NumAllocated() + 10u * areas.NumAllocated() + + touchingCoverIndex.NumAllocated() + areaCoverIndex.NumAllocated() + + portalIndex.NumAllocated() + edgeIndex.NumAllocated() + 67u) + + obstaclePVS.NumAllocated(); +} + +bool idAAS2File::LoadBinary(const char* const fileName, + const char* const binaryFileName, + const std::uint32_t mapFileCRC, + const std::uint32_t sourceTimestamp) { + idFileLocal file(fileSystem->OpenFileRead(binaryFileName, true, false)); + if (file.file == nullptr) return false; + + std::uint32_t magic = 0; + std::uint8_t version[3] = {}; + std::uint32_t storedTimestamp = 0; + std::uint32_t storedCRC = 0; + if (!ReadExact(*file.file, &magic, 4)) return false; + const bool byteSwap = magic != AAS2_FILE_ID_BINARY + && ByteSwap32(magic) == AAS2_FILE_ID_BINARY; + if ((!byteSwap && magic != AAS2_FILE_ID_BINARY) + || !ReadExact(*file.file, version, 3) + || version[0] != AAS2_BINARY_MAJOR + || version[1] != AAS2_BINARY_MINOR + || version[2] != AAS2_BINARY_REVISION + || !ReadExact(*file.file, &storedTimestamp, 4)) { + return false; + } + if (byteSwap) Swap(storedTimestamp); + if ((sourceTimestamp != 0 && sourceTimestamp != std::uint32_t(-1) + && sourceTimestamp != storedTimestamp) + || !ReadExact(*file.file, &storedCRC, 4)) return false; + if (byteSwap) Swap(storedCRC); + if (storedCRC != mapFileCRC) return false; + + Clear(); + major = AAS2_BINARY_MAJOR; + minor = AAS2_BINARY_MINOR; + timestamp = sourceTimestamp; + crc = mapFileCRC; + if (!ReadExact(*file.file, &firstFakeVertex, 4) + || !ReadExact(*file.file, &firstFakeEdge, 4) + || !ReadExact(*file.file, &firstFakeEdgeIndex, 4) + || !ReadExact(*file.file, &firstFakeArea, 4)) { + return false; + } + if (byteSwap) { + Swap(firstFakeVertex); Swap(firstFakeEdge); + Swap(firstFakeEdgeIndex); Swap(firstFakeArea); + } + if (!ReadBinarySettings(*file.file, settings, byteSwap) + || !ReadList(*file.file, planes, byteSwap) + || !ReadList(*file.file, vertices, byteSwap) + || !ReadList(*file.file, edges, byteSwap) + || !ReadList(*file.file, edgeIndex, byteSwap) + || !ReadList(*file.file, reachabilities, byteSwap) + || !ReadList(*file.file, areas, byteSwap) + || !ReadList(*file.file, nodes, byteSwap) + || !ReadList(*file.file, portals, byteSwap) + || !ReadList(*file.file, portalIndex, byteSwap) + || !ReadList(*file.file, clusters, byteSwap) + || !ReadList(*file.file, obstaclePVS, byteSwap) + || !ReadList(*file.file, reachabilityNames, byteSwap) + || !ReadList(*file.file, animNames, byteSwap) + || !ReadList(*file.file, dependencyNames, byteSwap) + || !ReadList(*file.file, interactionEntityNames, byteSwap) + || !ReadList(*file.file, traversalEntityNames, byteSwap) + || !ReadList(*file.file, cover, byteSwap) + || !ReadList(*file.file, areaCoverIndex, byteSwap) + || !ReadList(*file.file, touchingCoverIndex, byteSwap) + || !ReadList(*file.file, traversalPoints, byteSwap) + || !ReadList(*file.file, hintNodes, byteSwap) + || !ReadList(*file.file, trees, byteSwap) + || !ReadList(*file.file, areaBounds, byteSwap)) { + Clear(); + return false; + } + FlagNoPushAreas(); + for (int tree = 0; tree < trees.Num(); ++tree) { + if (MaxTreeDepth(tree) > 128) { + MakeDefault(); + return false; + } + } + + // Shipping-data corrections present verbatim in the recovered loader. + // They are keyed by both map name and the complete area-table checksum, + // so unrelated or regenerated AAS data cannot accidentally receive them. + const std::uint32_t areaChecksum = CanonicalXboxAreaChecksum(areas); + if (fileName != nullptr && std::strstr(fileName, "ghost_hideout") != nullptr + && areaChecksum == 0xF7C91972u && areas.Num() > 1271) { + areas[1271].travelFlags |= AAS_TFL_INVALID; + } else if (fileName != nullptr + && std::strstr(fileName, "wellspring_coop") != nullptr + && areaChecksum == 0x2A1D82D3u && areas.Num() > 79) { + areas[79].flags |= AAS_AREA_NOPUSH; + } + return true; +} + +bool idAAS2File::WriteBinary(const char*, const char* const binaryFileName, + const std::uint32_t mapFileCRC, + const std::uint32_t sourceTimestamp) { + ResetCover(); + idFileLocal file(fileSystem->OpenFileWrite(binaryFileName, FSPATH_BASE)); + if (file.file == nullptr) return false; + const std::uint8_t version[] = { + AAS2_BINARY_MAJOR, AAS2_BINARY_MINOR, AAS2_BINARY_REVISION + }; + return WriteExact(*file.file, &AAS2_FILE_ID_BINARY, 4) + && WriteExact(*file.file, version, 3) + && WriteExact(*file.file, &sourceTimestamp, 4) + && WriteExact(*file.file, &mapFileCRC, 4) + && WriteExact(*file.file, &firstFakeVertex, 4) + && WriteExact(*file.file, &firstFakeEdge, 4) + && WriteExact(*file.file, &firstFakeEdgeIndex, 4) + && WriteExact(*file.file, &firstFakeArea, 4) + && settings.WriteToFileBinary(*file.file) + && WriteList(*file.file, planes) + && WriteList(*file.file, vertices) + && WriteList(*file.file, edges) + && WriteList(*file.file, edgeIndex) + && WriteList(*file.file, reachabilities) + && WriteList(*file.file, areas) + && WriteList(*file.file, nodes) + && WriteList(*file.file, portals) + && WriteList(*file.file, portalIndex) + && WriteList(*file.file, clusters) + && WriteList(*file.file, obstaclePVS) + && WriteList(*file.file, reachabilityNames) + && WriteList(*file.file, animNames) + && WriteList(*file.file, dependencyNames) + && WriteList(*file.file, interactionEntityNames) + && WriteList(*file.file, traversalEntityNames) + && WriteList(*file.file, cover) + && WriteList(*file.file, areaCoverIndex) + && WriteList(*file.file, touchingCoverIndex) + && WriteList(*file.file, traversalPoints) + && WriteList(*file.file, hintNodes) + && WriteList(*file.file, trees) + && WriteList(*file.file, areaBounds); +} + +void idAAS2File::LoadResource() { + idStr binaryName(GetName()); + const char* dot = std::strrchr(GetName(), '.'); + const char* extension = dot != nullptr ? dot + 1 : "aas"; + char binaryPath[256] = {}; + idStr binaryExtension("b"); + binaryExtension.Append(extension); + fileSystem->FixLongFilename("generated", binaryExtension.c_str(), + GetName(), binaryPath, sizeof(binaryPath)); + const std::uint32_t sourceTime = fileSystem->GetTimestamp(GetName(), false); + if (LoadBinary(GetName(), binaryPath, 0, sourceTime)) { + resourceError = nullptr; + return; + } + if (!LoadText(GetName(), sourceTime)) { + resourceError = "AAS source and generated binary unavailable"; + MakeDefault(); + return; + } + WriteBinary(GetName(), binaryPath, 0, sourceTime); + FlagNoPushAreas(); + for (int tree = 0; tree < trees.Num(); ++tree) { + if (MaxTreeDepth(tree) > 128) { + resourceError = "AAS BSP exceeds recovered maximum depth"; + MakeDefault(); + return; + } + } + resourceError = nullptr; +} + +bool idAAS2File::ReloadIfStale() { + const std::uint32_t current = fileSystem->GetTimestamp(GetName(), false); + if (current == timestamp) return false; + const char* dot = std::strrchr(GetName(), '.'); + const char* extension = dot != nullptr ? dot + 1 : "aas"; + idStr binaryExtension("b"); + binaryExtension.Append(extension); + char binaryPath[256] = {}; + fileSystem->FixLongFilename("generated", binaryExtension.c_str(), + GetName(), binaryPath, sizeof(binaryPath)); + fileSystem->RemoveFile(binaryPath, FSPATH_BASE); + LoadResource(); + return true; +} + +bool idAAS2File::GetAASAnim( + const idIndex index, + const aas2AnimName_t** const value) const { + const int number = index.Get(); + if (number < 0 || number >= animNames.Num()) { + *value = nullptr; + return false; + } + *value = &animNames[number]; + return true; +} + +idIndex +idAAS2File::GetAASAnimIndexByName(const char* const name) const { + return FindNameIndex>(animNames, name); +} + +bool idAAS2File::GetAASDependency( + const idIndex index, + const aas2DependencyName_t** const value) const { + const int number = index.Get(); + if (number < 0 || number >= dependencyNames.Num()) { + *value = nullptr; + return false; + } + *value = &dependencyNames[number]; + return true; +} + +idIndex +idAAS2File::GetAASDependencyIndexByName(const char* const name) const { + return FindNameIndex>( + dependencyNames, name); +} + +bool idAAS2File::GetAASInteractionEntity( + const idIndex index, + const aas2InteractionEntityName_t** const value) const { + const int number = index.Get(); + if (number < 0 || number >= interactionEntityNames.Num()) { + *value = nullptr; + return false; + } + *value = &interactionEntityNames[number]; + return true; +} + +idIndex +idAAS2File::GetAASInteractionEntityIndexByName(const char* const name) const { + return FindNameIndex>( + interactionEntityNames, name); +} + +bool idAAS2File::GetAASTraversalNameIndex( + const idIndex index, + const aas2TraversalEntityName_t** const value) const { + const int number = index.Get(); + if (number < 0 || number >= traversalEntityNames.Num()) { + *value = nullptr; + return false; + } + *value = &traversalEntityNames[number]; + return true; +} + +idIndex +idAAS2File::GetAASTraversalNameIndexByName(const char* const name) const { + return FindNameIndex>( + traversalEntityNames, name); +} + +int idAAS2File::GetAASTraversalIndexByNameIndex( + const idIndex index) const { + if (index.Get() < 0) return 0; + for (int traversal = 1; traversal < traversalPoints.Num(); ++traversal) { + if (traversalPoints[traversal].traversalNameIndex == index) { + return traversal; + } + } + return 0; +} + +bool idAAS2File::SetTraversalFlag(const int index, const int flags) { + if (index <= 0 || index >= traversalPoints.Num()) return false; + aas2Traversal_t& traversal = traversalPoints[index]; + const std::uint32_t changed = traversal.flags + | static_cast(flags); + if (changed == traversal.flags) return false; + traversal.flags = changed; + const int reachability = traversal.reachabilityIndex.Get(); + if ((changed & 1u) == 0 && reachability >= 0 + && reachability < reachabilities.Num()) { + reachabilities[reachability].travelFlags &= ~std::uint32_t(AAS_TFL_INVALID); + } + return true; +} + +bool idAAS2File::ClearTraversalFlag(const int index, const int flags) { + if (index <= 0 || index >= traversalPoints.Num()) return false; + aas2Traversal_t& traversal = traversalPoints[index]; + const std::uint32_t changed = traversal.flags + & ~static_cast(flags); + if (changed == traversal.flags) return false; + traversal.flags = changed; + const int reachability = traversal.reachabilityIndex.Get(); + if ((changed & 1u) == 0 && reachability >= 0 + && reachability < reachabilities.Num()) { + reachabilities[reachability].travelFlags |= AAS_TFL_INVALID; + } + return true; +} + +bool idAAS2File::GetAASTraversalAreas(const int index, int& startArea, + int& goalArea) const { + if (index <= 0 || index >= traversalPoints.Num()) return false; + startArea = traversalPoints[index].startAreaNum; + goalArea = traversalPoints[index].endAreaNum; + return true; +} + +idIndex +idAAS2File::FindReachabilityByName(const char* const name) const { + for (int index = 0; index < reachabilityNames.Num(); ++index) { + if (_stricmp(name != nullptr ? name : "", reachabilityNames[index].name) == 0) { + return idIndex( + static_cast(reachabilityNames[index].index)); + } + } + return idIndex(); +} + +int idAAS2File::GetTraversalsForReachability( + const idIndex reachIndex, + idList& traversals) const { + traversals.Clear(); + const int number = reachIndex.Get(); + if (number < 0 || number >= reachabilities.Num()) return 0; + const aas2Reachability_t& reachability = reachabilities[number]; + if (!IsValidArea(areas, reachability.fromAreaNum)) return 0; + const aas2Area_t& area = areas[reachability.fromAreaNum]; + for (int index = area.firstTraversal; + index < area.firstTraversal + area.numTraversals + && index < traversalPoints.Num(); ++index) { + const aas2Traversal_t& traversal = traversalPoints[index]; + if (traversal.startAreaNum == reachability.fromAreaNum + && traversal.endAreaNum == reachability.toAreaNum) { + traversals.Append(index); + } + } + return traversals.Num(); +} + +int idAAS2File::GetTraversalsForInteractableEntity( + const idIndex entityIndex, + idList& traversals) const { + traversals.Clear(); + for (int index = 0; index < traversalPoints.Num(); ++index) { + if (traversalPoints[index].interactionEntIndex == entityIndex) { + traversals.Append(index); + } + } + return traversals.Num(); +} + +void idAAS2File::ResetCover() { + for (int index = 0; index < cover.Num(); ++index) { + cover[index].usableTime = 0; + cover[index].reservedBy = 0x1FFF; + } +} + +idIndex idAAS2File::GetTreeForFloorNormal( + const idVec3& normal) const { + float bestDot = -2.0f; + int bestTree = -1; + for (int tree = 0; tree < trees.Num(); ++tree) { + const float dot = trees[tree].floorNormal.Dot(normal); + if (dot > 0.70710677f && dot > bestDot) { + bestDot = dot; + bestTree = tree; + } + } + return idIndex(bestTree); +} + +idIndex idAAS2File::GetTreeForArea( + const int areaNum) const { + for (int tree = 0; tree < trees.Num(); ++tree) { + if (areaNum >= trees[tree].firstArea && areaNum < trees[tree].lastArea) { + return idIndex(tree); + } + } + return idIndex(0); +} + +int idAAS2File::GetNumAreasInTree( + const idIndex treeNum) const { + const int index = treeNum.Get(); + return index >= 0 && index < trees.Num() + ? trees[index].lastArea - trees[index].firstArea : 0; +} + +const idVec3* idAAS2File::GetFloorNormalForTree( + const idIndex treeNum) const { + const int index = treeNum.Get(); + return index >= 0 && index < trees.Num() ? &trees[index].floorNormal : nullptr; +} + +const idVec3* idAAS2File::GetFloorNormalForArea(const int areaNum) const { + return GetFloorNormalForTree(GetTreeForArea(areaNum)); +} + +void idAAS2File::FloodAddVisitedArea(const int areaNum) { + if (areaNum < 0 || areaNum >= areas.Num()) return; + visitedAreas.Append(areaNum); + areas[areaNum].flags |= AAS_AREA_FLOOD_VISITED; +} + +void idAAS2File::FloodClearVisitedAreas(const int offset) { + const int first = (std::max)(0, offset); + for (int index = first; index < visitedAreas.Num(); ++index) { + const int areaNum = visitedAreas[index]; + if (areaNum >= 0 && areaNum < areas.Num()) { + areas[areaNum].flags &= ~AAS_AREA_FLOOD_VISITED; + } + } + visitedAreas.SetNum((std::min)(first, visitedAreas.Num())); +} + +void idAAS2File::FlagNoPushAreas() { + for (int areaNum = 1; areaNum < areas.Num(); ++areaNum) { + if (areas[areaNum].numEdges <= 0) continue; + idVec3 center = AreaCenter(areaNum); + if (PushPointIntoAreaNum(areaNum, center)) { + areas[areaNum].flags |= AAS_AREA_NOPUSH; + } + } +} + +void idAAS2File::MaxTreeDepth_r(const int nodeNum, const int depth, + int& maximumDepth) const { + maximumDepth = (std::max)(maximumDepth, depth); + if (nodeNum <= 0 || nodeNum >= nodes.Num() || depth > 256) return; + MaxTreeDepth_r(nodes[nodeNum].children[0], depth + 1, maximumDepth); + MaxTreeDepth_r(nodes[nodeNum].children[1], depth + 1, maximumDepth); +} + +int idAAS2File::MaxTreeDepth(const int tree) const { + if (tree < 0 || tree >= trees.Num()) return 0; + int maximumDepth = 0; + MaxTreeDepth_r(trees[tree].headNode, 0, maximumDepth); + return maximumDepth; +} diff --git a/source/engine/aas2file/aas2file.h b/source/engine/aas2file/aas2file.h index 31cfad9..40bf3fa 100644 --- a/source/engine/aas2file/aas2file.h +++ b/source/engine/aas2file/aas2file.h @@ -1,94 +1,475 @@ #pragma once -// Reconstructed C++ declarations from IDA Local Types and PDB/DIA metadata. -// Original PDB header: w:\tech5\engine\aas2file\aas2file.h -// Recovered logical types: 5 -// Signatures retain Xbox 360 ABI evidence and may still require manual review. +#include "aas2file/aastraversalchaindata.h" +#include "framework/resource.h" +#include "idlib/bv/bounds.h" +#include "idlib/containers/list.h" +#include "idlib/math/plane.h" +#include "idlib/text/str.h" +#include -// IDA Local Type ordinal 21931; PDB kind: struct. -struct idAAS2File::bspTree_t -{ - idVec3 floorNormal; - int headNode; - int firstArea; - int lastArea; +class idFile; +class idLexer; + +enum aas2TravelFlag_t : int { + AAS_TFL_INVALID = 0x1, + AAS_TFL_INVALID_TEAM1 = 0x2, + AAS_TFL_INVALID_TEAM2 = 0x4, + AAS_TFL_AIR = 0x8, + AAS_TFL_WATER = 0x10, + AAS_TFL_WALK = 0x20, + AAS_TFL_WALKOFFLEDGE = 0x40, + AAS_TFL_WALKOFFBARRIER = 0x80, + AAS_TFL_BARRIERJUMP = 0x100, + AAS_TFL_JUMP = 0x200, + AAS_TFL_LADDER = 0x400, + AAS_TFL_SWIM = 0x800, + AAS_TFL_TELEPORT = 0x1000, + AAS_TFL_ELEVATOR = 0x2000, + AAS_TFL_CROUCH = 0x4000, + AAS_TFL_LEAP = 0x8000, + AAS_TFL_TRAVERSAL_CLASS_A = 0x10000, + AAS_TFL_TRAVERSAL_CLASS_B = 0x20000, + AAS_TFL_TRAVERSAL_CLASS_C = 0x40000, + AAS_TFL_TRAVERSAL_EVASION = 0x80000, + AAS_TFL_CLIMB = 0x100000, + AAS_TFL_FLY = 0x200000, + AAS_TFL_TRAVERSAL_EMERGENCY = 0x400000, + AAS_TFL_TRAVERSAL_CLASS_D = 0x800000, + AAS_TFL_TRAVERSAL_CLASS_E = 0x1000000, + AAS_TFL_TRAVERSAL_COMBAT = 0x2000000, + AAS_TFL_BLOCK_PLAYER = 0x4000000, + AAS_TFL_MELEE = 0x8000000, + AAS_TFL_MAX = 0x8000001, + AAS_TFL_VALID_TEAM1 = static_cast(0xFFFFFFFCu), + AAS_TFL_VALID_TEAM2 = static_cast(0xFFFFFFFAu), + AAS_TFL_VALID_BOTH_TEAMS = static_cast(0xFFFFFFFEu), + AAS_TFL_VALID_WALK_TEAM1 = 0x3C, + AAS_TFL_VALID_WALK_TEAM2 = 0x3A, + AAS_TFL_VALID_WALK_BOTH_TEAMS = 0x3E, + AAS_TFL_TRAVERSAL_ANY_CLASS = 0x1870000 }; -// IDA Local Type ordinal 22000; PDB kind: class. -class idAAS2File : public idResource -{ +enum aas2AreaFlag_t : int { + AAS_AREA_LEDGE = 0x1, + AAS_AREA_REACHABLE_WALK = 0x2, + AAS_AREA_OUTSIDE = 0x4, + AAS_AREA_HIGH_CEILING = 0x8, + AAS_AREA_NOPUSH = 0x10, + AAS_AREA_CONTENTS_SOLID = 0x100, + AAS_AREA_CONTENTS_WATER = 0x200, + AAS_AREA_CONTENTS_CLUSTERPORTAL = 0x400, + AAS_AREA_CONTENTS_OBSTACLE = 0x800, + AAS_AREA_FAKE = 0x1000, + AAS_AREA_CONTENTS_FLY = 0x2000, + AAS_AREA_FLOOD_VISITED = 0x8000, + AAS_AREA_MAX = 0x8001 +}; + +enum aas2EdgeFlag_t : int { + AAS_EDGE_WALL = 0x1, + AAS_EDGE_LEDGE = 0x2, + AAS_EDGE_WALL_CORNER = 0x4, + AAS_EDGE_LEDGE_CORNER = 0x8, + AAS_EDGE_STEP_TOP = 0x10, + AAS_EDGE_STEP_BOTTOM = 0x20, + AAS_EDGE_VERTICAL = 0x40, + AAS_EDGE_WATER = 0x80, + AAS_EDGE_LADDER = 0x100, + AAS_EDGE_STEP_SIDE = 0x200, + AAS_EDGE_WALL_CORNER_V0 = 0x400, + AAS_EDGE_WALL_CORNER_V1 = 0x800, + AAS_EDGE_FAKE = 0x1000, + AAS_EDGE_MAX = 0x1001 +}; + +enum aas2CoverFlag_t : int { + AAS_COVERFLAG_CROUCH = 0x1, + AAS_COVERFLAG_LEANLEFT = 0x2, + AAS_COVERFLAG_LEANRIGHT = 0x4, + AAS_COVERFLAG_STEPLEFT = 0x8, + AAS_COVERFLAG_STEPRIGHT = 0x10, + AAS_COVERFLAG_FIREOVER = 0x20, + AAS_COVERFLAG_FIRELEANLEFT = 0x40, + AAS_COVERFLAG_FIRELEANRIGHT = 0x80, + AAS_COVERFLAG_FIRESTEPLEFT = 0x100, + AAS_COVERFLAG_FIRESTEPRIGHT = 0x200, + AAS_COVERFLAG_EXPOSED = 0x400, + AAS_COVERFLAG_EXPLICIT = 0x800, + AAS_COVERFLAG_FORCE = 0x1000, + AAS_COVERFLAG_INVALID = 0x2000, + AAS_COVERFLAG_TRANSITION_APPROACH_FORWARD = 0x4000, + AAS_COVERFLAG_TRANSITION_APPROACH_LEFT = 0x8000, + AAS_COVERFLAG_TRANSITION_APPROACH_TURN_AROUND_LEFT = 0x10000, + AAS_COVERFLAG_TRANSITION_APPROACH_WRAP_AROUND_LEFT = 0x20000, + AAS_COVERFLAG_TRANSITION_APPROACH_RIGHT = 0x40000, + AAS_COVERFLAG_TRANSITION_APPROACH_TURN_AROUND_RIGHT = 0x80000, + AAS_COVERFLAG_TRANSITION_APPROACH_WRAP_AROUND_RIGHT = 0x100000, + AAS_COVERFLAG_TRANSITION_APPROACH_JUMPOVER = 0x200000, + AAS_COVERFLAG_AVOID = 0x400000, + AAS_COVERFLAG_TRANSITION_ALL = 0x3FC000 +}; + +class idAAS2Settings { public: - // Recovered virtual interface; IDA vtable ordinal 22001. - virtual ~idAAS2File(); - virtual void LoadResource(); - virtual bool ReloadIfStale(); - virtual void WriteResourceFile(); - virtual idResourceList *GetResourceList(); - virtual void Print(); - virtual void List(); + enum type_t : int { AAS_PLAYER = 0, AAS_MONSTER = 1, AAS_VEHICLE = 2, AAS_MAX = 3 }; + enum aasPrimitiveMode_t : int { + AAS_PRIMITIVE_MODE_DEFAULT = 0, + AAS_PRIMITIVE_MODE_NEVER = 1, + AAS_PRIMITIVE_MODE_ALWAYS = 2 + }; - unsigned int crc; - unsigned int timestamp; - idList visitedAreas; - idAAS2Settings settings; - int major; - int minor; - int firstFakeVertex; - int firstFakeEdge; - int firstFakeEdgeIndex; - int firstFakeArea; - idList trees; - idList planes; - idList vertices; - idList edges; - idList edgeIndex; - idList reachabilities; - idList areas; - idList nodes; - idList portals; - idList portalIndex; - idList clusters; - idList obstaclePVS; - idList reachabilityNames; - idList animNames; - idList dependencyNames; - idList interactionEntityNames; - idList traversalEntityNames; - idList cover; - idList areaCoverIndex; - idList touchingCoverIndex; - idList chokePoints; - idList traversalPoints; - idList hintNodes; - idList areaBounds; + idAAS2Settings(); + bool ParseInt(idLexer& source, int& value); + bool ParseFloat(idLexer& source, float& value); + bool ParseVector(idLexer& source, idVec3& vector); + bool ParseBounds(idLexer& source, idBounds& bounds); + bool ValidForBounds(const idBounds& bounds) const; + bool ReadFromFile(idLexer& source); + bool WriteToFileBinary(idFile& file) const; + bool ReadFromFileBinary(idFile& file); + + type_t type; + idStr fileExtensionAAS; + idStr groupName; + idStr explicitGroupName; + idBounds boundingBox; + int primitiveModeBrush; + int primitiveModePatch; + int primitiveModeModel; + idVec3 gravityDir; + float gravityValue; + float maxStepHeight; + float maxBarrierHeight; + float maxWaterJumpHeight; + float maxFallHeight; + float minFloorCos; + float minHighCeiling; + float groundSpeed; + float waterSpeed; + float ladderSpeed; + float wallCornerEdgeRadius; + float ledgeCornerEdgeRadius; + float obstaclePVSRadius; + float wallCornerReachabilityBackoff; + float highQualityReachabilityBackoff; + float minCrouchingCoverHeight; + float minStandingCoverHeight; + float crouchingFireHeight; + float standingFireHeight; + float minWallWidth; + float maxWallWidth; + float minDoorWidth; + float maxDoorWidth; + float coverCornerDistance; + float coverWallDistance; + float chokePointWidth; + int tt_barrierJump; + int tt_waterJump; + int tt_startWalkOffLedge; + int tt_startLadderClimb; }; -// IDA Local Type ordinal 22021; PDB kind: struct. -struct idAAS2File::bestReachableArea_t -{ - float bboxHeight; - float maxEdgeDist; - int areaFlags; - int excludeTravelFlags; - int pointAreaNum; - float pointAreaFloorDist; - int boundsAreaNum; - float boundsAreaFloorDist; +struct aas2Edge_t { int vertexNum[2]; int flags; }; +struct aas2Node_t { std::uint32_t planeNum; std::uint32_t flags; int children[2]; }; +struct aas2Portal_t { + std::uint16_t areaNum; + std::int16_t clusters[2]; + std::uint16_t clusterAreaNum[2]; + std::uint16_t maxAreaTravelTime; +}; +struct aas2Cluster_t { int numAreas; int numReachableAreas; int numPortals; int firstPortal; }; +struct aas2Name_t { char name[128]; int index; }; +struct aas2AnimName_t { char name[128]; }; +struct aas2DependencyName_t { char name[128]; }; +struct aas2InteractionEntityName_t { char name[128]; }; +struct aas2TraversalEntityName_t { char name[128]; }; +struct aas2AreaBounds_t { std::int16_t min[3]; std::int16_t max[3]; }; + +struct aas2Reachability_t { + std::uint32_t travelFlags; + std::uint16_t travelTime; + std::uint16_t fromAreaNum; + std::uint16_t toAreaNum; + std::uint16_t padding; + std::int16_t start[3]; + std::int16_t end[3]; + std::uint32_t areaTTOfsAndNumber; + idIndex next; + idIndex rev_next; + + idVec3 Start() const { return idVec3(float(start[0]), float(start[1]), float(start[2])); } + idVec3 End() const { return idVec3(float(end[0]), float(end[1]), float(end[2])); } }; -// IDA Local Type ordinal 22022; PDB kind: struct. -struct idAAS2File::floorEdgeSplitPoint_t -{ - idVec3 point; - float dist; - int edgeNum; +struct aas2Area_t { + std::uint32_t travelFlags; + std::uint16_t flags; + std::int16_t numEdges; + int firstEdge; + std::int16_t cluster; + std::uint16_t clusterAreaNum; + std::uint32_t obstaclePVSOffset; + idIndex reach; + idIndex rev_reach; + std::uint16_t firstChokePoint; + std::int16_t numChokePoints; + std::uint16_t firstCover; + std::uint16_t numCover; + std::uint16_t firstTraversal; + std::uint16_t numTraversals; + std::uint16_t firstHintNode; + std::uint16_t numHintNodes; + + aas2Area_t(); }; -// IDA Local Type ordinal 23832; PDB kind: struct. -struct idAAS2File::GetObstaclePVSWallEdges::__l25::wallEdge_t -{ - int edgeNum; - int verts[2]; - idAAS2File::GetObstaclePVSWallEdges::__l25::wallEdge_t *next; +struct aas2Cover_t { + idVec3 origin; + idVec3 dir; + std::int16_t areaNum; + std::int16_t flags; + int numTouching; + int firstTouching; + float durationSec; + float minRange; + float maxRange; + int reservedBy; + int usableTime; + + aas2Cover_t(); }; + +struct aas2ChokePoint_t { idVec3 points[2]; std::uint16_t rooms[2]; }; + +struct aas2HintNode_t { + enum hintNodeType_t : int { + HINT_NODE_TYPE_SEARCH_AUTO = 0, + HINT_NODE_TYPE_SEARCH_ANIMATION = 1, + HINT_NODE_TYPE_GRENADE = 2 + }; + + idVec3 origin; + std::int16_t areaNum; + std::int16_t radius; + std::uint8_t hintType; + std::uint8_t orientation; + std::uint8_t dirFlags; + std::uint8_t grouping; + int hintData; + + aas2HintNode_t(); + void GetOrientationVector(idVec3& direction) const; +}; + +struct aas2Trace_t { + int flags; + int travelFlags; + int maxAreas; + int getOutOfSolid; + float fraction; + idVec3 endpos; + int planeNum; + int lastAreaNum; + int blockingAreaNum; + int numAreas; + int* areas; + idVec3* points; +}; +struct aas2TraceHeight_t { int maxPoints; int numPoints; idVec3* points; }; +struct aas2EdgeCrossed_t { int toAreaNum; int edgeNum; idVec3 edgePoint; }; +struct aas2TraceFloor_t { + float fraction; + idVec3 endpos; + int lastAreaNum; + aas2EdgeCrossed_t firstEdge; + aas2EdgeCrossed_t lastEdge; + int maxAreas; + int numAreas; + int maxReachIndices; + int numReachIndices; + int* areas; + idIndex* reachIndices; +}; + +class idAAS2File : public idResource { +public: + struct bspTree_t { idVec3 floorNormal; int headNode; int firstArea; int lastArea; }; + struct bestReachableArea_t { + float bboxHeight; + float maxEdgeDist; + int areaFlags; + int excludeTravelFlags; + int pointAreaNum; + float pointAreaFloorDist; + int boundsAreaNum; + float boundsAreaFloorDist; + }; + struct floorEdgeSplitPoint_t { idVec3 point; float dist; int edgeNum; }; + + idAAS2File(); + ~idAAS2File() override; + void LoadResource() override; + bool ReloadIfStale() override; + idResourceList* GetResourceList() override; + + void Clear(); + void MakeDefault(); + unsigned int MemorySize() const; + bool LoadBinary(const char* fileName, const char* binaryFileName, + std::uint32_t mapFileCRC, std::uint32_t sourceTimestamp); + bool WriteBinary(const char* fileName, const char* binaryFileName, + std::uint32_t mapFileCRC, std::uint32_t sourceTimestamp); + + bool GetAASAnim(idIndex index, + const aas2AnimName_t** value) const; + idIndex GetAASAnimIndexByName(const char* name) const; + bool GetAASDependency(idIndex index, + const aas2DependencyName_t** value) const; + idIndex GetAASDependencyIndexByName(const char* name) const; + bool GetAASInteractionEntity(idIndex index, + const aas2InteractionEntityName_t** value) const; + idIndex GetAASInteractionEntityIndexByName(const char* name) const; + bool GetAASTraversalNameIndex(idIndex index, + const aas2TraversalEntityName_t** value) const; + idIndex GetAASTraversalNameIndexByName(const char* name) const; + int GetAASTraversalIndexByNameIndex(idIndex index) const; + bool SetTraversalFlag(int index, int flags); + bool ClearTraversalFlag(int index, int flags); + bool GetAASTraversalAreas(int index, int& startArea, int& goalArea) const; + idIndex FindReachabilityByName(const char* name) const; + int GetTraversalsForReachability(idIndex reachIndex, + idList& traversals) const; + int GetTraversalsForInteractableEntity(idIndex index, + idList& traversals) const; + + void FlagNoPushAreas(); + void ResetCover(); + idIndex GetTreeForFloorNormal(const idVec3& normal) const; + idIndex GetTreeForArea(int areaNum) const; + int GetNumAreasInTree(idIndex treeNum) const; + const idVec3* GetFloorNormalForArea(int areaNum) const; + const idVec3* GetFloorNormalForTree(idIndex treeNum) const; + + idVec3 AreaCenter(int areaNum) const; + idBounds EdgeBounds(int edgeNum) const; + idBounds AreaBounds(int areaNum) const; + int PointAreaNum(int tree, const idVec3& origin) const; + int BoundsAreaNums(int tree, const idBounds& bounds, int* outputAreas, + int maxAreas) const; + void FlagBoundsAreas(int tree, const idBounds& bounds, + bool* areasTouched) const; + int BoundsReachableAreaNum(int tree, const idBounds& bounds, + std::uint16_t areaFlags, int excludeTravelFlags) const; + bool TraceHeight(int tree, aas2TraceHeight_t& trace, + const idVec3& start, const idVec3& end) const; + bool GetFloorEdgeSplitPoints(floorEdgeSplitPoint_t& nearest, + floorEdgeSplitPoint_t& furthest, int areaNum, + const idPlane& pathPlane, const idPlane& nearPlane) const; + int MaxTreeDepth(int tree) const; + bool Trace(int tree, aas2Trace_t& trace, const idVec3& start, + const idVec3& end) const; + int GetObstaclePVSWallEdges(int areaNum, int edgeFlags, int* outputEdges, + int maxEdges) const; + void ClipGridToAreas(const idVec3& startOrigin, int startAreaNum, + int travelFlags, unsigned int cellSize, int dimension, + std::uint8_t* reachable) const; + float GetFloorDistance(int areaNum, const idPlane& floorPlane, + const idVec3& point, float bboxHeight, float maxEdgeDist) const; + int PointReachableAreaNum(idIndex tree, + const idVec3& origin, int areaFlags, int excludeTravelFlags) const; + bool PushPointIntoAreaNum(int areaNum, idVec3& point) const; + bool TraceFloor(aas2TraceFloor_t& trace, const idVec3& start, + int startAreaNum, const idVec3& end, int endAreaNum, int travelFlags, + bool allowFloorNormalChange, bool ignoreGravityDirectionDistance, + bool ignoreSameArea) const; + + void FloodAddVisitedArea(int areaNum); + void FloodClearVisitedAreas(int offset); + + static idResourceList resourceList; + + std::uint32_t crc; + std::uint32_t timestamp; + idList visitedAreas; + idAAS2Settings settings; + int major; + int minor; + int firstFakeVertex; + int firstFakeEdge; + int firstFakeEdgeIndex; + int firstFakeArea; + idList trees; + idList planes; + idList vertices; + idList edges; + idList edgeIndex; + idList reachabilities; + idList areas; + idList nodes; + idList portals; + idList portalIndex; + idList clusters; + idList obstaclePVS; + idList reachabilityNames; + idList animNames; + idList dependencyNames; + idList interactionEntityNames; + idList traversalEntityNames; + idList cover; + idList areaCoverIndex; + idList touchingCoverIndex; + idList chokePoints; + idList traversalPoints; + idList hintNodes; + idList areaBounds; + +private: + bool LoadText(const char* fileName, std::uint32_t sourceTimestamp); + bool ParseIndex(idLexer& source, idList& indexes); + bool ParseNames(idLexer& source, idList& names); + template + bool ParseInteractionEntityNames(idLexer& source, + idList& names); + bool ParseVertices(idLexer& source); + bool ParseEdges(idLexer& source); + bool ParseNodes(idLexer& source); + bool ParsePortals(idLexer& source); + bool ParseClusters(idLexer& source); + bool ParseObstaclePVS(idLexer& source); + bool ParsePlanes(idLexer& source); + bool ParseReachabilities(idLexer& source); + bool ParseAreas(idLexer& source); + bool ParseCover(idLexer& source); + bool ParseTraversalPoints(idLexer& source); + bool ParseHintNodes(idLexer& source); + bool ParseTrees(idLexer& source); + void CalculateAreaBounds(); + void BuildReachabilityChains(); + void PointBestReachableAreaNum(int tree, const idVec3& origin, + bestReachableArea_t& bestArea) const; + void BoundsBestReachableAreaNum(int tree, const idBounds& bounds, + const idVec3& origin, bestReachableArea_t& bestArea) const; + void MaxTreeDepth_r(int nodeNum, int depth, int& maxDepth) const; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idAAS2Settings) == 268, + "Recovered idAAS2Settings ABI changed"); +static_assert(sizeof(aas2Edge_t) == 12, "Recovered aas2Edge_t ABI changed"); +static_assert(sizeof(aas2Node_t) == 16, "Recovered aas2Node_t ABI changed"); +static_assert(sizeof(aas2Portal_t) == 12, "Recovered aas2Portal_t ABI changed"); +static_assert(sizeof(aas2Reachability_t) == 32, + "Recovered aas2Reachability_t ABI changed"); +static_assert(sizeof(aas2Area_t) == 40, "Recovered aas2Area_t ABI changed"); +static_assert(sizeof(aas2Cover_t) == 56, "Recovered aas2Cover_t ABI changed"); +static_assert(sizeof(aas2ChokePoint_t) == 28, + "Recovered aas2ChokePoint_t ABI changed"); +static_assert(sizeof(aas2HintNode_t) == 24, + "Recovered aas2HintNode_t ABI changed"); +static_assert(sizeof(aas2Trace_t) == 56, "Recovered aas2Trace_t ABI changed"); +static_assert(sizeof(aas2TraceFloor_t) == 84, + "Recovered aas2TraceFloor_t ABI changed"); +static_assert(sizeof(idAAS2File) == 736, "Recovered idAAS2File ABI changed"); +#endif diff --git a/source/engine/aas2file/aas2file_sample.cpp b/source/engine/aas2file/aas2file_sample.cpp new file mode 100644 index 0000000..bb41375 --- /dev/null +++ b/source/engine/aas2file/aas2file_sample.cpp @@ -0,0 +1,971 @@ +#include "aas2file/aas2file.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +int AbsoluteEdge(const int edgeNum) { + return edgeNum < 0 ? -edgeNum : edgeNum; +} + +bool ValidArea(const idAAS2File& file, const int areaNum) { + return areaNum > 0 && areaNum < file.areas.Num(); +} + +bool ValidEdge(const idAAS2File& file, const int edgeNum) { + const int number = AbsoluteEdge(edgeNum); + return number >= 0 && number < file.edges.Num(); +} + +idBounds EmptyBounds() { + idBounds result; + const float maximum = (std::numeric_limits::max)(); + result[0].Set(maximum, maximum, maximum); + result[1].Set(-maximum, -maximum, -maximum); + return result; +} + +void Include(idBounds& bounds, const idVec3& point) { + for (int axis = 0; axis < 3; ++axis) { + bounds[0][axis] = (std::min)(bounds[0][axis], point[axis]); + bounds[1][axis] = (std::max)(bounds[1][axis], point[axis]); + } +} + +bool Intersects(const idBounds& left, const idBounds& right) { + for (int axis = 0; axis < 3; ++axis) { + if (left[1][axis] < right[0][axis] + || left[0][axis] > right[1][axis]) return false; + } + return true; +} + +bool AreaMatches(const aas2Area_t& area, const int requiredAreaFlags, + const int excludedTravelFlags) { + return (area.flags & requiredAreaFlags) != 0 + && (area.travelFlags & excludedTravelFlags) == 0; +} + +void CollectBoundsAreas(const idAAS2File& file, const int nodeNum, + const idBounds& bounds, std::vector& result, + const int maximum) { + if (static_cast(result.size()) >= maximum || nodeNum == 0) return; + if (nodeNum < 0) { + const int areaNum = -nodeNum; + if (areaNum > 0 + && std::find(result.begin(), result.end(), areaNum) == result.end()) { + result.push_back(areaNum); + } + return; + } + if (nodeNum >= file.nodes.Num()) return; + const aas2Node_t& node = file.nodes[nodeNum]; + if (static_cast(node.planeNum) >= file.planes.Num()) return; + const int side = bounds.PlaneSide(file.planes[node.planeNum], 0.1f); + if (side == 3) { + CollectBoundsAreas(file, node.children[0], bounds, result, maximum); + CollectBoundsAreas(file, node.children[1], bounds, result, maximum); + } else { + CollectBoundsAreas(file, node.children[side], bounds, result, maximum); + } +} + +void MarkBoundsAreas(const idAAS2File& file, const int nodeNum, + const idBounds& bounds, bool* touched) { + if (nodeNum == 0) return; + if (nodeNum < 0) { + touched[-nodeNum] = true; + return; + } + if (nodeNum >= file.nodes.Num()) return; + const aas2Node_t& node = file.nodes[nodeNum]; + if (static_cast(node.planeNum) >= file.planes.Num()) return; + const int side = bounds.PlaneSide(file.planes[node.planeNum], 0.1f); + if (side == 3) { + MarkBoundsAreas(file, node.children[0], bounds, touched); + MarkBoundsAreas(file, node.children[1], bounds, touched); + } else { + MarkBoundsAreas(file, node.children[side], bounds, touched); + } +} + +struct TraceStackEntry { + idVec3 start; + idVec3 end; + int planeNum; + int nodeNum; +}; + +struct HeightTraceStackEntry { + idVec3 start; + idVec3 end; + int planeNum; + int nodeNum; +}; + +float SegmentFraction(const idVec3& start, const idVec3& end, + const idVec3& point, const bool ignoreZ = false) { + idVec3 delta = end - start; + idVec3 traveled = point - start; + if (ignoreZ) { + delta.z = 0.0f; + traveled.z = 0.0f; + } + const float lengthSquared = delta.LengthSqr(); + if (lengthSquared <= 1.0e-20f) return 0.0f; + return (std::max)(0.0f, (std::min)(1.0f, + traveled.Dot(delta) / lengthSquared)); +} + +void BuildFloorTracePlanes(const idVec3& floorNormal, + const idVec3& start, const idVec3& end, + idPlane& pathPlane, idPlane& nearPlane) { + idVec3 pathNormal = floorNormal.Cross(end - start); + pathNormal.NormalizeFast(); + idVec3 nearNormal = pathNormal.Cross(floorNormal); + nearNormal.NormalizeFast(); + pathPlane = idPlane(pathNormal, pathNormal.Dot(start)); + nearPlane = idPlane(nearNormal, nearNormal.Dot(start)); +} + +bool SameVector(const idVec3& first, const idVec3& second) { + return first.x == second.x && first.y == second.y + && first.z == second.z; +} + +} // namespace + +idVec3 idAAS2File::AreaCenter(const int areaNum) const { + idVec3 center(0.0f, 0.0f, 0.0f); + if (!ValidArea(*this, areaNum)) return center; + const aas2Area_t& area = areas[areaNum]; + int count = 0; + for (int index = 0; index < area.numEdges; ++index) { + const int edgeListIndex = area.firstEdge + index; + if (edgeListIndex < 0 || edgeListIndex >= edgeIndex.Num()) continue; + const int orientedEdge = edgeIndex[edgeListIndex]; + if (!ValidEdge(*this, orientedEdge)) continue; + const aas2Edge_t& edge = edges[AbsoluteEdge(orientedEdge)]; + if (edge.vertexNum[0] < 0 || edge.vertexNum[0] >= vertices.Num() + || edge.vertexNum[1] < 0 || edge.vertexNum[1] >= vertices.Num()) { + continue; + } + center = center + (vertices[edge.vertexNum[0]] + + vertices[edge.vertexNum[1]]) * 0.5f; + ++count; + } + if (count > 0) center = center * (1.0f / static_cast(count)); + return center; +} + +idBounds idAAS2File::EdgeBounds(const int edgeNum) const { + idBounds bounds = EmptyBounds(); + if (!ValidEdge(*this, edgeNum)) return bounds; + const aas2Edge_t& edge = edges[AbsoluteEdge(edgeNum)]; + for (int endpoint = 0; endpoint < 2; ++endpoint) { + if (edge.vertexNum[endpoint] >= 0 + && edge.vertexNum[endpoint] < vertices.Num()) { + Include(bounds, vertices[edge.vertexNum[endpoint]]); + } + } + return bounds; +} + +idBounds idAAS2File::AreaBounds(const int areaNum) const { + idBounds bounds = EmptyBounds(); + if (!ValidArea(*this, areaNum)) return bounds; + const aas2Area_t& area = areas[areaNum]; + for (int index = 0; index < area.numEdges; ++index) { + const int listIndex = area.firstEdge + index; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const idBounds edgeBounds = EdgeBounds(edgeIndex[listIndex]); + Include(bounds, edgeBounds[0]); + Include(bounds, edgeBounds[1]); + } + return bounds; +} + +int idAAS2File::PointAreaNum(const int tree, const idVec3& origin) const { + if (tree < 0 || tree >= trees.Num()) return 0; + int nodeNum = trees[tree].headNode; + int guard = 0; + while (nodeNum > 0 && guard++ <= nodes.Num()) { + if (nodeNum >= nodes.Num()) return 0; + const aas2Node_t& node = nodes[nodeNum]; + if (static_cast(node.planeNum) >= planes.Num()) return 0; + nodeNum = node.children[planes[node.planeNum].Distance(origin) <= 0.0f]; + } + return nodeNum < 0 ? -nodeNum : 0; +} + +int idAAS2File::BoundsAreaNums(const int tree, const idBounds& bounds, + int* const outputAreas, const int maxAreas) const { + if (tree < 0 || tree >= trees.Num() || outputAreas == nullptr + || maxAreas <= 0) return 0; + std::vector found; + found.reserve((std::min)(maxAreas, 128)); + CollectBoundsAreas(*this, trees[tree].headNode, bounds, found, maxAreas); + for (int index = 0; index < static_cast(found.size()); ++index) { + outputAreas[index] = found[index]; + } + return static_cast(found.size()); +} + +void idAAS2File::FlagBoundsAreas(const int tree, const idBounds& bounds, + bool* const areasTouched) const { + if (tree < 0 || tree >= trees.Num() || areasTouched == nullptr) return; + MarkBoundsAreas(*this, trees[tree].headNode, bounds, areasTouched); +} + +int idAAS2File::BoundsReachableAreaNum(const int tree, + const idBounds& bounds, const std::uint16_t areaFlags, + const int excludeTravelFlags) const { + int found[256] = {}; + const int count = BoundsAreaNums(tree, bounds, found, 256); + for (int index = 0; index < count; ++index) { + const int areaNum = found[index]; + if (ValidArea(*this, areaNum) + && AreaMatches(areas[areaNum], areaFlags, excludeTravelFlags)) { + return areaNum; + } + } + return 0; +} + +bool idAAS2File::TraceHeight(const int tree, aas2TraceHeight_t& trace, + const idVec3& start, const idVec3& end) const { + if (tree < 0 || tree >= trees.Num()) return false; + trace.numPoints = 0; + std::vector stack; + stack.reserve(128); + stack.push_back({ start, end, 0, trees[tree].headNode }); + while (!stack.empty()) { + const HeightTraceStackEntry entry = stack.back(); + stack.pop_back(); + if (entry.nodeNum <= 0) continue; + if (entry.nodeNum >= nodes.Num()) return false; + const aas2Node_t& node = nodes[entry.nodeNum]; + if ((node.flags & 2u) != 0) { + if (trace.points != nullptr && trace.numPoints < trace.maxPoints) { + trace.points[trace.numPoints] = entry.start; + trace.points[trace.numPoints].z = static_cast( + static_cast(node.flags >> 2) - 0x20000000); + ++trace.numPoints; + } + continue; + } + if (static_cast(node.planeNum) >= planes.Num()) return false; + const idPlane& plane = planes[node.planeNum]; + const float firstDistance = plane.Distance(entry.start); + const float secondDistance = plane.Distance(entry.end); + if (firstDistance >= -0.1f && secondDistance >= -0.1f) { + stack.push_back({ entry.start, entry.end, entry.planeNum, + node.children[0] }); + continue; + } + if (firstDistance < 0.1f && secondDistance < 0.1f) { + stack.push_back({ entry.start, entry.end, entry.planeNum, + node.children[1] }); + continue; + } + + float fraction = (firstDistance >= 0.0f + ? firstDistance - 0.125f : firstDistance + 0.125f) + / (firstDistance - secondDistance); + fraction = (std::max)(0.001f, (std::min)(0.999f, fraction)); + const idVec3 middle = entry.start + + (entry.end - entry.start) * fraction; + const int firstSide = firstDistance < 0.0f ? 1 : 0; + if (stack.size() + 2 > 128) return false; + stack.push_back({ middle, entry.end, + static_cast(node.planeNum), node.children[1 - firstSide] }); + stack.push_back({ entry.start, middle, entry.planeNum, + node.children[firstSide] }); + } + return true; +} + +bool idAAS2File::GetFloorEdgeSplitPoints(floorEdgeSplitPoint_t& nearest, + floorEdgeSplitPoint_t& furthest, const int areaNum, + const idPlane& pathPlane, const idPlane& nearPlane) const { + nearest.point.Zero(); nearest.dist = 1.0e30f; nearest.edgeNum = 0; + furthest.point.Zero(); furthest.dist = -1.0e30f; furthest.edgeNum = 0; + if (!ValidArea(*this, areaNum)) return false; + const aas2Area_t& area = areas[areaNum]; + int splits = 0; + for (int index = 0; index < area.numEdges; ++index) { + const int listIndex = area.firstEdge + index; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const int orientedEdge = edgeIndex[listIndex]; + if (!ValidEdge(*this, orientedEdge)) continue; + const aas2Edge_t& edge = edges[AbsoluteEdge(orientedEdge)]; + const idVec3& first = vertices[edge.vertexNum[0]]; + const idVec3& second = vertices[edge.vertexNum[1]]; + const float firstSide = pathPlane.Distance(first); + const float secondSide = pathPlane.Distance(second); + if ((firstSide < 0.0f && secondSide < 0.0f) + || (firstSide > 0.0f && secondSide > 0.0f)) continue; + float fraction = 0.0f; + if (std::fabs(firstSide - secondSide) > 1.0e-20f) { + fraction = firstSide / (firstSide - secondSide); + } + fraction = (std::max)(0.0f, (std::min)(1.0f, fraction)); + const idVec3 split = first + (second - first) * fraction; + const float distance = nearPlane.Distance(split); + if (distance < nearest.dist) { + nearest.point = split; + nearest.dist = distance; + nearest.edgeNum = orientedEdge; + } + if (distance > furthest.dist) { + furthest.point = split; + furthest.dist = distance; + furthest.edgeNum = orientedEdge; + } + ++splits; + } + return splits != 0; +} + +bool idAAS2File::Trace(const int tree, aas2Trace_t& trace, + const idVec3& start, const idVec3& end) const { + if (tree < 0 || tree >= trees.Num()) return false; + trace.numAreas = 0; + trace.lastAreaNum = 0; + trace.blockingAreaNum = 0; + std::vector stack; + stack.reserve(128); + stack.push_back({ start, end, 0, trees[tree].headNode }); + const idVec3 traceDirection = end - start; + const float traceLength = traceDirection.Length(); + while (!stack.empty()) { + const TraceStackEntry entry = stack.back(); + stack.pop_back(); + if (entry.nodeNum < 0) { + const int areaNum = -entry.nodeNum; + if (!ValidArea(*this, areaNum)) continue; + const aas2Area_t& area = areas[areaNum]; + const bool areaBlocked = (area.flags & trace.flags) != 0 + || (area.travelFlags & trace.travelFlags) != 0; + if (areaBlocked) { + trace.fraction = trace.lastAreaNum != 0 && traceLength > 0.0f + ? (entry.start - start).Length() / traceLength : 0.0f; + trace.endpos = entry.start; + trace.planeNum = entry.planeNum; + trace.blockingAreaNum = areaNum; + if (entry.planeNum >= 0 && entry.planeNum < planes.Num() + && traceDirection.Dot(planes[entry.planeNum].Normal()) > 0.0f) { + trace.planeNum ^= 1; + } + return true; + } + trace.lastAreaNum = areaNum; + if (trace.numAreas < trace.maxAreas) { + if (trace.areas != nullptr) trace.areas[trace.numAreas] = areaNum; + if (trace.points != nullptr) trace.points[trace.numAreas] = entry.start; + ++trace.numAreas; + } + continue; + } + if (entry.nodeNum == 0) { + trace.fraction = trace.lastAreaNum != 0 && traceLength > 0.0f + ? (entry.start - start).Length() / traceLength : 0.0f; + trace.endpos = entry.start; + trace.planeNum = entry.planeNum; + trace.blockingAreaNum = 0; + if (entry.planeNum >= 0 && entry.planeNum < planes.Num() + && traceDirection.Dot(planes[entry.planeNum].Normal()) > 0.0f) { + trace.planeNum ^= 1; + } + if (trace.lastAreaNum != 0 || trace.getOutOfSolid == 0) return true; + continue; + } + if (entry.nodeNum >= nodes.Num()) return false; + const aas2Node_t& node = nodes[entry.nodeNum]; + if (static_cast(node.planeNum) >= planes.Num()) return false; + const idPlane& plane = planes[node.planeNum]; + const float firstDistance = plane.Distance(entry.start); + const float secondDistance = plane.Distance(entry.end); + if (firstDistance >= -0.1f && secondDistance >= -0.1f) { + stack.push_back({ entry.start, entry.end, entry.planeNum, + node.children[0] }); + continue; + } + if (firstDistance < 0.1f && secondDistance < 0.1f) { + stack.push_back({ entry.start, entry.end, entry.planeNum, + node.children[1] }); + continue; + } + + float fraction = (firstDistance >= 0.0f + ? firstDistance - 0.125f : firstDistance + 0.125f) + / (firstDistance - secondDistance); + fraction = (std::max)(0.001f, (std::min)(0.999f, fraction)); + const idVec3 middle = entry.start + + (entry.end - entry.start) * fraction; + const int firstSide = firstDistance < 0.0f ? 1 : 0; + if (stack.size() + 2 > 128) return false; + stack.push_back({ middle, entry.end, static_cast(node.planeNum), + node.children[1 - firstSide] }); + stack.push_back({ entry.start, middle, entry.planeNum, + node.children[firstSide] }); + } + trace.fraction = trace.lastAreaNum != 0 ? 1.0f : 0.0f; + trace.endpos = trace.lastAreaNum != 0 ? end : start; + trace.planeNum = 0; + return false; +} + +int idAAS2File::GetObstaclePVSWallEdges(const int areaNum, + const int edgeFlags, int* const outputEdges, const int maxEdges) const { + if (!ValidArea(*this, areaNum) || outputEdges == nullptr || maxEdges <= 0) { + return 0; + } + std::vector collected; + collected.reserve(maxEdges); + auto appendAreaEdges = [&](const int visibleArea) { + if (!ValidArea(*this, visibleArea)) return; + const aas2Area_t& area = areas[visibleArea]; + for (int index = 0; index < area.numEdges + && static_cast(collected.size()) < maxEdges; ++index) { + const int listIndex = area.firstEdge + index; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const int edgeNum = edgeIndex[listIndex]; + if (!ValidEdge(*this, edgeNum) + || (edges[AbsoluteEdge(edgeNum)].flags & edgeFlags) == 0) continue; + collected.push_back(edgeNum); + } + }; + + std::uint32_t offset = areas[areaNum].obstaclePVSOffset; + int visibleArea = 0; + while (offset < static_cast(obstaclePVS.Num()) + && visibleArea < areas.Num() + && static_cast(collected.size()) < maxEdges) { + const std::uint8_t encoded = obstaclePVS[offset++]; + if ((encoded & 0x80u) != 0) { + int skip = encoded & 0x3Fu; + if ((encoded & 0x40u) != 0 + && offset < static_cast(obstaclePVS.Num())) { + skip |= obstaclePVS[offset++] << 6; + } + visibleArea += skip + 1; + continue; + } + for (int bit = 0; bit < 7 && visibleArea < areas.Num(); ++bit) { + if ((encoded & (1u << bit)) != 0) appendAreaEdges(visibleArea); + ++visibleArea; + } + } + + if (collected.empty()) return 0; + if (static_cast(collected.size()) >= maxEdges) { + for (int index = 0; index < maxEdges; ++index) { + outputEdges[index] = collected[index]; + } + return maxEdges; + } + + auto startVertex = [&](const int edgeNum) { + const aas2Edge_t& edge = edges[AbsoluteEdge(edgeNum)]; + return edge.vertexNum[edgeNum < 0 ? 1 : 0]; + }; + auto endVertex = [&](const int edgeNum) { + const aas2Edge_t& edge = edges[AbsoluteEdge(edgeNum)]; + return edge.vertexNum[edgeNum < 0 ? 0 : 1]; + }; + std::vector> chains; + chains.reserve(collected.size()); + for (const int edgeNum : collected) chains.push_back({ edgeNum }); + for (std::size_t first = 0; first < chains.size(); ++first) { + bool joined = true; + while (joined) { + joined = false; + for (std::size_t second = first + 1; second < chains.size(); ++second) { + if (endVertex(chains[first].back()) + == startVertex(chains[second].front())) { + chains[first].insert(chains[first].end(), + chains[second].begin(), chains[second].end()); + } else if (endVertex(chains[second].back()) + == startVertex(chains[first].front())) { + chains[first].insert(chains[first].begin(), + chains[second].begin(), chains[second].end()); + } else { + continue; + } + chains.erase(chains.begin() + second); + joined = true; + break; + } + } + } + int outputCount = 0; + for (const std::vector& chain : chains) { + for (const int edgeNum : chain) outputEdges[outputCount++] = edgeNum; + } + return static_cast(collected.size()); +} + +void idAAS2File::ClipGridToAreas(const idVec3& startOrigin, + const int startAreaNum, const int travelFlags, + const unsigned int cellSize, const int dimension, + std::uint8_t* const reachable) const { + if (reachable == nullptr || dimension <= 0 || cellSize == 0) return; + const int cellCount = dimension * dimension; + std::memset(reachable, 0, static_cast(cellCount)); + if (!ValidArea(*this, startAreaNum)) return; + + const idIndex treeIndex = GetTreeForArea(startAreaNum); + const int treeNum = treeIndex.Get(); + if (treeNum < 0 || treeNum >= trees.Num()) return; + const float half = 0.5f * static_cast(cellSize * dimension); + idBounds searchBounds; + const idVec3 extents(half, half, half); + searchBounds[0] = startOrigin - extents; + searchBounds[1] = startOrigin + extents; + std::unique_ptr areasTouched(new bool[areas.Num()]()); + FlagBoundsAreas(treeNum, searchBounds, areasTouched.get()); + + std::vector queue; + queue.push_back(startAreaNum); + areasTouched[startAreaNum] = false; + for (std::size_t cursor = 0; cursor < queue.size() && queue.size() < 256; ++cursor) { + const aas2Area_t& area = areas[queue[cursor]]; + int reachIndex = area.reach.Get(); + int guard = 0; + while (reachIndex >= 0 && reachIndex < reachabilities.Num() + && guard++ < reachabilities.Num()) { + const aas2Reachability_t& reachability = reachabilities[reachIndex]; + const int target = reachability.toAreaNum; + if (ValidArea(*this, target) && areasTouched[target] + && (reachability.travelFlags & travelFlags) != 0 + && (reachability.travelFlags & ~travelFlags) == 0 + && (areas[target].travelFlags & travelFlags) != 0 + && (areas[target].travelFlags & ~travelFlags) == 0) { + areasTouched[target] = false; + queue.push_back(target); + } + reachIndex = reachability.next.Get(); + } + } + + const idVec3& floorNormal = trees[treeNum].floorNormal; + for (const int areaNum : queue) { + const aas2Area_t& area = areas[areaNum]; + for (int y = 0; y < dimension; ++y) { + for (int x = 0; x < dimension; ++x) { + const idVec3 point( + startOrigin.x - half + (x + 0.5f) * cellSize, + startOrigin.y - half + (y + 0.5f) * cellSize, + startOrigin.z); + bool inside = true; + bool testedEdge = false; + for (int edgeOffset = 0; edgeOffset < area.numEdges; + ++edgeOffset) { + const int listIndex = area.firstEdge + edgeOffset; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const int orientedEdge = edgeIndex[listIndex]; + if (!ValidEdge(*this, orientedEdge)) continue; + const aas2Edge_t& edge = edges[AbsoluteEdge(orientedEdge)]; + if ((edge.flags & AAS_EDGE_VERTICAL) != 0) continue; + const int firstIndex = orientedEdge < 0 + ? edge.vertexNum[1] : edge.vertexNum[0]; + const int secondIndex = orientedEdge < 0 + ? edge.vertexNum[0] : edge.vertexNum[1]; + if (firstIndex < 0 || firstIndex >= vertices.Num() + || secondIndex < 0 || secondIndex >= vertices.Num()) { + continue; + } + const idVec3& first = vertices[firstIndex]; + idVec3 inward = (vertices[secondIndex] - first).Cross(floorNormal); + if (inward.NormalizeFast() == 0.0f) continue; + testedEdge = true; + if ((point - first).Dot(inward) < 0.0f) { + inside = false; + break; + } + } + if (inside && testedEdge) reachable[y * dimension + x] = 0xFF; + } + } + } +} + +float idAAS2File::GetFloorDistance(const int areaNum, + const idPlane& floorPlane, const idVec3& point, const float bboxHeight, + const float maxEdgeDist) const { + if (!ValidArea(*this, areaNum)) return 1.0e30f; + const idIndex treeIndex = GetTreeForArea(areaNum); + const idVec3* const floorNormal = GetFloorNormalForTree(treeIndex); + if (floorNormal == nullptr) return 1.0e30f; + const float denominator = floorNormal->Dot(floorPlane.Normal()); + if (std::fabs(denominator) <= 1.0e-20f) return 1.0e30f; + float floorDistance = std::fabs(floorPlane.Distance(point)) / denominator; + if (floorDistance < bboxHeight) return floorDistance; + + float nearestEdgeSquared = 1.0e30f; + idVec3 nearestDelta(0.0f, 0.0f, 0.0f); + const aas2Area_t& area = areas[areaNum]; + for (int index = 0; index < area.numEdges; ++index) { + const int listIndex = area.firstEdge + index; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const int edgeNum = AbsoluteEdge(edgeIndex[listIndex]); + if (edgeNum < 0 || edgeNum >= edges.Num()) continue; + const aas2Edge_t& edge = edges[edgeNum]; + if (edge.vertexNum[0] < 0 || edge.vertexNum[0] >= vertices.Num() + || edge.vertexNum[1] < 0 || edge.vertexNum[1] >= vertices.Num()) { + continue; + } + const idVec3& first = vertices[edge.vertexNum[0]]; + const idVec3& second = vertices[edge.vertexNum[1]]; + const idVec3 segment = second - first; + const float lengthSquared = segment.LengthSqr(); + if (lengthSquared < 0.1f) continue; + float fraction = (point - first).Dot(segment) / lengthSquared; + fraction = (std::max)(0.0f, (std::min)(1.0f, fraction)); + const idVec3 delta = point - (first + segment * fraction); + const float distanceSquared = delta.LengthSqr(); + if (distanceSquared < nearestEdgeSquared) { + nearestEdgeSquared = distanceSquared; + nearestDelta = delta; + } + } + if (nearestEdgeSquared < maxEdgeDist * maxEdgeDist) { + const float edgeFloorDistance = std::fabs(floorNormal->Dot(nearestDelta)); + if (edgeFloorDistance < floorDistance) floorDistance = edgeFloorDistance; + } + return floorDistance; +} + +void idAAS2File::PointBestReachableAreaNum(const int tree, + const idVec3& origin, bestReachableArea_t& bestArea) const { + if (tree < 0 || tree >= trees.Num()) return; + int floorPlaneNum = -1; + int nodeNum = trees[tree].headNode; + while (nodeNum >= 0) { + if (nodeNum == 0 || nodeNum >= nodes.Num()) return; + const aas2Node_t& node = nodes[nodeNum]; + if (static_cast(node.planeNum) >= planes.Num()) return; + if (planes[node.planeNum].Distance(origin) <= 0.0f) { + nodeNum = node.children[1]; + } else { + nodeNum = node.children[0]; + if ((node.flags & 1u) != 0) { + floorPlaneNum = static_cast(node.planeNum); + } + } + } + const int areaNum = -nodeNum; + if (!ValidArea(*this, areaNum) || floorPlaneNum < 0 + || !AreaMatches(areas[areaNum], bestArea.areaFlags, + bestArea.excludeTravelFlags)) { + return; + } + bestArea.pointAreaFloorDist = GetFloorDistance(areaNum, + planes[floorPlaneNum], origin, bestArea.bboxHeight, + bestArea.maxEdgeDist); + bestArea.pointAreaNum = areaNum; +} + +void idAAS2File::BoundsBestReachableAreaNum(const int tree, + const idBounds& bounds, const idVec3& origin, + bestReachableArea_t& bestArea) const { + if (tree < 0 || tree >= trees.Num()) return; + int floorPlaneNum = -1; + std::vector stack; + int nodeNum = trees[tree].headNode; + for (;;) { + while (nodeNum > 0) { + if (nodeNum >= nodes.Num()) return; + const aas2Node_t& node = nodes[nodeNum]; + if (static_cast(node.planeNum) >= planes.Num()) return; + const int side = bounds.PlaneSide(planes[node.planeNum], 0.1f); + if ((node.flags & 1u) != 0 && side != 1) { + floorPlaneNum = static_cast(node.planeNum); + } + if (side == 3) { + stack.push_back(node.children[1]); + nodeNum = node.children[0]; + } else { + nodeNum = node.children[side]; + } + } + const int areaNum = -nodeNum; + if (nodeNum < 0 && ValidArea(*this, areaNum) && floorPlaneNum >= 0 + && AreaMatches(areas[areaNum], bestArea.areaFlags, + bestArea.excludeTravelFlags)) { + const float floorDistance = GetFloorDistance(areaNum, + planes[floorPlaneNum], origin, bestArea.bboxHeight, + bestArea.maxEdgeDist); + if (floorDistance < bestArea.pointAreaFloorDist + - bestArea.bboxHeight + && floorDistance < bestArea.boundsAreaFloorDist) { + bestArea.boundsAreaFloorDist = floorDistance; + bestArea.boundsAreaNum = areaNum; + } + } + if (stack.empty()) break; + nodeNum = stack.back(); + stack.pop_back(); + } +} + +int idAAS2File::PointReachableAreaNum( + const idIndex tree, const idVec3& origin, + const int areaFlags, const int excludeTravelFlags) const { + const int treeNum = tree.Get(); + if (treeNum < 0 || treeNum >= trees.Num()) return 0; + + bestReachableArea_t bestArea{}; + bestArea.bboxHeight = settings.boundingBox[1].z + - settings.boundingBox[0].z; + bestArea.maxEdgeDist = (std::min)(settings.boundingBox[1].x, + settings.boundingBox[1].y) * 0.25f * 6.0f; + bestArea.areaFlags = areaFlags; + bestArea.excludeTravelFlags = excludeTravelFlags; + bestArea.pointAreaFloorDist = 1.0e30f; + bestArea.boundsAreaFloorDist = 1.0e30f; + PointBestReachableAreaNum(treeNum, origin, bestArea); + + const float expansion = (std::min)(settings.boundingBox[1].x, + settings.boundingBox[1].y) * 0.25f; + idBounds search; + search[0].Set(origin.x - expansion, origin.y - expansion, origin.z); + search[1].Set(origin.x + expansion, origin.y + expansion, origin.z); + for (int pass = 0; pass < 4; ++pass) { + BoundsBestReachableAreaNum(treeNum, search, origin, bestArea); + if (bestArea.boundsAreaNum != 0 + && bestArea.boundsAreaFloorDist < bestArea.bboxHeight * 2.0f) { + break; + } + search[0] = search[0] - idVec3(expansion, expansion, expansion); + search[1] = search[1] + idVec3(expansion, expansion, expansion); + } + return bestArea.boundsAreaNum != 0 + ? bestArea.boundsAreaNum : bestArea.pointAreaNum; +} + +bool idAAS2File::PushPointIntoAreaNum(const int areaNum, idVec3& point) const { + if (!ValidArea(*this, areaNum)) return false; + const idIndex treeIndex = GetTreeForArea(areaNum); + const idVec3* floorNormal = GetFloorNormalForTree(treeIndex); + if (floorNormal == nullptr) return false; + const aas2Area_t& area = areas[areaNum]; + bool pushed = false; + bool liesOnEdge = false; + idVec3 nearestVertex = point; + float nearestDistance = (std::numeric_limits::max)(); + for (int index = 0; index < area.numEdges; ++index) { + const int listIndex = area.firstEdge + index; + if (listIndex < 0 || listIndex >= edgeIndex.Num()) continue; + const int orientedEdge = edgeIndex[listIndex]; + if (!ValidEdge(*this, orientedEdge)) continue; + const aas2Edge_t& edge = edges[AbsoluteEdge(orientedEdge)]; + if ((edge.flags & AAS_EDGE_VERTICAL) != 0) continue; + const int firstIndex = orientedEdge < 0 ? edge.vertexNum[1] : edge.vertexNum[0]; + const int secondIndex = orientedEdge < 0 ? edge.vertexNum[0] : edge.vertexNum[1]; + const idVec3& first = vertices[firstIndex]; + const idVec3& second = vertices[secondIndex]; + const idVec3 edgeDirection = second - first; + idVec3 inward = edgeDirection.Cross(*floorNormal); + if (inward.NormalizeFast() == 0.0f) continue; + const float distance = (point - first).Dot(inward); + if (distance < 0.0f) { + point = point - inward * distance; + pushed = true; + } + const float firstDistance = (point - first).LengthSqr(); + if (firstDistance < nearestDistance) { + nearestDistance = firstDistance; + nearestVertex = first; + } + const float along = (point - first).Dot(edgeDirection); + if (std::fabs(distance) < 0.1f && along >= 0.0f + && along <= edgeDirection.LengthSqr()) liesOnEdge = true; + } + if (pushed && !liesOnEdge) point = nearestVertex; + return pushed; +} + +bool idAAS2File::TraceFloor(aas2TraceFloor_t& trace, + const idVec3& start, const int startAreaNum, const idVec3& end, + int endAreaNum, const int travelFlags, + const bool allowFloorNormalChange, + const bool ignoreGravityDirectionDistance, + const bool ignoreSameArea) const { + trace.fraction = 0.0f; + trace.endpos = start; + trace.lastAreaNum = startAreaNum; + trace.firstEdge = { 0, 0, start }; + trace.lastEdge = { 0, 0, start }; + trace.numAreas = 0; + trace.numReachIndices = 0; + if (!ValidArea(*this, startAreaNum)) return false; + + const idVec3* initialFloorNormal = GetFloorNormalForArea(startAreaNum); + if (initialFloorNormal == nullptr) return false; + idVec3 floorNormal = *initialFloorNormal; + idPlane pathPlane; + idPlane nearPlane; + BuildFloorTracePlanes(floorNormal, start, end, pathPlane, nearPlane); + const idPlane endPlane(nearPlane.Normal(), nearPlane.Normal().Dot(end)); + if (endAreaNum == 0) endAreaNum = PointAreaNum(0, end); + + floorEdgeSplitPoint_t nearest; + floorEdgeSplitPoint_t furthest; + if (!GetFloorEdgeSplitPoints(nearest, furthest, startAreaNum, + pathPlane, nearPlane)) { + nearest = { start, 0.0f, 0 }; + furthest = nearest; + } + + // The recovered routine uses the file's mutable flood flags as scratch + // storage, then clears them before returning. Keeping the identical + // visited set locally preserves const behavior on the PC port. + std::vector visited(static_cast(areas.Num()), 0); + std::vector visitedOrder; + visitedOrder.reserve(256); + int currentAreaNum = startAreaNum; + + for (;;) { + if (trace.areas != nullptr && trace.numAreas < trace.maxAreas) { + trace.areas[trace.numAreas++] = currentAreaNum; + } + visited[currentAreaNum] = 1; + visitedOrder.push_back(currentAreaNum); + + if ((currentAreaNum == endAreaNum + || endPlane.Distance(furthest.point) > 0.1f) + && ignoreSameArea) { + trace.endpos = end; + trace.fraction = 1.0f; + break; + } + + trace.lastAreaNum = currentAreaNum; + trace.endpos = furthest.point; + trace.lastEdge.edgeNum = furthest.edgeNum; + trace.lastEdge.edgePoint = furthest.point; + nearPlane.SetDist(nearPlane.Normal().Dot(trace.endpos)); + + const std::size_t visitedBase = visitedOrder.size(); + int acceptedReachIndex = -1; + int reachIndex = areas[currentAreaNum].reach.Get(); + int guard = 0; + while (reachIndex >= 0 && reachIndex < reachabilities.Num() + && guard++ < reachabilities.Num()) { + const aas2Reachability_t& reachability = reachabilities[reachIndex]; + const int targetAreaNum = reachability.toAreaNum; + const std::uint32_t reachTravelFlags = reachability.travelFlags; + const bool validTravel = (reachTravelFlags & travelFlags) != 0 + && (reachTravelFlags & ~static_cast(travelFlags)) == 0; + const bool validTarget = ValidArea(*this, targetAreaNum) + && (areas[targetAreaNum].travelFlags & travelFlags) != 0 + && (areas[targetAreaNum].travelFlags + & ~static_cast(travelFlags)) == 0 + && visited[targetAreaNum] == 0; + if (!validTravel || !validTarget) { + reachIndex = reachability.next.Get(); + continue; + } + + // This append intentionally precedes the geometric tests: the + // original records every otherwise eligible reachability it tries. + if (trace.reachIndices != nullptr + && trace.numReachIndices < trace.maxReachIndices) { + trace.reachIndices[trace.numReachIndices++] = + idIndex( + static_cast(reachIndex)); + } + visited[targetAreaNum] = 1; + visitedOrder.push_back(targetAreaNum); + + const idVec3* targetFloorNormal = + GetFloorNormalForArea(targetAreaNum); + if (targetFloorNormal == nullptr) { + reachIndex = reachability.next.Get(); + continue; + } + + bool accepted = false; + bool changedFloorNormal = false; + idPlane candidatePathPlane = pathPlane; + idPlane candidateNearPlane = nearPlane; + if (allowFloorNormalChange + && !SameVector(*targetFloorNormal, floorNormal)) { + BuildFloorTracePlanes(*targetFloorNormal, start, end, + candidatePathPlane, candidateNearPlane); + candidateNearPlane.SetDist( + candidateNearPlane.Normal().Dot(trace.endpos)); + GetFloorEdgeSplitPoints(nearest, furthest, targetAreaNum, + candidatePathPlane, candidateNearPlane); + accepted = nearest.dist < 1.0e30f && furthest.dist >= 0.1f; + changedFloorNormal = accepted; + } else { + GetFloorEdgeSplitPoints(nearest, furthest, targetAreaNum, + pathPlane, nearPlane); + if (nearest.dist < 1.0e30f && furthest.dist >= -0.1f) { + const bool fly = + (reachTravelFlags & AAS_TFL_FLY) != 0; + idVec3 delta = trace.endpos - nearest.point; + if (fly && std::fabs(furthest.dist) + < std::fabs(nearest.dist)) { + delta = trace.endpos - furthest.point; + } + const float gravityDistance = delta.Dot(floorNormal); + const idVec3 gravityDelta = + floorNormal * gravityDistance; + const idVec3 horizontalDelta = delta - gravityDelta; + accepted = (fly || gravityDelta.LengthSqr() + <= settings.maxStepHeight * settings.maxStepHeight) + && (fly || horizontalDelta.LengthSqr() + <= 0.040000003f); + } + } + + if (accepted) { + acceptedReachIndex = reachIndex; + currentAreaNum = targetAreaNum; + if (changedFloorNormal) { + floorNormal = *targetFloorNormal; + pathPlane = candidatePathPlane; + nearPlane = candidateNearPlane; + } + break; + } + reachIndex = reachability.next.Get(); + } + + if (acceptedReachIndex < 0) break; + for (std::size_t index = visitedBase; index < visitedOrder.size(); ++index) { + visited[visitedOrder[index]] = 0; + } + visitedOrder.resize(visitedBase); + if (trace.firstEdge.edgeNum == 0) { + trace.firstEdge.toAreaNum = currentAreaNum; + trace.firstEdge.edgeNum = nearest.edgeNum; + trace.firstEdge.edgePoint = nearest.point; + } + } + + if (trace.fraction != 1.0f) { + const idVec3 total = end - start; + const float totalDistanceSquared = total.LengthSqr(); + if (std::fabs(totalDistanceSquared) <= + (std::numeric_limits::min)()) { + trace.fraction = 0.0f; + } else { + idVec3 traveled = trace.endpos - start; + if (ignoreGravityDirectionDistance) traveled.z = 0.0f; + trace.fraction = std::sqrt( + traveled.LengthSqr() / totalDistanceSquared); + } + } + trace.lastEdge.toAreaNum = currentAreaNum; + return true; +} diff --git a/source/engine/aas2file/aas2file_text.cpp b/source/engine/aas2file/aas2file_text.cpp new file mode 100644 index 0000000..5f55cf3 --- /dev/null +++ b/source/engine/aas2file/aas2file_text.cpp @@ -0,0 +1,485 @@ +#include "aas2file/aas2file.h" + +#include "idlib/text/lexer.h" + +#include +#include +#include +#include + +namespace { + +bool BeginArray(idLexer& source, int& count) { + count = source.ParseInt(); + return count >= 0 && source.ExpectTokenString("{"); +} + +bool BeginRecord(idLexer& source) { + source.ParseInt(); // the serialized record index is informational + return source.ExpectTokenString("("); +} + +std::int16_t ClampShort(const float value) { + const int integer = static_cast(value); + return static_cast((std::max)(-32768, + (std::min)(32767, integer))); +} + +} // namespace + +bool idAAS2File::ParseIndex(idLexer& source, idList& indexes) { + int count = 0; + if (!BeginArray(source, count)) return false; + indexes.Clear(); + if (!indexes.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + source.ParseInt(); + if (!source.ExpectTokenString("(")) return false; + const int value = source.ParseInt(); + if (!source.ExpectTokenString(")")) return false; + indexes.Append(value); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseNames(idLexer& source, + idList& names) { + int count = 0; + if (!BeginArray(source, count)) return false; + names.Clear(); + if (!names.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + idToken token; + aas2Name_t entry{}; + if (!source.ReadToken(token)) return false; + strncpy_s(entry.name, token.c_str(), _TRUNCATE); + entry.index = source.ParseInt(); + names.Append(entry); + } + return source.ExpectTokenString("}"); +} + +template +bool idAAS2File::ParseInteractionEntityNames(idLexer& source, + idList& names) { + int count = 0; + if (!BeginArray(source, count)) return false; + names.Clear(); + if (!names.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + idToken token; + nameType entry{}; + if (!source.ReadToken(token)) return false; + strncpy_s(entry.name, token.c_str(), _TRUNCATE); + names.Append(entry); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseVertices(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + vertices.Clear(); + if (!vertices.PreAllocate(count + 1)) return false; + for (int index = 0; index < count; ++index) { + source.ParseInt(); + idVec3 vertex; + if (!source.Parse1DMatrix(3, &vertex.x, false)) return false; + vertices.Append(vertex); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseEdges(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + edges.Clear(); + if (!edges.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Edge_t edge{}; + if (!BeginRecord(source)) return false; + edge.vertexNum[0] = source.ParseInt(); + edge.vertexNum[1] = source.ParseInt(); + edge.flags = source.ParseInt(); + if (!source.ExpectTokenString(")")) return false; + edges.Append(edge); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseNodes(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + nodes.Clear(); + if (!nodes.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Node_t node{}; + if (!BeginRecord(source)) return false; + node.planeNum = source.ParseUnsignedInt(); + node.flags = source.ParseUnsignedInt(); + node.children[0] = source.ParseInt(); + node.children[1] = source.ParseInt(); + if (!source.ExpectTokenString(")")) return false; + nodes.Append(node); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParsePortals(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + portals.Clear(); + if (!portals.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Portal_t portal{}; + if (!BeginRecord(source)) return false; + portal.areaNum = static_cast(source.ParseInt()); + portal.clusters[0] = static_cast(source.ParseInt()); + portal.clusters[1] = static_cast(source.ParseInt()); + portal.clusterAreaNum[0] = static_cast(source.ParseInt()); + portal.clusterAreaNum[1] = static_cast(source.ParseInt()); + portal.maxAreaTravelTime = 0; + if (!source.ExpectTokenString(")")) return false; + portals.Append(portal); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseClusters(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + clusters.Clear(); + if (!clusters.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Cluster_t cluster{}; + if (!BeginRecord(source)) return false; + cluster.numAreas = source.ParseInt(); + cluster.numReachableAreas = source.ParseInt(); + cluster.firstPortal = source.ParseInt(); + cluster.numPortals = source.ParseInt(); + if (!source.ExpectTokenString(")")) return false; + clusters.Append(cluster); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseObstaclePVS(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + obstaclePVS.Clear(); + if (!obstaclePVS.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + if (!BeginRecord(source)) return false; + const auto value = static_cast(source.ParseInt()); + if (!source.ExpectTokenString(")")) return false; + obstaclePVS.Append(value); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParsePlanes(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + planes.Clear(); + if (!planes.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + source.ParseInt(); + float values[4] = {}; + if (!source.Parse1DMatrix(4, values, false)) return false; + planes.Append(idPlane(values[0], values[1], values[2], -values[3])); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseReachabilities(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + reachabilities.Clear(); + if (!reachabilities.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Reachability_t reachability{}; + reachability.next.Invalidate(); + reachability.rev_next.Invalidate(); + if (!BeginRecord(source)) return false; + reachability.travelFlags = source.ParseUnsignedInt(); + reachability.travelTime = static_cast(source.ParseInt()); + reachability.fromAreaNum = static_cast(source.ParseInt()); + reachability.toAreaNum = static_cast(source.ParseInt()); + if (!source.ExpectTokenString("(")) return false; + for (int axis = 0; axis < 3; ++axis) { + reachability.start[axis] = static_cast(source.ParseInt()); + } + if (!source.ExpectTokenString(")") + || !source.ExpectTokenString("(")) return false; + for (int axis = 0; axis < 3; ++axis) { + reachability.end[axis] = static_cast(source.ParseInt()); + } + if (!source.ExpectTokenString(")") + || !source.ExpectTokenString(")")) return false; + reachabilities.Append(reachability); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseAreas(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + areas.Clear(); + if (!areas.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + aas2Area_t area; + area.reach.Invalidate(); + area.rev_reach.Invalidate(); + area.firstChokePoint = 0; + area.numChokePoints = 0; + if (!BeginRecord(source)) return false; + area.flags = static_cast(source.ParseInt()); + area.travelFlags = source.ParseUnsignedInt(); + area.numEdges = static_cast(source.ParseInt()); + area.firstEdge = source.ParseInt(); + area.cluster = static_cast(source.ParseInt()); + area.clusterAreaNum = static_cast(source.ParseInt()); + area.obstaclePVSOffset = source.ParseUnsignedInt(); + area.firstCover = static_cast(source.ParseInt()); + area.numCover = static_cast(source.ParseInt()); + area.firstTraversal = static_cast(source.ParseInt()); + area.numTraversals = static_cast(source.ParseInt()); + area.firstHintNode = static_cast(source.ParseInt()); + area.numHintNodes = static_cast(source.ParseInt()); + if (!source.ExpectTokenString(")")) return false; + areas.Append(area); + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseCover(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + cover.Clear(); + if (!cover.SetNum(count)) return false; + for (int index = 0; index < count; ++index) { + if (!BeginRecord(source)) return false; + aas2Cover_t& entry = cover[index]; + const float originX = source.ParseFloat(); + const float originY = source.ParseFloat(); + const float originZ = source.ParseFloat(); + entry.origin.Set(originX, originY, originZ); + const float dirX = source.ParseFloat(); + const float dirY = source.ParseFloat(); + const float dirZ = source.ParseFloat(); + entry.dir.Set(dirX, dirY, dirZ); + entry.areaNum = static_cast(source.ParseInt()); + entry.flags = static_cast(source.ParseInt()); + entry.firstTouching = source.ParseInt(); + entry.numTouching = source.ParseInt(); + if (source.CheckTokenString(")")) continue; + entry.durationSec = source.ParseFloat(); + entry.minRange = source.ParseFloat(); + entry.maxRange = source.ParseFloat(); + if (!source.ExpectTokenString(")")) return false; + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseTraversalPoints(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + traversalPoints.Clear(); + if (!traversalPoints.SetNum(count)) return false; + for (int index = 0; index < count; ++index) { + if (!BeginRecord(source)) return false; + aas2Traversal_t& traversal = traversalPoints[index]; + traversal.Clear(); + const float startX = source.ParseFloat(); + const float startY = source.ParseFloat(); + const float startZ = source.ParseFloat(); + traversal.startPoint.Set(startX, startY, startZ); + const float endX = source.ParseFloat(); + const float endY = source.ParseFloat(); + const float endZ = source.ParseFloat(); + traversal.endPoint.Set(endX, endY, endZ); + traversal.orientationFwd.x = static_cast(source.ParseInt()); + traversal.orientationFwd.y = static_cast(source.ParseInt()); + traversal.orientationFwd.z = static_cast(source.ParseInt()); + traversal.extrusionFwd.x = static_cast(source.ParseInt()); + traversal.extrusionFwd.y = static_cast(source.ParseInt()); + traversal.extrusionFwd.z = static_cast(source.ParseInt()); + traversal.extrusionDistance = static_cast(source.ParseInt()); + traversal.startAreaNum = static_cast(source.ParseInt()); + traversal.endAreaNum = static_cast(source.ParseInt()); + traversal.animIndex = idIndex( + static_cast(source.ParseInt())); + traversal.reachabilityIndex = idIndex( + static_cast(source.ParseInt())); + traversal.flags = source.ParseUnsignedInt(); + traversal.dependencyIndex = idIndex( + static_cast(source.ParseInt())); + traversal.interactionEntIndex = idIndex( + static_cast(source.ParseInt())); + traversal.traversalNameIndex = idIndex( + static_cast(source.ParseInt())); + if (!source.ExpectTokenString(")")) return false; + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseHintNodes(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + hintNodes.Clear(); + if (!hintNodes.SetNum(count)) return false; + for (int index = 0; index < count; ++index) { + if (!BeginRecord(source)) return false; + aas2HintNode_t& hint = hintNodes[index]; + const float originX = source.ParseFloat(); + const float originY = source.ParseFloat(); + const float originZ = source.ParseFloat(); + hint.origin.Set(originX, originY, originZ); + hint.areaNum = static_cast(source.ParseInt()); + hint.radius = static_cast(source.ParseInt()); + hint.hintType = static_cast(source.ParseInt()); + hint.orientation = static_cast(source.ParseInt()); + hint.dirFlags = static_cast(source.ParseInt()); + hint.grouping = static_cast(source.ParseInt()); + hint.hintData = source.ParseInt(); + if (!source.ExpectTokenString(")")) return false; + } + return source.ExpectTokenString("}"); +} + +bool idAAS2File::ParseTrees(idLexer& source) { + int count = 0; + if (!BeginArray(source, count)) return false; + trees.Clear(); + if (!trees.PreAllocate(count)) return false; + for (int index = 0; index < count; ++index) { + bspTree_t tree{}; + source.ParseInt(); + if (!source.ExpectTokenString("(")) return false; + const float normalX = source.ParseFloat(); + const float normalY = source.ParseFloat(); + const float normalZ = source.ParseFloat(); + tree.floorNormal.Set(normalX, normalY, normalZ); + if (!source.ExpectTokenString(")")) return false; + tree.headNode = source.ParseInt(); + tree.firstArea = source.ParseInt(); + tree.lastArea = source.ParseInt(); + trees.Append(tree); + } + return source.ExpectTokenString("}"); +} + +void idAAS2File::BuildReachabilityChains() { + for (int areaNum = 0; areaNum < areas.Num(); ++areaNum) { + areas[areaNum].reach.Invalidate(); + areas[areaNum].rev_reach.Invalidate(); + } + for (int index = 0; index < reachabilities.Num(); ++index) { + aas2Reachability_t& reachability = reachabilities[index]; + if (reachability.fromAreaNum < areas.Num()) { + reachability.next = areas[reachability.fromAreaNum].reach; + areas[reachability.fromAreaNum].reach = + idIndex(static_cast(index)); + } + if (reachability.toAreaNum < areas.Num()) { + reachability.rev_next = areas[reachability.toAreaNum].rev_reach; + areas[reachability.toAreaNum].rev_reach = + idIndex(static_cast(index)); + } + } +} + +void idAAS2File::CalculateAreaBounds() { + areaBounds.SetNum(areas.Num()); + for (int areaNum = 0; areaNum < areas.Num(); ++areaNum) { + const idBounds bounds = AreaBounds(areaNum); + for (int axis = 0; axis < 3; ++axis) { + areaBounds[areaNum].min[axis] = ClampShort(std::floor(bounds[0][axis])); + areaBounds[areaNum].max[axis] = ClampShort(std::ceil(bounds[1][axis])); + } + } +} + +bool idAAS2File::LoadText(const char* const fileName, + const std::uint32_t sourceTimestamp) { + idLexer source(564); + if (!source.LoadFile(fileName, false)) return false; + + idToken identifier; + idToken version; + if (!source.ReadToken(identifier) || !source.ReadToken(version)) return false; + if (std::strcmp(identifier.c_str(), "DewmAAS") != 0 + && std::strcmp(identifier.c_str(), "DewmAAS2") != 0 + && std::strcmp(identifier.c_str(), "AAS2") != 0) { + return false; + } + int parsedMajor = 0; + int parsedMinor = 0; + if (std::sscanf(version.c_str(), "%d.%d", &parsedMajor, &parsedMinor) != 2 + || parsedMajor != 3 || parsedMinor != 13) { + return false; + } + const std::uint32_t parsedCRC = source.ParseUnsignedInt(); + if (!source.ExpectTokenString("firstFakeVertex")) return false; + const int parsedFirstFakeVertex = source.ParseInt(); + if (!source.ExpectTokenString("firstFakeEdge")) return false; + const int parsedFirstFakeEdge = source.ParseInt(); + if (!source.ExpectTokenString("firstFakeEdgeIndex")) return false; + const int parsedFirstFakeEdgeIndex = source.ParseInt(); + if (!source.ExpectTokenString("firstFakeArea")) return false; + const int parsedFirstFakeArea = source.ParseInt(); + + Clear(); + major = parsedMajor; + minor = parsedMinor; + crc = parsedCRC; + timestamp = sourceTimestamp; + firstFakeVertex = parsedFirstFakeVertex; + firstFakeEdge = parsedFirstFakeEdge; + firstFakeEdgeIndex = parsedFirstFakeEdgeIndex; + firstFakeArea = parsedFirstFakeArea; + + idToken section; + while (source.ReadToken(section)) { + const char* const name = section.c_str(); + bool ok = false; + if (std::strcmp(name, "settings") == 0) ok = settings.ReadFromFile(source); + else if (std::strcmp(name, "planes") == 0) ok = ParsePlanes(source); + else if (std::strcmp(name, "vertices") == 0) ok = ParseVertices(source); + else if (std::strcmp(name, "edges") == 0) ok = ParseEdges(source); + else if (std::strcmp(name, "edgeIndex") == 0) ok = ParseIndex(source, edgeIndex); + else if (std::strcmp(name, "reachabilities") == 0) ok = ParseReachabilities(source); + else if (std::strcmp(name, "areas") == 0) ok = ParseAreas(source); + else if (std::strcmp(name, "nodes") == 0) ok = ParseNodes(source); + else if (std::strcmp(name, "portals") == 0) ok = ParsePortals(source); + else if (std::strcmp(name, "portalIndex") == 0) ok = ParseIndex(source, portalIndex); + else if (std::strcmp(name, "clusters") == 0) ok = ParseClusters(source); + else if (std::strcmp(name, "obstaclePVS") == 0) ok = ParseObstaclePVS(source); + else if (std::strcmp(name, "reachNames") == 0) ok = ParseNames(source, reachabilityNames); + else if (std::strcmp(name, "traversalAnimNames") == 0 + || std::strcmp(name, "animNames") == 0) ok = ParseInteractionEntityNames(source, animNames); + else if (std::strcmp(name, "dependencyNames") == 0) ok = ParseInteractionEntityNames(source, dependencyNames); + else if (std::strcmp(name, "interactionEntityNames") == 0) ok = ParseInteractionEntityNames(source, interactionEntityNames); + else if (std::strcmp(name, "traversalEntityNames") == 0) ok = ParseInteractionEntityNames(source, traversalEntityNames); + else if (std::strcmp(name, "cover") == 0) ok = ParseCover(source); + else if (std::strcmp(name, "areaCoverIndex") == 0) ok = ParseIndex(source, areaCoverIndex); + else if (std::strcmp(name, "touchingCoverIndex") == 0) ok = ParseIndex(source, touchingCoverIndex); + else if (std::strcmp(name, "traversalPoints") == 0) ok = ParseTraversalPoints(source); + else if (std::strcmp(name, "hintNodes") == 0) ok = ParseHintNodes(source); + else if (std::strcmp(name, "trees") == 0) ok = ParseTrees(source); + else return false; + if (!ok) return false; + } + + if (trees.Num() == 0) { + bspTree_t tree{}; + tree.floorNormal = -settings.gravityDir; + tree.headNode = 1; + tree.firstArea = 1; + tree.lastArea = areas.Num(); + trees.Append(tree); + } + BuildReachabilityChains(); + CalculateAreaBounds(); + return true; +} diff --git a/source/engine/aas2file/aastraversalchaindata.h b/source/engine/aas2file/aastraversalchaindata.h index 96fc1b2..232c398 100644 --- a/source/engine/aas2file/aastraversalchaindata.h +++ b/source/engine/aas2file/aastraversalchaindata.h @@ -1,69 +1,85 @@ #pragma once -// Reconstructed C++ declarations from IDA Local Types and PDB/DIA metadata. -// Original PDB header: w:\tech5\engine\aas2file\aastraversalchaindata.h -// Recovered logical types: 6 -// Signatures retain Xbox 360 ABI evidence and may still require manual review. +#include "idlib/index.h" +#include "idlib/math/vector.h" +#include -// IDA Local Type ordinal 2227; PDB kind: enum. -enum idAASTraversalChainData::localFlags_t : __int32 -{ - EVASION_ONLY = 0x1, - EMERGENCY_ONLY = 0x2, - COMBAT_ONLY = 0x4, - ENABLED = 0x8, - RUN_WHEN_DONE = 0x10, - DELTA_CORRECT = 0x20, - MAX_FLAG_BITS = 0x6, +enum aasType_t : int { + AAS_MONSTER16 = 0, + AAS_MONSTER24 = 1, + AAS_MONSTER32 = 2, + AAS_MONSTER48 = 3, + AAS_MONSTER96 = 4, + AAS_MONSTER128 = 5, + AAS_MONSTER160 = 6, + AAS_FILE_EXTENSION_MAX = 8 }; -// IDA Local Type ordinal 2933; PDB kind: enum. -enum idAASTraversalChainData::localClass_t : __int32 -{ - CLASS_A = 0x1, - CLASS_B = 0x2, - CLASS_C = 0x4, - CLASS_D = 0x8, - CLASS_E = 0x10, - MAX_CLASS_BITS = 0x5, +enum invalidReachability_t : int { INVALID_REACHABILITY_INDEX = -1 }; +enum invalidAASAnimIndex_t : int { INVALID_AAS_ANIM_INDEX = -1 }; +enum invalidAASTree_t : int { INVALID_AAS_TREE_INDEX = -1 }; +enum invalidAASDependencyIndex_t : int { INVALID_AAS_DEPENDENCY_NODE = -1 }; +enum invalidAASInteractionEntIndex_t : int { INVALID_AAS_INTERACTION_ENT = -1 }; +enum invalidAASTraversalNameIndex_t : int { INVALID_AAS_TRAVERSAL_NAME = -1 }; + +struct aas2Traversal_t { + idVec3 startPoint; + idVec3 endPoint; + idQuantizedVec3 orientationFwd; + idQuantizedVec3 extrusionFwd; + idIndex animIndex; + idIndex reachabilityIndex; + idIndex dependencyIndex; + idIndex interactionEntIndex; + std::int16_t extrusionDistance; + std::int16_t startAreaNum; + std::int16_t endAreaNum; + idIndex traversalNameIndex; + std::uint32_t flags; + + aas2Traversal_t(); + void Clear(); + void CalcExtrusionPoint(idVec3& extrusionPoint, const idVec3& up) const; }; -// IDA Local Type ordinal 14103; PDB kind: struct. -const struct aas2Traversal_t -{ - idVec3 startPoint; - idVec3 endPoint; - idQuantizedVec3 orientationFwd; - idQuantizedVec3 extrusionFwd; - idIndex animIndex; - idIndex reachabilityIndex; - idIndex dependencyIndex; - idIndex interactionEntIndex; - __int16 extrusionDistance; - __int16 startAreaNum; - __int16 endAreaNum; - idIndex traversalNameIndex; - unsigned int flags; -}; +using aasTraversalNameIndex_t = + idIndex; -// IDA Local Type ordinal 20467; PDB kind: class. -class idAASTraversalChainData -{ +class idDeclAnimWeb; + +class idAASTraversalChainData { public: - aasType_t mAASType; - idAASTraversalChainData::localClass_t mClass; - idAASTraversalChainData::localFlags_t mFlags; - idVec3 mExtrusionEndpoint; - const idDeclAnimWeb *mAnimWeb; + enum localFlags_t : int { + EVASION_ONLY = 0x1, + EMERGENCY_ONLY = 0x2, + COMBAT_ONLY = 0x4, + ENABLED = 0x8, + RUN_WHEN_DONE = 0x10, + DELTA_CORRECT = 0x20, + MAX_FLAG_BITS = 6 + }; + enum localClass_t : int { + CLASS_A = 0x1, + CLASS_B = 0x2, + CLASS_C = 0x4, + CLASS_D = 0x8, + CLASS_E = 0x10, + MAX_CLASS_BITS = 5 + }; + + struct flagMap_t { int gameFlag; int aasFlag; }; + + aasType_t mAASType; + localClass_t mClass; + localFlags_t mFlags; + idVec3 mExtrusionEndpoint; + const idDeclAnimWeb* mAnimWeb; }; -// IDA Local Type ordinal 22459; PDB kind: struct. -struct idAASTraversalChainData::flagMap_t -{ - int gameFlag; - int aasFlag; -}; - -// IDA Local Type ordinal 27754; PDB kind: typedef. -typedef idIndex aasTraversalNameIndex_t; +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(aas2Traversal_t) == 56, + "Recovered aas2Traversal_t ABI changed"); +static_assert(sizeof(idAASTraversalChainData) == 28, + "Recovered idAASTraversalChainData ABI changed"); +#endif