diff --git a/source/shared/idlib/analysisclient.cpp b/source/shared/idlib/analysisclient.cpp new file mode 100644 index 0000000..5fcbabb --- /dev/null +++ b/source/shared/idlib/analysisclient.cpp @@ -0,0 +1,206 @@ +#include "analysisclient.h" + +#ifdef nullptr +#undef nullptr +#endif +#ifdef snprintf +#undef snprintf +#endif + +#include +#include +#include + +idCVar mq_analysisReportRate( + "mq_analysisReportRate", "1", CVAR_INTEGER, + "Control the rate at which asserts and map stats get reported."); +idCVar mq_enable( + "mq_enable", "0", CVAR_BOOL, + "Enables the analysis client"); + +idAnalysisClient analysisClient; + +namespace { + +std::string CurrentTimestamp() { + std::time_t now = std::time(0); + std::tm utcTime = {}; + gmtime_s(&utcTime, &now); + char buffer[32] = {}; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &utcTime); + return buffer; +} + +std::string EventTimestamp(const char* timestamp) { + return timestamp == 0 || timestamp[0] == 0 + ? CurrentTimestamp() : std::string(timestamp); +} + +int NextReportTime() { + const int seconds = std::max(1, mq_analysisReportRate.GetInteger()); + return Sys_Milliseconds() + seconds * 1000; +} + +} // namespace + +idAnalysisClient::idAnalysisClient() + : idMQClientThread(), reportTime(0), channel(nullptr), currentMap(), + assertMutex(), mapLoadMutex(), viewNoteMutex(), pendingAsserts(), + pendingMapLoads(), pendingViewNotes() { +} + +idAnalysisClient::~idAnalysisClient() { + StopThread(true); +} + +void idAnalysisClient::StartMessageSystem() { + if (mq_enable.GetBool()) { + StartThread("AMQP idAnalysisClient >> Server"); + } +} + +void idAnalysisClient::StopMessageSystem() { + StopThread(true); +} + +void idAnalysisClient::SetCurrentMap(const char* mapName) { + currentMap = mapName == nullptr ? "" : mapName; +} + +void idAnalysisClient::QueueAssert(const idreports::AssertReport& report, + const char* timestamp) { + idScopedCriticalSection lock(assertMutex); + pendingEvent_t event; + event.timestamp = EventTimestamp(timestamp); + event.message = report; + pendingAsserts.push_back(event); +} + +void idAnalysisClient::QueueMapLoad(const idreports::MapReport& report, + const char* timestamp) { + idScopedCriticalSection lock(mapLoadMutex); + pendingEvent_t event; + event.timestamp = EventTimestamp(timestamp); + event.message = report; + pendingMapLoads.push_back(event); +} + +void idAnalysisClient::QueueViewNote(const idreports::ViewNoteReport& report, + const char* timestamp) { + idScopedCriticalSection lock(viewNoteMutex); + pendingEvent_t event; + event.timestamp = EventTimestamp(timestamp); + event.message = report; + pendingViewNotes.push_back(event); +} + +int idAnalysisClient::NumPendingAsserts() { + idScopedCriticalSection lock(assertMutex); + return static_cast(pendingAsserts.size()); +} + +int idAnalysisClient::NumPendingMapLoads() { + idScopedCriticalSection lock(mapLoadMutex); + return static_cast(pendingMapLoads.size()); +} + +int idAnalysisClient::NumPendingViewNotes() { + idScopedCriticalSection lock(viewNoteMutex); + return static_cast(pendingViewNotes.size()); +} + +void idAnalysisClient::PreRun() { + channel = connection.GetChannel(); + if (channel != nullptr) { + channel->ExchangeDeclare(idStr("idtech5"), idStr("topic"), true, + false); + } + reportTime = NextReportTime(); +} + +void PublishEvent(idreports::LogEvent& logEvent, const char* timestamp, + const idreports::LogEvent_Severity severity, const char* message, + google::protobuf::MessageLite& outgoing, const char* messageType, + idMQChannel* channel, const char* routingKey) { + logEvent.Clear(); + logEvent.set_timestamp(timestamp == nullptr ? "" : timestamp); + logEvent.set_severity(severity); + logEvent.set_message(message == nullptr ? "" : message); + logEvent.set_datatype(messageType == nullptr ? "" : messageType); + + std::string outgoingData; + outgoing.SerializePartialToString(&outgoingData); + logEvent.set_data(outgoingData.data(), outgoingData.size()); + + std::string envelope; + logEvent.SerializePartialToString(&envelope); + if (channel != nullptr) { + channel->BasicPublish(idStr("idtech5"), + idStr(routingKey == nullptr ? "" : routingKey), false, false, + envelope.data(), static_cast(envelope.size())); + } + logEvent.Clear(); +} + +void idAnalysisClient::ThreadSlice() { + if (reportTime >= Sys_Milliseconds()) { + Sleep(25); + return; + } + + std::vector > asserts; + std::vector > mapLoads; + std::vector > viewNotes; + { + idScopedCriticalSection lock(assertMutex); + asserts.swap(pendingAsserts); + } + { + idScopedCriticalSection lock(mapLoadMutex); + mapLoads.swap(pendingMapLoads); + } + { + idScopedCriticalSection lock(viewNoteMutex); + viewNotes.swap(pendingViewNotes); + } + + idreports::LogEvent logEvent; + for (std::size_t index = 0; index < asserts.size(); ++index) { + char message[1200]; + std::snprintf(message, sizeof(message), "Hit an assert in %s", + asserts[index].message.mapname().c_str()); + PublishEvent(logEvent, asserts[index].timestamp.c_str(), + idreports::LogEvent_Severity_SEV_ERROR, message, + asserts[index].message, "AssertReport", channel, + "idtech5.events.assert_report"); + } + for (std::size_t index = 0; index < mapLoads.size(); ++index) { + char message[1200]; + std::snprintf(message, sizeof(message), "%s loaded the map %s", + mapLoads[index].message.username().c_str(), + mapLoads[index].message.mapname().c_str()); + PublishEvent(logEvent, mapLoads[index].timestamp.c_str(), + idreports::LogEvent_Severity_SEV_WARNING, message, + mapLoads[index].message, "MapReport", channel, + "idtech5.events.map_load"); + } + for (std::size_t index = 0; index < viewNotes.size(); ++index) { + char message[1200]; + std::snprintf(message, sizeof(message), + "%s published a viewnote for %s", + viewNotes[index].message.username().c_str(), + viewNotes[index].message.mappath().c_str()); + PublishEvent(logEvent, viewNotes[index].timestamp.c_str(), + idreports::LogEvent_Severity_SEV_INFO, message, + viewNotes[index].message, "ViewNoteReport", channel, + "idtech5.events.viewnote"); + } + reportTime = NextReportTime(); + Sleep(250); +} + +void idAnalysisClient::OnThreadTerminate() { + channel = nullptr; + idLib::Printf("idAnalysisClient::OnThreadTerminate \n"); +} + diff --git a/source/shared/idlib/analysisclient.h b/source/shared/idlib/analysisclient.h new file mode 100644 index 0000000..7ef358c --- /dev/null +++ b/source/shared/idlib/analysisclient.h @@ -0,0 +1,96 @@ +#pragma once + +#include "idlib/networking/amqp/mqmessaging.h" +#include "idlib/networking/protocols/reports.pb.h" + +#include +#include + +struct mapLoadEvent_t { + const char* timestamp; + idreports::MapReport message; +}; + +struct viewNoteEvent_t { + const char* timestamp; + idreports::ViewNoteReport message; +}; + +struct assertEvent_t { + const char* timestmap; // Original spelling retained for recovered ABI users. + idreports::AssertReport message; +}; + +struct idUniqueWarning { + idStr message; + unsigned int count; +}; + +struct viewNoteData_t { + idStr bugTitle; + idStr taskType; + idStr reproSteps; + idStr details; + idStr severity; + idStr priority; + idStr attachmentFilename; + idStr gameName; + bool isMultiplayer; + int buildNumberMajor; + int buildNumberMinor; + idStr vt_filePath; + idStr vt_filePathOverride; + idStr mapName; +}; + +class idAnalysisClient : public idMQClientThread { +public: + idAnalysisClient(); + ~idAnalysisClient() override; + + void StartMessageSystem() override; + void StopMessageSystem() override; + void PreRun() override; + void ThreadSlice() override; + void OnThreadTerminate() override; + + void SetCurrentMap(const char* mapName); + void QueueAssert(const idreports::AssertReport& report, + const char* timestamp = 0); + void QueueMapLoad(const idreports::MapReport& report, + const char* timestamp = 0); + void QueueViewNote(const idreports::ViewNoteReport& report, + const char* timestamp = 0); + + int NumPendingAsserts(); + int NumPendingMapLoads(); + int NumPendingViewNotes(); + + int reportTime; + idMQChannel* channel; + idStrStatic<1024> currentMap; + idSysMutex assertMutex; + idSysMutex mapLoadMutex; + idSysMutex viewNoteMutex; + +private: + template + struct pendingEvent_t { + std::string timestamp; + T message; + }; + + std::vector > pendingAsserts; + std::vector > pendingMapLoads; + std::vector > pendingViewNotes; +}; + +extern idCVar mq_enable; +extern idCVar mq_analysisReportRate; +extern idAnalysisClient analysisClient; + +void PublishEvent(idreports::LogEvent& logEvent, const char* timestamp, + idreports::LogEvent_Severity severity, const char* message, + google::protobuf::MessageLite& outgoing, const char* messageType, + idMQChannel* channel, const char* routingKey); + diff --git a/source/shared/idlib/blockalloc.h b/source/shared/idlib/blockalloc.h new file mode 100644 index 0000000..9522f5e --- /dev/null +++ b/source/shared/idlib/blockalloc.h @@ -0,0 +1,7 @@ +#pragma once + +// The complete allocator implementation survived in Doom 3 BFG's Heap.h. +// Tungsten split the same fixed-block and dynamic-block templates into this +// path; retain that source-level include path while using the vetted PC code. +#include "idlib/precompiled.h" + diff --git a/source/shared/idlib/bv/bounds.h b/source/shared/idlib/bv/bounds.h new file mode 100644 index 0000000..50902fc --- /dev/null +++ b/source/shared/idlib/bv/bounds.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../math/vector.h" + +// Minimal tungsten ABI facade used while the BFG idBounds implementation is +// still the baseline for the larger geometry subsystem. +class idBounds { +public: + idVec3 b[2]; + + const idVec3& operator[](const int index) const { + return b[index]; + } + + idVec3& operator[](const int index) { + return b[index]; + } +}; + +static_assert(sizeof(idBounds) == 24, "Recovered idBounds layout changed"); diff --git a/source/shared/idlib/bv/bounds2d.cpp b/source/shared/idlib/bv/bounds2d.cpp new file mode 100644 index 0000000..4c1b50f --- /dev/null +++ b/source/shared/idlib/bv/bounds2d.cpp @@ -0,0 +1,43 @@ +#include "bounds2d.h" + +bool idBounds2D::AddPoint(const idVec2& point) { + bool expanded = false; + + if (point.x < bounds[0].x) { + bounds[0].x = point.x; + expanded = true; + } + if (point.x > bounds[1].x) { + bounds[1].x = point.x; + expanded = true; + } + if (point.y < bounds[0].y) { + bounds[0].y = point.y; + expanded = true; + } + if (point.y > bounds[1].y) { + bounds[1].y = point.y; + expanded = true; + } + + return expanded; +} + +bool idBounds2D::ContainsPoint(const idVec2& point) const { + return point.x >= bounds[0].x + && point.x <= bounds[1].x + && point.y >= bounds[0].y + && point.y <= bounds[1].y; +} + +bool idBounds2D::IntersectBounds(const idBounds2D& other) const { + if (&other == this) { + return true; + } + + return other.bounds[0].x <= bounds[1].x + && other.bounds[1].x >= bounds[0].x + && other.bounds[0].y <= bounds[1].y + && other.bounds[1].y >= bounds[0].y; +} + diff --git a/source/shared/idlib/bv/bounds2d.h b/source/shared/idlib/bv/bounds2d.h new file mode 100644 index 0000000..2cd8d90 --- /dev/null +++ b/source/shared/idlib/bv/bounds2d.h @@ -0,0 +1,23 @@ +#pragma once + +#include "../math/vector.h" + +// tungsten.exe.h type 13101. +class idBounds2D { +public: + idVec2 bounds[2]; + + bool AddPoint(const idVec2& point); + bool ContainsPoint(const idVec2& point) const; + bool IntersectBounds(const idBounds2D& other) const; + + const idVec2& operator[](const int index) const { + return bounds[index]; + } + + idVec2& operator[](const int index) { + return bounds[index]; + } +}; + +static_assert(sizeof(idBounds2D) == 16, "Recovered idBounds2D layout changed"); diff --git a/source/shared/idlib/bv/boundsshort.h b/source/shared/idlib/bv/boundsshort.h new file mode 100644 index 0000000..3b46059 --- /dev/null +++ b/source/shared/idlib/bv/boundsshort.h @@ -0,0 +1,49 @@ +#pragma once + +#include "bounds.h" + +#include +#include +#include + +class idBoundsShort { +public: + std::int16_t b[2][3]; + + void SetBounds(const idBounds& bounds) { + for (int side = 0; side < 2; ++side) { + for (int axis = 0; axis < 3; ++axis) { + const int integerValue = static_cast(bounds[side][axis]); + b[side][axis] = static_cast(std::max( + static_cast(std::numeric_limits::min()), + std::min( + static_cast(std::numeric_limits::max()), + integerValue + ) + )); + } + } + } + + idBounds ToBounds() const { + idBounds result; + for (int side = 0; side < 2; ++side) { + for (int axis = 0; axis < 3; ++axis) { + result[side][axis] = static_cast(b[side][axis]); + } + } + return result; + } + + bool IntersectsBounds(const idBoundsShort& other) const { + for (int axis = 0; axis < 3; ++axis) { + if (b[0][axis] > other.b[1][axis] + || other.b[0][axis] > b[1][axis]) { + return false; + } + } + return true; + } +}; + +static_assert(sizeof(idBoundsShort) == 12, "idBoundsShort ABI changed"); diff --git a/source/shared/idlib/bv/frustum.cpp b/source/shared/idlib/bv/frustum.cpp new file mode 100644 index 0000000..0488418 --- /dev/null +++ b/source/shared/idlib/bv/frustum.cpp @@ -0,0 +1,19 @@ +#include "frustum.h" + +void idFrustum::ToPoints(idVec3 points[8]) const { + const idVec3 nearCenter = origin + axis[0] * dNear; + const idVec3 nearLeft = axis[1] * (dLeft * dNear * invFar); + const idVec3 nearUp = axis[2] * (dUp * dNear * invFar); + points[0] = nearCenter + nearLeft + nearUp; + points[1] = nearCenter - nearLeft + nearUp; + points[2] = nearCenter - nearLeft - nearUp; + points[3] = nearCenter + nearLeft - nearUp; + + const idVec3 farCenter = origin + axis[0] * dFar; + const idVec3 farLeft = axis[1] * dLeft; + const idVec3 farUp = axis[2] * dUp; + points[4] = farCenter + farLeft + farUp; + points[5] = farCenter - farLeft + farUp; + points[6] = farCenter - farLeft - farUp; + points[7] = farCenter + farLeft - farUp; +} diff --git a/source/shared/idlib/bv/frustum.h b/source/shared/idlib/bv/frustum.h new file mode 100644 index 0000000..19e2c8a --- /dev/null +++ b/source/shared/idlib/bv/frustum.h @@ -0,0 +1,32 @@ +#pragma once + +#include "idlib/precompiled.h" + +class idFrustum { +public: + idFrustum() + : origin(vec3_origin), axis(mat3_identity), dNear(0.0f), dFar(0.0f), + dLeft(0.0f), dUp(0.0f), invFar(0.0f) { + } + + idFrustum(const idVec3& newOrigin, const idMat3& newAxis, + const float nearDistance, const float farDistance, + const float farLeft, const float farUp) + : origin(newOrigin), axis(newAxis), dNear(nearDistance), + dFar(farDistance), dLeft(farLeft), dUp(farUp), + invFar(farDistance != 0.0f ? 1.0f / farDistance : 0.0f) { + } + + void ToPoints(idVec3 points[8]) const; + +private: + idVec3 origin; + idMat3 axis; + float dNear; + float dFar; + float dLeft; + float dUp; + float invFar; +}; + +static_assert(sizeof(idFrustum) == 68, "Recovered idFrustum ABI changed"); diff --git a/source/shared/idlib/color.cpp b/source/shared/idlib/color.cpp new file mode 100644 index 0000000..4abd750 --- /dev/null +++ b/source/shared/idlib/color.cpp @@ -0,0 +1,190 @@ +#include "color.h" + +#include +#include +#include +#include +#include + +const idColor idColor::colorBlack(0.00f, 0.00f, 0.00f, 1.00f); +const idColor idColor::colorWhite(1.00f, 1.00f, 1.00f, 1.00f); +const idColor idColor::colorRed(1.00f, 0.00f, 0.00f, 1.00f); +const idColor idColor::colorGreen(0.00f, 1.00f, 0.00f, 1.00f); +const idColor idColor::colorBlue(0.00f, 0.00f, 1.00f, 1.00f); +const idColor idColor::colorYellow(1.00f, 1.00f, 0.00f, 1.00f); +const idColor idColor::colorMagenta(1.00f, 0.00f, 1.00f, 1.00f); +const idColor idColor::colorCyan(0.00f, 1.00f, 1.00f, 1.00f); +const idColor idColor::colorOrange(1.00f, 0.50f, 0.00f, 1.00f); +const idColor idColor::colorPurple(0.60f, 0.00f, 0.60f, 1.00f); +const idColor idColor::colorPink(0.73f, 0.40f, 0.48f, 1.00f); +const idColor idColor::colorBrown(0.40f, 0.35f, 0.08f, 1.00f); +const idColor idColor::colorLtGrey(0.75f, 0.75f, 0.75f, 1.00f); +const idColor idColor::colorMdGrey(0.50f, 0.50f, 0.50f, 1.00f); +const idColor idColor::colorDkGrey(0.25f, 0.25f, 0.25f, 1.00f); +const idColor idColor::colorDefault = idColor::colorWhite; + +// These axis assignments look unusual but are explicit in the recovered +// dynamic initializers: X=blue, Y=red, Z=green. +const idColor idColor::colorXAxis = idColor::colorBlue; +const idColor idColor::colorYAxis = idColor::colorRed; +const idColor idColor::colorZAxis = idColor::colorGreen; + +namespace { + +constexpr int kStringBufferSize = 4096; + +std::uint8_t FloatToByte(const float value) { + const int integer = static_cast(value * 255.0f); + return static_cast(std::max(0, std::min(255, integer))); +} + +void SkipWhitespace(const char*& cursor) { + while (*cursor != '\0' + && std::isspace(static_cast(*cursor)) != 0) { + ++cursor; + } +} + +bool ParseFloat(const char*& cursor, float& value) { + SkipWhitespace(cursor); + char* end = nullptr; + value = std::strtof(cursor, &end); + if (end == cursor) { + return false; + } + cursor = end; + return true; +} + +void AppendFloat( + char* destination, + const int destinationSize, + int& length, + const float value, + const int precision, + const bool addLeadingSpace +) { + char number[64]; + const int safePrecision = std::max(0, std::min(9, precision)); + std::snprintf(number, sizeof(number), "%.*f", safePrecision, value); + + if (safePrecision > 0) { + int numberLength = static_cast(std::strlen(number)); + while (numberLength > 0 && number[numberLength - 1] == '0') { + number[--numberLength] = '\0'; + } + if (numberLength > 0 && number[numberLength - 1] == '.') { + number[--numberLength] = '\0'; + } + } + + const int written = std::snprintf( + destination + length, + static_cast(destinationSize - length), + addLeadingSpace ? " %s" : "%s", + number + ); + if (written > 0) { + length = std::min(destinationSize - 1, length + written); + } +} + +} // namespace + +const char* idColor::ToString(const int precision, const bool parens) const { + static thread_local int bufferIndex = 0; + static thread_local char buffers[4][kStringBufferSize]; + + char values[kStringBufferSize]; + values[0] = '\0'; + int length = 0; + AppendFloat(values, kStringBufferSize, length, r, precision, false); + AppendFloat(values, kStringBufferSize, length, g, precision, true); + AppendFloat(values, kStringBufferSize, length, b, precision, true); + AppendFloat(values, kStringBufferSize, length, a, precision, true); + + char* const result = buffers[bufferIndex]; + bufferIndex = (bufferIndex + 1) & 3; + if (parens) { + std::snprintf(result, kStringBufferSize, "( %s )", values); + } else { + std::snprintf(result, kStringBufferSize, "%s", values); + } + return result; +} + +void idColor::Lerp( + const idColor& from, + const idColor& to, + const float lerp +) { + if (lerp <= 0.0f) { + *this = from; + } else if (lerp >= 1.0f) { + *this = to; + } else { + r = from.r + (to.r - from.r) * lerp; + g = from.g + (to.g - from.g) * lerp; + b = from.b + (to.b - from.b) * lerp; + a = from.a + (to.a - from.a) * lerp; + } +} + +std::uint32_t idColor::PackColor(const idVec4& color) { + const std::uint32_t red = FloatToByte(color.x); + const std::uint32_t green = FloatToByte(color.y); + const std::uint32_t blue = FloatToByte(color.z); + const std::uint32_t alpha = FloatToByte(color.w); + return red | (green << 8) | (blue << 16) | (alpha << 24); +} + +void idColor::UnpackColor( + const std::uint32_t color, + idVec4& unpackedColor +) { + constexpr float kByteToFloat = 1.0f / 255.0f; + unpackedColor.Set( + static_cast((color >> 0) & 255) * kByteToFloat, + static_cast((color >> 8) & 255) * kByteToFloat, + static_cast((color >> 16) & 255) * kByteToFloat, + static_cast((color >> 24) & 255) * kByteToFloat + ); +} + +bool idColor::SetFromString(const char* str, const bool parseParens) { + if (str == nullptr) { + return false; + } + + const char* cursor = str; + SkipWhitespace(cursor); + if (parseParens) { + if (*cursor != '(') { + return false; + } + ++cursor; + } + + idColor parsed; + if (!ParseFloat(cursor, parsed.r) + || !ParseFloat(cursor, parsed.g) + || !ParseFloat(cursor, parsed.b) + || !ParseFloat(cursor, parsed.a)) { + return false; + } + + SkipWhitespace(cursor); + if (parseParens) { + if (*cursor != ')') { + return false; + } + ++cursor; + SkipWhitespace(cursor); + } + if (*cursor != '\0') { + return false; + } + + *this = parsed; + return true; +} diff --git a/source/shared/idlib/color.h b/source/shared/idlib/color.h new file mode 100644 index 0000000..c659ab3 --- /dev/null +++ b/source/shared/idlib/color.h @@ -0,0 +1,58 @@ +#pragma once + +#include "math/vector.h" + +#include + +// tungsten.exe.h type 12194. +class idColor { +public: + float r; + float g; + float b; + float a; + + idColor() = default; + + idColor( + const float red, + const float green, + const float blue, + const float alpha = 1.0f + ) + : r(red) + , g(green) + , b(blue) + , a(alpha) { + } + + const char* ToString(int precision, bool parens) const; + void Lerp(const idColor& from, const idColor& to, float lerp); + + static std::uint32_t PackColor(const idVec4& color); + static void UnpackColor(std::uint32_t color, idVec4& unpackedColor); + + bool SetFromString(const char* str, bool parseParens); + + static const idColor colorBlack; + static const idColor colorWhite; + static const idColor colorRed; + static const idColor colorGreen; + static const idColor colorBlue; + static const idColor colorYellow; + static const idColor colorMagenta; + static const idColor colorCyan; + static const idColor colorOrange; + static const idColor colorPurple; + static const idColor colorPink; + static const idColor colorBrown; + static const idColor colorLtGrey; + static const idColor colorMdGrey; + static const idColor colorDkGrey; + static const idColor colorDefault; + static const idColor colorXAxis; + static const idColor colorYAxis; + static const idColor colorZAxis; +}; + +static_assert(sizeof(idColor) == 16, "Recovered idColor layout changed"); diff --git a/source/shared/idlib/containers/autoptr.h b/source/shared/idlib/containers/autoptr.h new file mode 100644 index 0000000..48c8295 --- /dev/null +++ b/source/shared/idlib/containers/autoptr.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +template +class idAutoPtr { +public: + explicit idAutoPtr(type* pointer = nullptr) + : Pointee(pointer) { + } + + ~idAutoPtr() { + delete Pointee; + } + + idAutoPtr(idAutoPtr&& other) noexcept + : Pointee(other.Release()) { + } + + idAutoPtr& operator=(idAutoPtr&& other) noexcept { + if (this != &other) { + Reset(other.Release()); + } + return *this; + } + + idAutoPtr(const idAutoPtr&) = delete; + idAutoPtr& operator=(const idAutoPtr&) = delete; + + type* Get() const { return Pointee; } + type* operator->() const { return Pointee; } + type& operator*() const { return *Pointee; } + explicit operator bool() const { return Pointee != nullptr; } + + type* Release() { + type* const result = Pointee; + Pointee = nullptr; + return result; + } + + void Reset(type* pointer = nullptr) { + if (Pointee != pointer) { + delete Pointee; + Pointee = pointer; + } + } + +private: + type* Pointee; +}; + +template +class idAutoPtr_Array { +public: + explicit idAutoPtr_Array(type* pointer = nullptr) + : Pointee(pointer) { + } + + virtual ~idAutoPtr_Array() { + std::free(Pointee); + } + + idAutoPtr_Array(const idAutoPtr_Array&) = delete; + idAutoPtr_Array& operator=(const idAutoPtr_Array&) = delete; + + type* Get() const { return Pointee; } + type& operator[](const int index) const { return Pointee[index]; } + + type* Release() { + type* const result = Pointee; + Pointee = nullptr; + return result; + } + +private: + type* Pointee; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idAutoPtr) == 4, "Recovered idAutoPtr ABI changed"); +static_assert(sizeof(idAutoPtr_Array) == 8, + "Recovered idAutoPtr_Array ABI changed"); +#endif diff --git a/source/shared/idlib/containers/binaryheap.cpp b/source/shared/idlib/containers/binaryheap.cpp new file mode 100644 index 0000000..ad205fb --- /dev/null +++ b/source/shared/idlib/containers/binaryheap.cpp @@ -0,0 +1,5 @@ +#include "binaryheap.h" + +// The recovered BinaryHeap.cpp contains only the testBinaryHeap console +// command. The reusable implementation was inline in BinaryHeap.h; its PC +// regression coverage lives in source/tests/idlib_containers_test.cpp. diff --git a/source/shared/idlib/containers/binaryheap.h b/source/shared/idlib/containers/binaryheap.h new file mode 100644 index 0000000..d313b08 --- /dev/null +++ b/source/shared/idlib/containers/binaryheap.h @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include + +template +class idBinaryHeap { +public: + struct idHeapNode { + nodeType node; + priorityType priority; + }; + + explicit idBinaryHeap(const int initialSize_ = 16) + : nodes(nullptr), curSize(0), initialSize(std::max(1, initialSize_)), + numNodes(0), ordered(true), externalBuffer(false) { + Allocate(initialSize); + } + + idBinaryHeap(idHeapNode* const buffer, const int bufferSize) + : nodes(buffer), curSize(std::max(0, bufferSize - 1)), + initialSize(std::max(0, bufferSize - 1)), numNodes(0), + ordered(true), externalBuffer(true) { + if (nodes != nullptr && bufferSize > 0) { + nodes[0].priority = LOWEST_VALUE; + } + } + + ~idBinaryHeap() { + if (!externalBuffer) { + std::free(nodes); + } + } + + idBinaryHeap(const idBinaryHeap&) = delete; + idBinaryHeap& operator=(const idBinaryHeap&) = delete; + + bool Insert(const nodeType& node, const priorityType& priority) { + if (!ordered) { + return InsertUnsorted(node, priority); + } + if (!EnsureCapacity()) { + return false; + } + int hole = ++numNodes; + while (hole > 1 && priority < nodes[hole / 2].priority) { + nodes[hole] = nodes[hole / 2]; + hole /= 2; + } + nodes[hole].node = node; + nodes[hole].priority = priority; + return true; + } + + bool InsertUnsorted(const nodeType& node, const priorityType& priority) { + if (!EnsureCapacity()) { + return false; + } + ++numNodes; + nodes[numNodes].node = node; + nodes[numNodes].priority = priority; + if (numNodes > 1 && priority < nodes[numNodes / 2].priority) { + ordered = false; + } + return true; + } + + nodeType GetMin() { + SortHeap(); + return numNodes == 0 ? nodeType() : nodes[1].node; + } + + priorityType GetMinPriority() { + SortHeap(); + return numNodes == 0 ? LOWEST_VALUE : nodes[1].priority; + } + + nodeType RemoveMin() { + SortHeap(); + if (numNodes == 0) { + return nodeType(); + } + const nodeType result = nodes[1].node; + nodes[1] = nodes[numNodes--]; + if (numNodes > 0) { + PercolateDown(1); + } + return result; + } + + void SortHeap() { + if (ordered || numNodes < 2) { + ordered = true; + return; + } + for (int index = numNodes / 2; index > 0; --index) { + PercolateDown(index); + } + ordered = true; + } + + void MakeEmpty() { + if (!externalBuffer && curSize != initialSize) { + std::free(nodes); + nodes = nullptr; + curSize = 0; + Allocate(initialSize); + } + numNodes = 0; + ordered = true; + if (nodes != nullptr) { + nodes[0].priority = LOWEST_VALUE; + } + } + + int Num() const { return numNodes; } + bool IsEmpty() const { return numNodes == 0; } + +private: + idHeapNode* nodes; + int curSize; + int initialSize; + int numNodes; + bool ordered; + bool externalBuffer; + + bool Allocate(const int size) { + idHeapNode* const storage = static_cast( + std::malloc(sizeof(idHeapNode) * static_cast(size + 1)) + ); + if (storage == nullptr) { + return false; + } + nodes = storage; + curSize = size; + nodes[0].priority = LOWEST_VALUE; + return true; + } + + bool Resize(const int newSize) { + if (externalBuffer || newSize <= curSize) { + return false; + } + idHeapNode* const replacement = static_cast( + std::malloc(sizeof(idHeapNode) * static_cast(newSize + 1)) + ); + if (replacement == nullptr) { + return false; + } + std::memcpy(replacement, nodes, + sizeof(idHeapNode) * static_cast(numNodes + 1)); + std::free(nodes); + nodes = replacement; + curSize = newSize; + return true; + } + + bool EnsureCapacity() { + if (numNodes < curSize) { + return true; + } + return Resize(std::max(1, curSize * 2)); + } + + void PercolateDown(int hole) { + const idHeapNode value = nodes[hole]; + while (hole * 2 <= numNodes) { + int child = hole * 2; + if (child != numNodes + && nodes[child + 1].priority < nodes[child].priority) { + ++child; + } + if (!(nodes[child].priority < value.priority)) { + break; + } + nodes[hole] = nodes[child]; + hole = child; + } + nodes[hole] = value; + } +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idBinaryHeap) == 20, + "Recovered idBinaryHeap ABI changed"); +#endif diff --git a/source/shared/idlib/containers/bitarray.h b/source/shared/idlib/containers/bitarray.h new file mode 100644 index 0000000..b9c9b38 --- /dev/null +++ b/source/shared/idlib/containers/bitarray.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include + +// Recovered from shared/idlib/containers/BitArray.h. The field order is +// intentionally kept identical to the 32-bit tungsten type. +class idBitArray { +public: + explicit idBitArray(const std::int16_t tag = 0) + : buffer(nullptr), bits(0), memTag(tag), ownsBuffer(false) { + } + + idBitArray(unsigned char* storage, const unsigned int numBits, + const std::int16_t tag = 0) + : buffer(storage), bits(numBits), memTag(tag), ownsBuffer(false) { + Clear(); + } + + ~idBitArray() { + Free(); + } + + idBitArray(const idBitArray&) = delete; + idBitArray& operator=(const idBitArray&) = delete; + + bool Alloc(const unsigned int numBits) { + Free(); + bits = numBits; + ownsBuffer = true; + const std::size_t bytes = ByteCount(); + if (bytes == 0) { + return true; + } + buffer = static_cast(std::calloc(bytes, 1)); + if (buffer == nullptr) { + bits = 0; + ownsBuffer = false; + return false; + } + return true; + } + + void Clear() { + if (buffer != nullptr) { + std::memset(buffer, 0, ByteCount()); + } + } + + void Set(const unsigned int bitNum) { + if (bitNum < bits && buffer != nullptr) { + buffer[bitNum >> 3] |= static_cast(1u << (bitNum & 7)); + } + } + + void Clear(const unsigned int bitNum) { + if (bitNum < bits && buffer != nullptr) { + buffer[bitNum >> 3] &= static_cast(~(1u << (bitNum & 7))); + } + } + + void Set(const unsigned int bitNum, const bool value) { + if (value) { + Set(bitNum); + } else { + Clear(bitNum); + } + } + + bool Get(const unsigned int bitNum) const { + return bitNum < bits && buffer != nullptr + && (buffer[bitNum >> 3] & (1u << (bitNum & 7))) != 0; + } + + unsigned int Num() const { + return bits; + } + +private: + unsigned char* buffer; + unsigned int bits; + std::int16_t memTag; + bool ownsBuffer; + + std::size_t ByteCount() const { + return static_cast((bits + 7u) >> 3); + } + + void Free() { + if (buffer != nullptr && ownsBuffer) { + std::free(buffer); + } + buffer = nullptr; + bits = 0; + ownsBuffer = false; + } +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idBitArray) == 12, "Recovered idBitArray ABI changed"); +#endif diff --git a/source/shared/idlib/containers/btree.cpp b/source/shared/idlib/containers/btree.cpp new file mode 100644 index 0000000..66d29cf --- /dev/null +++ b/source/shared/idlib/containers/btree.cpp @@ -0,0 +1,3 @@ +// Tungsten's containers/BTree.cpp contains only the testBinaryTree console +// command. The reusable idBTree implementation is header-only and is supplied +// by the compiled Doom 3 BFG idlib/containers/BTree.h baseline. diff --git a/source/shared/idlib/containers/list.cpp b/source/shared/idlib/containers/list.cpp new file mode 100644 index 0000000..794eecd --- /dev/null +++ b/source/shared/idlib/containers/list.cpp @@ -0,0 +1,4 @@ +// Tungsten's containers/List.cpp contains diagnostic console commands for the +// header-only idList and idArray implementations. The PC runtime templates are +// supplied by the compiled Doom 3 BFG idlib/containers/List.h baseline; the +// recovery suite exercises container behavior without registering engine CVars. diff --git a/source/shared/idlib/containers/recoveredlist.h b/source/shared/idlib/containers/recoveredlist.h new file mode 100644 index 0000000..33fca1f --- /dev/null +++ b/source/shared/idlib/containers/recoveredlist.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +template +class idRecoveredList { +public: + explicit idRecoveredList(const int initialGranularity = 16) + : list(nullptr), num(0), size(0), + granularity(static_cast(initialGranularity)), + memTag(0), listStatic(0) { + } + + ~idRecoveredList() { + Clear(true); + } + + idRecoveredList(const idRecoveredList&) = delete; + idRecoveredList& operator=(const idRecoveredList&) = delete; + + bool Reserve(const int capacity) { + if (capacity <= size) return true; + const int step = granularity > 0 ? granularity : 16; + const int newSize = ((capacity + step - 1) / step) * step; + T* const replacement = static_cast( + std::malloc(sizeof(T) * static_cast(newSize))); + if (replacement == nullptr) return false; + if (list != nullptr && num > 0) { + std::memcpy(replacement, list, + sizeof(T) * static_cast(num)); + } + std::free(list); + list = replacement; + size = newSize; + return true; + } + + T* Alloc() { + if (!Reserve(num + 1)) return nullptr; + T* const result = list + num++; + std::memset(result, 0, sizeof(T)); + return result; + } + + bool Append(const T& value) { + T* const destination = Alloc(); + if (destination == nullptr) return false; + *destination = value; + return true; + } + + void Clear(const bool freeMemory = false) { + num = 0; + if (freeMemory) { + std::free(list); + list = nullptr; + size = 0; + } + } + + int Num() const { return num; } + int Capacity() const { return size; } + T* Ptr() { return list; } + const T* Ptr() const { return list; } + T& operator[](const int index) { return list[index]; } + const T& operator[](const int index) const { return list[index]; } + +private: + T* list; + int num; + int size; + std::int16_t granularity; + std::uint8_t memTag; + std::uint8_t listStatic; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idRecoveredList) == 16, + "Recovered list ABI changed"); +#endif + diff --git a/source/shared/idlib/containers/search.h b/source/shared/idlib/containers/search.h new file mode 100644 index 0000000..7b9ea00 --- /dev/null +++ b/source/shared/idlib/containers/search.h @@ -0,0 +1,99 @@ +#pragma once + +template +class idSearch { +public: + virtual ~idSearch() = default; + virtual int Search(const type* base, unsigned int num, + const type& value) const = 0; + virtual int Search_FirstGreater(const type* base, int num, + const type& value) const = 0; + virtual int Search_FirstGreaterEqual(const type* base, int num, + const type& value) const = 0; + virtual int Search_LastLess(const type* base, int num, + const type& value) const = 0; + virtual int Search_LastLessEqual(const type* base, int num, + const type& value) const = 0; +}; + +template +class idSearch_Binary : public idSearch { +public: + int Search(const type* base, const unsigned int num, + const type& value) const override { + if (base == nullptr || num == 0) { + return -1; + } + const int index = Search_LastLessEqual(base, static_cast(num), value); + return Compare(base[index], value) == 0 ? index : -1; + } + + int Search_FirstGreater(const type* base, const int num, + const type& value) const override { + int first = 0; + int count = num < 0 ? 0 : num; + while (count > 0) { + const int step = count / 2; + const int middle = first + step; + if (Compare(base[middle], value) <= 0) { + first = middle + 1; + count -= step + 1; + } else { + count = step; + } + } + return first; + } + + int Search_FirstGreaterEqual(const type* base, const int num, + const type& value) const override { + int first = 0; + int count = num < 0 ? 0 : num; + while (count > 0) { + const int step = count / 2; + const int middle = first + step; + if (Compare(base[middle], value) < 0) { + first = middle + 1; + count -= step + 1; + } else { + count = step; + } + } + return first; + } + + int Search_LastLess(const type* base, const int num, + const type& value) const override { + const int result = Search_FirstGreaterEqual(base, num, value) - 1; + return result < 0 ? 0 : result; + } + + int Search_LastLessEqual(const type* base, const int num, + const type& value) const override { + const int result = Search_FirstGreater(base, num, value) - 1; + return result < 0 ? 0 : result; + } + +private: + int Compare(const type& left, const type& right) const { + return static_cast(this)->Compare(left, right); + } +}; + +template +class idSearch_BinaryDefault + : public idSearch_Binary> { +public: + int Compare(const type& left, const type& right) const { + if (left < right) { + return -1; + } + if (right < left) { + return 1; + } + return 0; + } +}; + +template +using idSearch_DefaultCompare = idSearch_BinaryDefault; diff --git a/source/shared/idlib/csystems/autocomplete.cpp b/source/shared/idlib/csystems/autocomplete.cpp new file mode 100644 index 0000000..1e3f19d --- /dev/null +++ b/source/shared/idlib/csystems/autocomplete.cpp @@ -0,0 +1,101 @@ +#include "autocomplete.h" + +#include +#include +#include +#include + +namespace { +int ComparePrefixInsensitive(const char* left, const char* right, + const int count) { + const unsigned char* safeLeft = reinterpret_cast( + left == nullptr ? "" : left + ); + const unsigned char* safeRight = reinterpret_cast( + right == nullptr ? "" : right + ); + for (int index = 0; index < count; ++index) { + const int leftChar = std::tolower(safeLeft[index]); + const int rightChar = std::tolower(safeRight[index]); + if (leftChar != rightChar || safeLeft[index] == '\0' + || safeRight[index] == '\0') { + return leftChar - rightChar; + } + } + return 0; +} +} + +idAutoComplete::~idAutoComplete() { + ClearSuggestions(); +} + +void idAutoComplete::ClearSuggestions() { + if (suggestions.list != nullptr) { + for (int index = 0; index < suggestions.size; ++index) { + suggestions.list[index].~idStr(); + } + std::free(suggestions.list); + } + suggestions.list = nullptr; + suggestions.num = 0; + suggestions.size = 0; +} + +void idAutoComplete::AppendSuggestion(const idStr& suggestion) { + if (suggestions.num == suggestions.size) { + const int newSize = suggestions.size + suggestions.granularity; + idStr* const replacement = static_cast( + std::malloc(sizeof(idStr) * static_cast(newSize)) + ); + if (replacement == nullptr) { + return; + } + + for (int index = 0; index < newSize; ++index) { + new (&replacement[index]) idStr(); + } + for (int index = 0; index < suggestions.num; ++index) { + replacement[index] = suggestions.list[index]; + } + if (suggestions.list != nullptr) { + for (int index = 0; index < suggestions.size; ++index) { + suggestions.list[index].~idStr(); + } + std::free(suggestions.list); + } + suggestions.list = replacement; + suggestions.size = newSize; + } + + suggestions.list[suggestions.num++] = suggestion; +} + +void idAutoComplete::Append( + const idStr& suggestion, + const int completingArg +) { + const int argumentIndex = completingArg < 0 + ? args.Argc() - 1 + : completingArg; + + const char* const partial = args.Argv(argumentIndex); + const int partialLength = static_cast(std::strlen(partial)); + if (partialLength > 0 + && ComparePrefixInsensitive(suggestion.c_str(), partial, partialLength) != 0) { + return; + } + + if (args.Argc() == 1 && argumentIndex == 0) { + AppendSuggestion(suggestion); + return; + } + + idStr completed; + for (int index = 0; index < argumentIndex; ++index) { + completed.Append(args.Argv(index)); + completed.Append(' '); + } + completed.Append(suggestion); + AppendSuggestion(completed); +} diff --git a/source/shared/idlib/csystems/autocomplete.h b/source/shared/idlib/csystems/autocomplete.h new file mode 100644 index 0000000..bb6bebf --- /dev/null +++ b/source/shared/idlib/csystems/autocomplete.h @@ -0,0 +1,69 @@ +#pragma once + +#include "../text/cmdargs.h" +#include "../text/str.h" + +#include + +struct idAutoCompleteStringList { + idStr* list; + int num; + int size; + std::int16_t granularity; + std::uint8_t memTag; + std::uint8_t listStatic; +}; + +static_assert( + sizeof(idAutoCompleteStringList) == 16, + "Recovered idList layout changed" +); + +// tungsten.exe.h type 12218. idCmdArgs and idList retain their recovered +// Win32 layouts through the active BFG-compatible core container layer. +class idAutoComplete { +public: + idAutoComplete() + : matchLength(0) + , currentIndex(-1) + , suggestions{nullptr, 0, 0, 16, 5, 0} { + } + + ~idAutoComplete(); + + void Append(const idStr& suggestion, int completingArg = -1); + + const char* GetArg(const int index) const { + return args.Argv(index); + } + + void Clear() { + matchLength = 0; + currentIndex = -1; + args.Clear(); + ClearSuggestions(); + } + + void SetArgs(const idCmdArgs& newArgs) { + args = newArgs; + } + + int GetNumSuggestions() const { + return suggestions.num; + } + + const idStr& GetSuggestion(const int index) const { + return suggestions.list[index]; + } + +private: + int matchLength; + int currentIndex; + idCmdArgs args; + idAutoCompleteStringList suggestions; + + void AppendSuggestion(const idStr& suggestion); + void ClearSuggestions(); +}; + +static_assert(sizeof(idAutoComplete) == 2332, "idAutoComplete ABI changed"); diff --git a/source/shared/idlib/filesystem/file_metrics.cpp b/source/shared/idlib/filesystem/file_metrics.cpp new file mode 100644 index 0000000..9e935b1 --- /dev/null +++ b/source/shared/idlib/filesystem/file_metrics.cpp @@ -0,0 +1,244 @@ +#include "file_metrics.h" + +#include "idlib/sys/sys_networking.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +idTCP metricsTCP; +unsigned char* sendQueue = nullptr; +int sendQueueSize = 0; +int sendIndex = 0; +int pendingIndex = 0; +char metricsServer[256] = "127.0.0.1"; +unsigned short metricsPort = 8012; +int initialRetryTime = 1000; +int maximumRetryTime = 60000; +int retryTime = 0; +std::uint64_t timeoutWait = 0; +std::mutex metricsMutex; + +std::uint64_t Milliseconds() { + using namespace std::chrono; + return static_cast(duration_cast( + steady_clock::now().time_since_epoch()).count()); +} + +void PutUInt32LE(unsigned char* output, const unsigned int value) { + output[0] = static_cast(value); + output[1] = static_cast(value >> 8); + output[2] = static_cast(value >> 16); + output[3] = static_cast(value >> 24); +} + +int StringBytes(const char* text) { + return static_cast(std::strlen(text == nullptr ? "" : text)) + 1; +} + +} // namespace + +idFile_Metrics::idFile_Metrics(const char* streamName) + : uniqID(0), name(streamName == nullptr ? "" : streamName), bytesSent(0) { +} + +idFile_Metrics::~idFile_Metrics() { + CloseMetricsStream(name.c_str()); +} + +const char* idFile_Metrics::GetFullPath() const { + const netadr_t address = metricsTCP.GetAddress(); + if (metricsTCP.IsOpen()) { + fullpath.Format("metrics://%u.%u.%u.%u:%u/%s", + static_cast(address.ip[0]), + static_cast(address.ip[1]), + static_cast(address.ip[2]), + static_cast(address.ip[3]), + static_cast(address.port), name.c_str()); + } else { + fullpath.Format("metrics://%s:%u/%s", metricsServer, + static_cast(metricsPort), name.c_str()); + } + return fullpath.c_str(); +} + +int idFile_Metrics::Read(void*, int) { + return 0; +} + +int idFile_Metrics::Write(const void* buffer, const int len) { + const int written = WriteInternal(name.c_str(), buffer, len); + bytesSent += written; + return written; +} + +int idFile_Metrics::Seek(long, fsOrigin_t) { + return -1; +} + +void idFile_Metrics::Flush() { + std::lock_guard lock(metricsMutex); + bool queueTraffic = false; + FlushBufferedWrites(queueTraffic); +} + +void idFile_Metrics::ForceFlush() { + Flush(); +} + +idFile_Metrics* idFile_Metrics::OpenMetricsStream(const char* streamName) { + return new idFile_Metrics(streamName); +} + +void idFile_Metrics::CloseMetricsStream(const char* streamName) { + std::lock_guard lock(metricsMutex); + if (!metricsTCP.IsOpen()) return; + + bool queueTraffic = false; + FlushBufferedWrites(queueTraffic); + + static const char control[] = "CONTROL"; + static const char close[] = "CLOSE"; + const char* const safeName = streamName == nullptr ? "" : streamName; + const int payloadSize = static_cast(sizeof(control) + sizeof(close)) + + StringBytes(safeName); + unsigned char prefix[4] = {}; + PutUInt32LE(prefix, static_cast(payloadSize)); + BufferedWriteInternal(queueTraffic, prefix, sizeof(prefix)); + BufferedWriteInternal(queueTraffic, control, sizeof(control)); + BufferedWriteInternal(queueTraffic, close, sizeof(close)); + BufferedWriteInternal(queueTraffic, safeName, StringBytes(safeName)); + if (!queueTraffic) FlushBufferedWrites(queueTraffic); +} + +void idFile_Metrics::ConfigureServer(const char* host, const unsigned short port, + const int initialRetryMilliseconds, + const int maximumRetryMilliseconds) { + std::lock_guard lock(metricsMutex); + metricsTCP.Close(); + const char* const safeHost = host == nullptr || *host == '\0' + ? "127.0.0.1" : host; + std::strncpy(metricsServer, safeHost, sizeof(metricsServer) - 1); + metricsServer[sizeof(metricsServer) - 1] = '\0'; + metricsPort = port; + initialRetryTime = std::max(1, initialRetryMilliseconds); + maximumRetryTime = std::max(initialRetryTime, maximumRetryMilliseconds); + retryTime = 0; + timeoutWait = 0; + sendIndex = pendingIndex = 0; +} + +void idFile_Metrics::ShutdownTransport() { + std::lock_guard lock(metricsMutex); + metricsTCP.Close(); + std::free(sendQueue); + sendQueue = nullptr; + sendQueueSize = sendIndex = pendingIndex = 0; + retryTime = 0; + timeoutWait = 0; +} + +bool idFile_Metrics::EnsureConnection() { + if (metricsTCP.IsOpen()) return true; + if (Milliseconds() < timeoutWait || metricsPort == 0) return false; + + // A blocking connect gives the PC port a definite connected state. All + // subsequent writes retain the non-blocking short-write queue semantics. + if (metricsTCP.Connect(metricsServer, metricsPort, false, true, true)) { + retryTime = 0; + timeoutWait = 0; + return true; + } + + retryTime = retryTime == 0 ? initialRetryTime + : std::min(maximumRetryTime, retryTime * 2); + timeoutWait = Milliseconds() + static_cast(retryTime); + return false; +} + +void idFile_Metrics::WriteToQueue(const void* buffer, const int len) { + if (buffer == nullptr || len <= 0) return; + const int retained = pendingIndex - sendIndex; + const int required = retained + len; + if (required > sendQueueSize) { + int newSize = std::max(1024, sendQueueSize); + while (newSize < required) newSize *= 2; + unsigned char* const replacement = static_cast( + std::malloc(static_cast(newSize))); + if (replacement == nullptr) return; + if (retained > 0) { + std::memcpy(replacement, sendQueue + sendIndex, + static_cast(retained)); + } + std::free(sendQueue); + sendQueue = replacement; + sendQueueSize = newSize; + sendIndex = 0; + pendingIndex = retained; + } else if (sendIndex > 0 && pendingIndex + len > sendQueueSize) { + if (retained > 0) { + std::memmove(sendQueue, sendQueue + sendIndex, + static_cast(retained)); + } + sendIndex = 0; + pendingIndex = retained; + } + std::memcpy(sendQueue + pendingIndex, buffer, static_cast(len)); + pendingIndex += len; +} + +void idFile_Metrics::BufferedWriteInternal(bool& queueTraffic, + const void* buffer, const int len) { + if (buffer == nullptr || len <= 0) return; + if (queueTraffic) { + WriteToQueue(buffer, len); + return; + } + const int written = metricsTCP.Write(buffer, len); + const int validWritten = std::max(0, written); + if (written != len) { + queueTraffic = true; + WriteToQueue(static_cast(buffer) + validWritten, + len - validWritten); + } +} + +void idFile_Metrics::FlushBufferedWrites(bool& queueTraffic) { + if (!metricsTCP.IsOpen() || sendIndex >= pendingIndex) { + if (sendIndex >= pendingIndex) sendIndex = pendingIndex = 0; + return; + } + const int available = pendingIndex - sendIndex; + const int written = metricsTCP.Write(sendQueue + sendIndex, available); + if (written > 0) sendIndex += written; + if (written < available) queueTraffic = true; + if (sendIndex >= pendingIndex) sendIndex = pendingIndex = 0; +} + +void idFile_Metrics::WriteFrame(bool& queueTraffic, const char* streamName, + const void* buffer, const int len) { + const char* const safeName = streamName == nullptr ? "" : streamName; + const int nameBytes = StringBytes(safeName); + unsigned char prefix[4] = {}; + PutUInt32LE(prefix, static_cast(nameBytes + len)); + BufferedWriteInternal(queueTraffic, prefix, sizeof(prefix)); + BufferedWriteInternal(queueTraffic, safeName, nameBytes); + BufferedWriteInternal(queueTraffic, buffer, len); +} + +int idFile_Metrics::WriteInternal(const char* streamName, + const void* buffer, const int len) { + if (buffer == nullptr || len <= 0) return 0; + std::lock_guard lock(metricsMutex); + if (!EnsureConnection()) return 0; + + bool queueTraffic = false; + FlushBufferedWrites(queueTraffic); + WriteFrame(queueTraffic, streamName, buffer, len); + return len; +} diff --git a/source/shared/idlib/filesystem/file_metrics.h b/source/shared/idlib/filesystem/file_metrics.h new file mode 100644 index 0000000..198c33d --- /dev/null +++ b/source/shared/idlib/filesystem/file_metrics.h @@ -0,0 +1,60 @@ +#pragma once + +// The recovery target still uses Doom 3 BFG's portable idFile implementation +// as its base file layer. idFile_Metrics retains the tungsten data layout and +// supplies the additional network-backed stream behavior recovered from the +// Xbox 360 executable. +#include "idlib/precompiled.h" + +// BFG emulates the C++11 keyword for its original compiler. Recovery sources +// use the real C++14 keyword and the standard library. +#ifdef nullptr +#undef nullptr +#endif + +class idFile_Metrics : public idFile { +public: + explicit idFile_Metrics(const char* streamName); + ~idFile_Metrics() override; + + const char* GetName() const override { return name.c_str(); } + const char* GetFullPath() const override; + int Read(void* buffer, int len) override; + int Write(const void* buffer, int len) override; + int Length() const override { return bytesSent; } + int Tell() const override { return bytesSent; } + int Seek(long offset, fsOrigin_t origin) override; + void Flush() override; + void ForceFlush() override; + + static idFile_Metrics* OpenMetricsStream(const char* streamName); + static void CloseMetricsStream(const char* streamName); + + // PC recovery controls. The original values came from metrics_* CVars. + static void ConfigureServer(const char* host, unsigned short port, + int initialRetryMilliseconds = 1000, + int maximumRetryMilliseconds = 60000); + static void ShutdownTransport(); + +private: + static int WriteInternal(const char* streamName, const void* buffer, int len); + static void WriteToQueue(const void* buffer, int len); + static void BufferedWriteInternal(bool& queueTraffic, + const void* buffer, int len); + static void FlushBufferedWrites(bool& queueTraffic); + static bool EnsureConnection(); + static void WriteFrame(bool& queueTraffic, const char* streamName, + const void* buffer, int len); + + // idTech 5 added uniqID to idFile; BFG's portable base predates it. Keep + // the field in the same derived-object position to preserve tungsten ABI. + unsigned int uniqID; + idStr name; + mutable idStr fullpath; + int bytesSent; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idFile_Metrics) == 76, + "Recovered idFile_Metrics ABI changed"); +#endif diff --git a/source/shared/idlib/filesystem/file_mtp.cpp b/source/shared/idlib/filesystem/file_mtp.cpp new file mode 100644 index 0000000..2ed8947 --- /dev/null +++ b/source/shared/idlib/filesystem/file_mtp.cpp @@ -0,0 +1,279 @@ +#include "file_mtp.h" + +#include "idlib/sys/sys_networking.h" + +#include +#include +#include +#include + +namespace { + +idTCP mtpTCP; +char mtpServer[256] = "10.4.20.180"; +unsigned short mtpPort = 2769; +int mtpWriteSize = 1400; +int mtpTimeout = 1000; +std::mutex mtpMutex; + +void PutUInt16BE(unsigned char* output, const unsigned int value) { + output[0] = static_cast(value >> 8); + output[1] = static_cast(value); +} + +void PutUInt32BE(unsigned char* output, const unsigned int value) { + output[0] = static_cast(value >> 24); + output[1] = static_cast(value >> 16); + output[2] = static_cast(value >> 8); + output[3] = static_cast(value); +} + +void PutUInt64BE(unsigned char* output, const std::uint64_t value) { + PutUInt32BE(output, static_cast(value >> 32)); + PutUInt32BE(output + 4, static_cast(value)); +} + +unsigned int GetUInt32BE(const unsigned char* input) { + return (static_cast(input[0]) << 24) + | (static_cast(input[1]) << 16) + | (static_cast(input[2]) << 8) + | static_cast(input[3]); +} + +std::uint64_t GetUInt64BE(const unsigned char* input) { + return (static_cast(GetUInt32BE(input)) << 32) + | GetUInt32BE(input + 4); +} + +} // namespace + +idFile_MTP::idFile_MTP() + : uniqID(0), position(0), mode(MTP_FS_READ), length(0), + timestamp(static_cast(-1)) { +} + +idFile_MTP::~idFile_MTP() = default; + +void idFile_MTP::ConfigureServer(const char* host, const unsigned short port, + const int writeSize, const int timeoutMilliseconds) { + std::lock_guard lock(mtpMutex); + mtpTCP.Close(); + const char* const safeHost = host == nullptr || *host == '\0' + ? "127.0.0.1" : host; + std::strncpy(mtpServer, safeHost, sizeof(mtpServer) - 1); + mtpServer[sizeof(mtpServer) - 1] = '\0'; + mtpPort = port; + mtpWriteSize = std::max(1, writeSize); + mtpTimeout = std::max(1, timeoutMilliseconds); +} + +void idFile_MTP::ShutdownTransport() { + std::lock_guard lock(mtpMutex); + mtpTCP.Close(); +} + +bool idFile_MTP::EnsureConnection() { + return mtpTCP.IsOpen() + || mtpTCP.Connect(mtpServer, mtpPort, false, true, false); +} + +bool idFile_MTP::SendRequest(const std::uint64_t offset, + const unsigned int requestLength, const operation_t operation, + const char* filename) { + if (!EnsureConnection()) return false; + const char* const safeName = filename == nullptr ? "" : filename; + const std::size_t filenameLength = std::min( + std::strlen(safeName), 0xFFFFu); + unsigned char request[16] = {}; + PutUInt64BE(request, offset); + PutUInt32BE(request + 8, requestLength); + PutUInt16BE(request + 12, static_cast(filenameLength)); + PutUInt16BE(request + 14, operation); + if (mtpTCP.WriteBlocking(request, sizeof(request), mtpTimeout) + != sizeof(request)) { + mtpTCP.Close(); + return false; + } + if (filenameLength > 0 && mtpTCP.WriteBlocking(safeName, + static_cast(filenameLength), mtpTimeout) + != static_cast(filenameLength)) { + mtpTCP.Close(); + return false; + } + return true; +} + +bool idFile_MTP::Open(const char* filename, const mtpFileMode_t openMode) { + const char* const safeName = filename == nullptr ? "" : filename; + name = safeName; + fullPath.Format("MTP:%s", safeName); + mode = openMode; + position = 0; + + std::lock_guard lock(mtpMutex); + if (!SendRequest(0, static_cast(openMode), OP_OPEN, + name.c_str())) return false; + unsigned char stats[16] = {}; + if (mtpTCP.ReadBlocking(stats, sizeof(stats), mtpTimeout) + != sizeof(stats)) { + mtpTCP.Close(); + return false; + } + length = GetUInt64BE(stats); + timestamp = GetUInt32BE(stats + 8); + if (timestamp == static_cast(-1)) return false; + if (mode == MTP_FS_APPEND) position = length; + return true; +} + +int idFile_MTP::Read(void* buffer, const int len) { + return ReadOfs(static_cast(position), buffer, len); +} + +int idFile_MTP::ReadOfs(const std::int64_t offset, void* buffer, const int len) { + if (buffer == nullptr || len <= 0 || offset < 0 + || (mode != MTP_FS_READ && mode != MTP_FS_READ_WRITE + && mode != MTP_FS_READ_NO_BUFFERING)) return 0; + std::lock_guard lock(mtpMutex); + + for (int attempt = 0; attempt < 5; ++attempt) { + if (!SendRequest(static_cast(offset), + static_cast(len), OP_READ, name.c_str())) { + mtpTCP.Close(); + continue; + } + int total = 0; + bool failed = false; + while (total < len) { + unsigned char sizeBytes[4] = {}; + if (mtpTCP.ReadBlocking(sizeBytes, sizeof(sizeBytes), mtpTimeout) + != sizeof(sizeBytes)) { + failed = true; + break; + } + const unsigned int chunk = GetUInt32BE(sizeBytes); + if (chunk == 0) break; + if (chunk > static_cast(len - total) + || mtpTCP.ReadBlocking(static_cast(buffer) + + total, static_cast(chunk), mtpTimeout) + != static_cast(chunk)) { + failed = true; + break; + } + total += static_cast(chunk); + } + if (!failed) { + position = static_cast(offset) + total; + return total; + } + mtpTCP.Close(); + } + return 0; +} + +int idFile_MTP::Write(const void* buffer, const int len) { + return WriteOfs(static_cast(position), buffer, len); +} + +int idFile_MTP::WriteOfs(const std::int64_t offset, const void* buffer, + const int len) { + if (buffer == nullptr || len <= 0 || offset < 0 + || (mode != MTP_FS_WRITE && mode != MTP_FS_READ_WRITE + && mode != MTP_FS_APPEND)) return 0; + std::lock_guard lock(mtpMutex); + int total = 0; + while (total < len) { + const int chunk = std::min(mtpWriteSize, len - total); + if (!SendRequest(static_cast(offset) + total, + static_cast(chunk), OP_WRITE, name.c_str()) + || mtpTCP.WriteBlocking(static_cast(buffer) + + total, chunk, mtpTimeout) != chunk) { + mtpTCP.Close(); + break; + } + unsigned char countBytes[4] = {}; + if (mtpTCP.ReadBlocking(countBytes, sizeof(countBytes), mtpTimeout) + != sizeof(countBytes)) { + mtpTCP.Close(); + break; + } + const int accepted = static_cast(GetUInt32BE(countBytes)); + if (accepted <= 0 || accepted > chunk) { + mtpTCP.Close(); + break; + } + total += accepted; + if (accepted != chunk) break; + } + position = static_cast(offset) + total; + length = std::max(length, position); + return total; +} + +void idFile_MTP::SetLength(const unsigned int len) { + std::lock_guard lock(mtpMutex); + if (SendRequest(0, len, OP_SET_LENGTH, name.c_str())) { + length = len; + if (position > length) position = length; + } +} + +int idFile_MTP::Length() const { + return length > 0x7FFFFFFFu ? 0x7FFFFFFF : static_cast(length); +} + +int idFile_MTP::Tell() const { + return position > 0x7FFFFFFFu ? 0x7FFFFFFF : static_cast(position); +} + +int idFile_MTP::Seek(const long offset, const fsOrigin_t origin) { + std::int64_t target = 0; + if (origin == FS_SEEK_CUR) { + target = static_cast(position) + offset; + } else if (origin == FS_SEEK_END) { + target = static_cast(length) + offset; + } else if (origin == FS_SEEK_SET) { + target = offset; + } else { + return -1; + } + if (target < 0) return -1; + position = static_cast(target); + return 0; +} + +bool idFile_MTP::List(const char* directory, const char* extension, + idList& list) { + const char* const safeDirectory = directory == nullptr ? "" : directory; + const char* const safeExtension = extension == nullptr ? "" : extension; + fullPath.Format("%s\\*%s", safeDirectory, safeExtension); + name = fullPath; + + std::lock_guard lock(mtpMutex); + if (!SendRequest(0, 0, OP_LIST, name.c_str())) return false; + unsigned char lengthBytes[4] = {}; + if (mtpTCP.ReadBlocking(lengthBytes, sizeof(lengthBytes), mtpTimeout) + != sizeof(lengthBytes)) { + mtpTCP.Close(); + return false; + } + const unsigned int byteCount = GetUInt32BE(lengthBytes); + if (byteCount > 16u * 1024u * 1024u) return false; + std::vector entries(byteCount); + if (byteCount > 0 && mtpTCP.ReadBlocking(entries.data(), + static_cast(byteCount), mtpTimeout) + != static_cast(byteCount)) { + mtpTCP.Close(); + return false; + } + unsigned int cursor = 0; + while (cursor < byteCount) { + const char* const entry = entries.data() + cursor; + const std::size_t remaining = byteCount - cursor; + const std::size_t entryLength = strnlen(entry, remaining); + if (entryLength == remaining) return false; + list.Append(idStr(entry)); + cursor += static_cast(entryLength + 1); + } + return true; +} diff --git a/source/shared/idlib/filesystem/file_mtp.h b/source/shared/idlib/filesystem/file_mtp.h new file mode 100644 index 0000000..bffa4ae --- /dev/null +++ b/source/shared/idlib/filesystem/file_mtp.h @@ -0,0 +1,73 @@ +#pragma once + +#include "idlib/precompiled.h" + +#ifdef nullptr +#undef nullptr +#endif + +#include + +enum mtpFileMode_t { + MTP_FS_READ = 0, + MTP_FS_WRITE = 1, + MTP_FS_READ_WRITE = 2, + MTP_FS_READ_NO_BUFFERING = 3, + MTP_FS_APPEND = 4 +}; + +class idFile_MTP : public idFile { +public: + idFile_MTP(); + ~idFile_MTP() override; + + const char* GetName() const override { return name.c_str(); } + const char* GetFullPath() const override { return fullPath.c_str(); } + int Read(void* buffer, int len) override; + int Write(const void* buffer, int len) override; + int Length() const override; + ID_TIME_T Timestamp() const override { return timestamp; } + int Tell() const override; + int Seek(long offset, fsOrigin_t origin) override; + + bool Open(const char* filename, mtpFileMode_t openMode); + int ReadOfs(std::int64_t offset, void* buffer, int len); + int WriteOfs(std::int64_t offset, const void* buffer, int len); + void SetLength(unsigned int len); + std::uint64_t Length64() const { return length; } + std::uint64_t Tell64() const { return position; } + bool List(const char* directory, const char* extension, + idList& list); + + static void ConfigureServer(const char* host, + unsigned short port = 2769, int writeSize = 1400, + int timeoutMilliseconds = 1000); + static void ShutdownTransport(); + +private: + enum operation_t : unsigned short { + OP_READ = 0, + OP_LIST = 1, + OP_WRITE = 2, + OP_SET_LENGTH = 3, + OP_OPEN = 4 + }; + + static bool EnsureConnection(); + static bool SendRequest(std::uint64_t offset, unsigned int requestLength, + operation_t operation, const char* filename); + + // BFG's idFile predates the recovered idTech 5 uniqID field. + unsigned int uniqID; + idStr name; + idStr fullPath; + std::uint64_t position; + mtpFileMode_t mode; + std::uint64_t length; + unsigned int timestamp; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idFile_MTP) == 104, + "Recovered idFile_MTP ABI changed"); +#endif diff --git a/source/shared/idlib/filesystem/file_nfs.cpp b/source/shared/idlib/filesystem/file_nfs.cpp new file mode 100644 index 0000000..38873b7 --- /dev/null +++ b/source/shared/idlib/filesystem/file_nfs.cpp @@ -0,0 +1,413 @@ +#include "file_nfs.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct MountEntry { + std::string alias; + std::wstring root; + bool readOnly; +}; + +struct FileState { + HANDLE handle = INVALID_HANDLE_VALUE; + std::mutex mutex; + std::wstring nativePath; + ~FileState() { + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); + } +}; + +std::mutex mountsMutex; +std::vector mounts; + +std::string Lower(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return text; +} + +std::wstring Wide(const char* text) { + if (text == nullptr || *text == 0) return std::wstring(); + int amount = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + text, -1, nullptr, 0); + UINT codePage = CP_UTF8; + DWORD flags = MB_ERR_INVALID_CHARS; + if (amount == 0) { + codePage = CP_ACP; + flags = 0; + amount = MultiByteToWideChar(codePage, flags, text, -1, nullptr, 0); + } + if (amount <= 0) return std::wstring(); + std::wstring result(static_cast(amount), L'\0'); + MultiByteToWideChar(codePage, flags, text, -1, &result[0], amount); + result.resize(static_cast(amount - 1)); + return result; +} + +std::wstring FullPath(const std::wstring& path) { + const DWORD required = GetFullPathNameW(path.c_str(), 0, nullptr, nullptr); + if (required == 0) return std::wstring(); + std::wstring result(static_cast(required), L'\0'); + const DWORD amount = GetFullPathNameW(path.c_str(), required, + &result[0], nullptr); + if (amount == 0 || amount >= required) return std::wstring(); + result.resize(amount); + while (result.size() > 3 + && (result.back() == L'\\' || result.back() == L'/')) { + result.pop_back(); + } + return result; +} + +bool PathStartsWith(const std::wstring& path, const std::wstring& root) { + if (path.size() < root.size()) return false; + if (_wcsnicmp(path.c_str(), root.c_str(), root.size()) != 0) return false; + return path.size() == root.size() || path[root.size()] == L'\\' + || path[root.size()] == L'/'; +} + +bool Resolve(const char* nfsPath, std::wstring& nativePath, + bool* readOnly = nullptr, std::string* aliasResult = nullptr) { + std::string path = nfsPath == nullptr ? "" : nfsPath; + if (path.size() >= 4 && Lower(path.substr(0, 4)) == "nfs:") { + path.erase(0, 4); + } + std::replace(path.begin(), path.end(), '\\', '/'); + while (!path.empty() && path.front() == '/') path.erase(path.begin()); + const std::size_t slash = path.find('/'); + const std::string alias = Lower(path.substr(0, slash)); + const std::string relative = slash == std::string::npos + ? std::string() : path.substr(slash + 1); + if (alias.empty()) return false; + + std::lock_guard lock(mountsMutex); + for (const MountEntry& mount : mounts) { + if (mount.alias != alias) continue; + std::wstring candidate = mount.root; + if (!relative.empty()) { + candidate.push_back(L'\\'); + std::wstring tail = Wide(relative.c_str()); + std::replace(tail.begin(), tail.end(), L'/', L'\\'); + candidate += tail; + } + candidate = FullPath(candidate); + if (candidate.empty() || !PathStartsWith(candidate, mount.root)) { + return false; + } + nativePath = candidate; + if (readOnly != nullptr) *readOnly = mount.readOnly; + if (aliasResult != nullptr) *aliasResult = alias; + return true; + } + return false; +} + +bool CreateDirectories(const std::wstring& path, const bool includeLast) { + std::wstring target = path; + if (!includeLast) { + const std::size_t slash = target.find_last_of(L"\\/"); + if (slash == std::wstring::npos) return true; + target.resize(slash); + } + const std::size_t rootEnd = target.size() >= 3 && target[1] == L':' ? 3 : 0; + for (std::size_t index = rootEnd; index <= target.size(); ++index) { + if (index != target.size() && target[index] != L'\\' + && target[index] != L'/') continue; + const std::wstring part = target.substr(0, index); + if (part.empty()) continue; + if (!CreateDirectoryW(part.c_str(), nullptr)) { + const DWORD error = GetLastError(); + if (error != ERROR_ALREADY_EXISTS) return false; + } + } + return true; +} + +bool RefreshMetadata(FileState& state, std::uint64_t& size, + unsigned int& timestamp) { + LARGE_INTEGER length{}; + FILETIME writeTime{}; + if (!GetFileSizeEx(state.handle, &length) + || !GetFileTime(state.handle, nullptr, nullptr, &writeTime)) return false; + size = static_cast(length.QuadPart); + ULARGE_INTEGER ticks{}; + ticks.LowPart = writeTime.dwLowDateTime; + ticks.HighPart = writeTime.dwHighDateTime; + constexpr std::uint64_t epoch = 116444736000000000ULL; + timestamp = ticks.QuadPart > epoch + ? static_cast((ticks.QuadPart - epoch) / 10000000ULL) : 0; + return true; +} + +bool HasExtension(const std::wstring& name, const char* extension) { + if (extension == nullptr || *extension == 0 + || std::strcmp(extension, "*") == 0) return true; + const std::wstring suffix = Wide(extension); + return name.size() >= suffix.size() + && _wcsicmp(name.c_str() + name.size() - suffix.size(), + suffix.c_str()) == 0; +} + +} // namespace + +idFile_Nfs::idFile_Nfs() + : uniqID(0), openRemote(false), openPadding{}, fh{}, + fullPath("invalid"), mode(NFS_FS_READ), position(0), size(0), + timeStamp(0), demandSeek(false), seekPadding{}, nfsClient(nullptr), + ro(false) {} + +idFile_Nfs::~idFile_Nfs() { + delete static_cast(nfsClient); + nfsClient = nullptr; + openRemote = false; +} + +bool idFile_Nfs::Mount(const char* alias, const char* windowsRoot, + const bool readOnly) { + std::string normalized = Lower(alias == nullptr ? "" : alias); + while (!normalized.empty() && normalized.front() == '/') { + normalized.erase(normalized.begin()); + } + while (!normalized.empty() && normalized.back() == '/') normalized.pop_back(); + if (normalized.empty() || normalized.find('/') != std::string::npos + || normalized.find('\\') != std::string::npos) return false; + const std::wstring root = FullPath(Wide(windowsRoot)); + if (root.empty()) return false; + const DWORD attributes = GetFileAttributesW(root.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES + || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) return false; + + std::lock_guard lock(mountsMutex); + for (MountEntry& mount : mounts) { + if (mount.alias == normalized) { + mount.root = root; + mount.readOnly = readOnly; + return true; + } + } + mounts.push_back({normalized, root, readOnly}); + return true; +} + +void idFile_Nfs::UnmountAll() { + std::lock_guard lock(mountsMutex); + mounts.clear(); +} + +bool idFile_Nfs::Open(const char* path, const nfsFileMode_t openMode, + bool create, const bool createPath) { + delete static_cast(nfsClient); + nfsClient = nullptr; + openRemote = false; + + std::wstring native; + bool mountReadOnly = false; + if (!Resolve(path, native, &mountReadOnly)) return false; + const bool wantsWrite = openMode == NFS_FS_WRITE + || openMode == NFS_FS_READ_WRITE || openMode == NFS_FS_APPEND; + if (mountReadOnly && wantsWrite) return false; + if (openMode == NFS_FS_WRITE || openMode == NFS_FS_APPEND) create = true; + if (createPath && create && !CreateDirectories(native, false)) return false; + + DWORD access = GENERIC_READ; + DWORD disposition = OPEN_EXISTING; + if (openMode == NFS_FS_WRITE) { + access = GENERIC_WRITE | GENERIC_READ; + disposition = CREATE_ALWAYS; + } else if (openMode == NFS_FS_READ_WRITE) { + access = GENERIC_WRITE | GENERIC_READ; + disposition = create ? OPEN_ALWAYS : OPEN_EXISTING; + } else if (openMode == NFS_FS_APPEND) { + access = GENERIC_WRITE | GENERIC_READ; + disposition = OPEN_ALWAYS; + } + + FileState* const state = new FileState; + state->nativePath = native; + state->handle = CreateFileW(native.c_str(), access, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + disposition, FILE_ATTRIBUTE_NORMAL, nullptr); + if (state->handle == INVALID_HANDLE_VALUE) { + delete state; + return false; + } + nfsClient = state; + openRemote = true; + ro = mountReadOnly; + mode = openMode; + fullPath = path == nullptr ? "" : path; + if (!RefreshMetadata(*state, size, timeStamp)) { + delete state; + nfsClient = nullptr; + openRemote = false; + return false; + } + position = openMode == NFS_FS_APPEND ? size : 0; + return true; +} + +int idFile_Nfs::ReadOfs(const std::int64_t offset, void* buffer, + const int len) { + FileState* const state = static_cast(nfsClient); + if (state == nullptr || buffer == nullptr || len <= 0 || offset < 0) return 0; + std::lock_guard lock(state->mutex); + LARGE_INTEGER saved{}, target{}; + target.QuadPart = offset; + if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT) + || !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN)) return 0; + DWORD amount = 0; + const BOOL success = ReadFile(state->handle, buffer, + static_cast(len), &amount, nullptr); + SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN); + return success ? static_cast(amount) : 0; +} + +int idFile_Nfs::WriteOfs(const std::int64_t offset, const void* buffer, + const int len) { + FileState* const state = static_cast(nfsClient); + if (state == nullptr || ro || buffer == nullptr || len <= 0 || offset < 0) return 0; + std::lock_guard lock(state->mutex); + LARGE_INTEGER saved{}, target{}; + target.QuadPart = offset; + if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT) + || !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN)) return 0; + DWORD amount = 0; + const BOOL success = WriteFile(state->handle, buffer, + static_cast(len), &amount, nullptr); + SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN); + if (success) { + size = std::max(size, static_cast(offset) + amount); + RefreshMetadata(*state, size, timeStamp); + } + return success ? static_cast(amount) : 0; +} + +int idFile_Nfs::Read(void* buffer, const int len) { + const int amount = ReadOfs(static_cast(position), buffer, len); + position += static_cast(amount); + return amount; +} + +int idFile_Nfs::Write(const void* buffer, const int len) { + if (mode == NFS_FS_APPEND) position = size; + const int amount = WriteOfs(static_cast(position), buffer, len); + position += static_cast(amount); + return amount; +} + +int idFile_Nfs::Length() const { + return size > static_cast(INT_MAX) ? INT_MAX + : static_cast(size); +} + +int idFile_Nfs::Tell() const { + return position > static_cast(INT_MAX) ? INT_MAX + : static_cast(position); +} + +int idFile_Nfs::Seek(const long offset, const fsOrigin_t origin) { + return Seek64(offset, origin); +} + +int idFile_Nfs::Seek64(const std::int64_t offset, const fsOrigin_t origin) { + std::int64_t base = 0; + if (origin == FS_SEEK_CUR) base = static_cast(position); + else if (origin == FS_SEEK_END) base = static_cast(size); + const std::int64_t next = base + offset; + if (next < 0) return -1; + position = static_cast(next); + demandSeek = true; + return 0; +} + +void idFile_Nfs::SetLength(const unsigned int len) { SetLength64(len); } + +bool idFile_Nfs::SetLength64(const std::uint64_t len) { + FileState* const state = static_cast(nfsClient); + if (state == nullptr || ro || len > static_cast(INT64_MAX)) return false; + std::lock_guard lock(state->mutex); + LARGE_INTEGER saved{}, target{}; + target.QuadPart = static_cast(len); + if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT) + || !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN) + || !SetEndOfFile(state->handle)) return false; + SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN); + size = len; + if (position > size) position = size; + RefreshMetadata(*state, size, timeStamp); + return true; +} + +void idFile_Nfs::Flush() { + FileState* const state = static_cast(nfsClient); + if (state != nullptr && !ro) FlushFileBuffers(state->handle); +} + +void idFile_Nfs::ForceFlush() { Flush(); } + +bool idFile_Nfs::CreateOsPath(const char* path) { + std::wstring native; + bool readOnly = false; + return Resolve(path, native, &readOnly) && !readOnly + && CreateDirectories(native, false); +} + +int idFile_Nfs::ListFiles(const char* path, const char* extension, + idList& list) { + list.Clear(); + std::wstring native; + if (!Resolve(path, native)) return 0; + std::wstring query = native; + if (!query.empty() && query.back() != L'\\') query.push_back(L'\\'); + query.push_back(L'*'); + WIN32_FIND_DATAW data{}; + HANDLE find = FindFirstFileW(query.c_str(), &data); + if (find == INVALID_HANDLE_VALUE) return 0; + do { + const std::wstring name = data.cFileName; + if (name == L"." || name == L".." || !HasExtension(name, extension)) continue; + const int amount = WideCharToMultiByte(CP_UTF8, 0, name.c_str(), -1, + nullptr, 0, nullptr, nullptr); + if (amount <= 1) continue; + std::string utf8(static_cast(amount), '\0'); + WideCharToMultiByte(CP_UTF8, 0, name.c_str(), -1, &utf8[0], amount, + nullptr, nullptr); + utf8.resize(static_cast(amount - 1)); + list.Append(idStr(utf8.c_str())); + } while (FindNextFileW(find, &data)); + FindClose(find); + return list.Num(); +} + +bool idFile_Nfs::RemoveFile(const char* path) { + std::wstring native; + bool readOnly = false; + if (!Resolve(path, native, &readOnly) || readOnly) return false; + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) return false; + return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 + ? RemoveDirectoryW(native.c_str()) != FALSE + : DeleteFileW(native.c_str()) != FALSE; +} + +bool idFile_Nfs::RenameFile(const char* oldPath, const char* newPath) { + std::wstring oldNative, newNative; + bool oldReadOnly = false, newReadOnly = false; + std::string oldAlias, newAlias; + if (!Resolve(oldPath, oldNative, &oldReadOnly, &oldAlias) + || !Resolve(newPath, newNative, &newReadOnly, &newAlias) + || oldReadOnly || newReadOnly || oldAlias != newAlias) return false; + return MoveFileExW(oldNative.c_str(), newNative.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) != FALSE; +} diff --git a/source/shared/idlib/filesystem/file_nfs.h b/source/shared/idlib/filesystem/file_nfs.h new file mode 100644 index 0000000..6434e4b --- /dev/null +++ b/source/shared/idlib/filesystem/file_nfs.h @@ -0,0 +1,79 @@ +#pragma once + +#include "idlib/precompiled.h" + +#ifdef nullptr +#undef nullptr +#endif +#ifdef strcmp +#undef strcmp +#endif + +#include + +enum nfsFileMode_t { + NFS_FS_READ = 0, + NFS_FS_WRITE = 1, + NFS_FS_READ_WRITE = 2, + NFS_FS_READ_NO_BUFFERING = 3, + NFS_FS_APPEND = 4 +}; + +class idFile_Nfs : public idFile { +public: + idFile_Nfs(); + ~idFile_Nfs() override; + + const char* GetName() const override { return fullPath.c_str(); } + const char* GetFullPath() const override { return fullPath.c_str(); } + int Read(void* buffer, int len) override; + int Write(const void* buffer, int len) override; + int Length() const override; + ID_TIME_T Timestamp() const override { return timeStamp; } + int Tell() const override; + int Seek(long offset, fsOrigin_t origin) override; + void Flush() override; + void ForceFlush() override; + + bool Open(const char* path, nfsFileMode_t openMode, bool create = false, + bool createPath = false); + int ReadOfs(std::int64_t offset, void* buffer, int len); + int WriteOfs(std::int64_t offset, const void* buffer, int len); + void SetLength(unsigned int len); + bool SetLength64(std::uint64_t len); + std::uint64_t Length64() const { return size; } + std::uint64_t Tell64() const { return position; } + int Seek64(std::int64_t offset, fsOrigin_t origin); + bool IsReadOnly() const { return ro; } + + static bool Mount(const char* alias, const char* windowsRoot, + bool readOnly = false); + static void UnmountAll(); + static bool CreateOsPath(const char* path); + static int ListFiles(const char* path, const char* extension, + idList& list); + static bool RemoveFile(const char* path); + static bool RenameFile(const char* oldPath, const char* newPath); + +private: + struct NfsInternalFh { std::uint32_t pad[38]; }; + + unsigned int uniqID; + bool openRemote; + unsigned char openPadding[3]; + NfsInternalFh fh; + idStr fullPath; + nfsFileMode_t mode; + std::uint64_t position; + std::uint64_t size; + unsigned int timeStamp; + bool demandSeek; + unsigned char seekPadding[3]; + void* nfsClient; + bool ro; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idFile_Nfs) == 232, + "Recovered idFile_Nfs ABI changed"); +#endif diff --git a/source/shared/idlib/filesystem/file_permanent.h b/source/shared/idlib/filesystem/file_permanent.h new file mode 100644 index 0000000..7f247c9 --- /dev/null +++ b/source/shared/idlib/filesystem/file_permanent.h @@ -0,0 +1,13 @@ +#pragma once + +// The portable permanent-file implementation currently comes from the BFG +// framework baseline. This compatibility include restores the original +// shared/idlib path used by tungsten sources while its wider idTech 5 layout +// is introduced incrementally at the filesystem boundary. +#include "idlib/precompiled.h" + +#ifdef nullptr +#undef nullptr +#endif + +inline int idFilePermanentSectorSize(const idFile_Permanent&) { return 1; } diff --git a/source/shared/idlib/filter/warningsfilter.cpp b/source/shared/idlib/filter/warningsfilter.cpp new file mode 100644 index 0000000..d039b6c --- /dev/null +++ b/source/shared/idlib/filter/warningsfilter.cpp @@ -0,0 +1,247 @@ +#include "warningsfilter.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::mutex filterMutex; + +struct filterConfig_t { + const mgWarningsFilter* owner; + bool enabled; +}; + +filterConfig_t filterConfigs[8] = {}; + +bool& EnabledFor(const mgWarningsFilter* owner) { + for (filterConfig_t& config : filterConfigs) { + if (config.owner == owner) return config.enabled; + } + for (filterConfig_t& config : filterConfigs) { + if (config.owner == nullptr) { + config.owner = owner; + config.enabled = true; + return config.enabled; + } + } + return filterConfigs[0].enabled; +} + +bool DecodeJSONString(const char* objectBegin, const char* objectEnd, + const char* key, std::string& result) { + const std::string needle = std::string("\"") + key + "\""; + const char* keyPosition = std::search(objectBegin, objectEnd, + needle.begin(), needle.end()); + if (keyPosition == objectEnd) return false; + const char* cursor = keyPosition + needle.size(); + while (cursor < objectEnd && *cursor != ':') ++cursor; + if (cursor == objectEnd) return false; + while (++cursor < objectEnd && (*cursor == ' ' || *cursor == '\t' + || *cursor == '\r' || *cursor == '\n')) {} + if (cursor == objectEnd || *cursor != '"') return false; + ++cursor; + result.clear(); + while (cursor < objectEnd && *cursor != '"') { + if (*cursor == '\\' && cursor + 1 < objectEnd) { + ++cursor; + switch (*cursor) { + case 'n': result.push_back('\n'); break; + case 'r': result.push_back('\r'); break; + case 't': result.push_back('\t'); break; + default: result.push_back(*cursor); break; + } + } else { + result.push_back(*cursor); + } + ++cursor; + } + return cursor < objectEnd; +} + +} // namespace + +warningfilter_t::warningfilter_t() : reg{} {} + +warningfilter_t::warningfilter_t(const warningfilter_t& other) + : pattern(other.pattern), owner(other.owner), reg{} { + if (other.reg.value != nullptr) { + try { + reg.value = new std::regex(pattern.c_str(), + std::regex_constants::extended); + } catch (const std::regex_error&) { + reg.value = nullptr; + } + } +} + +warningfilter_t::~warningfilter_t() { + delete static_cast(reg.value); +} + +warningfilter_t& warningfilter_t::operator=(const warningfilter_t& other) { + if (this == &other) return *this; + delete static_cast(reg.value); + reg = {}; + pattern = other.pattern; + owner = other.owner; + if (other.reg.value != nullptr) { + try { + reg.value = new std::regex(pattern.c_str(), + std::regex_constants::extended); + } catch (const std::regex_error&) { + reg.value = nullptr; + } + } + return *this; +} + +mgWarningsFilter::mgWarningsFilter() : filters{}, init(false) { + filters.granularity = 16; + EnabledFor(this) = true; +} + +mgWarningsFilter::~mgWarningsFilter() { + Clear(); + for (filterConfig_t& config : filterConfigs) { + if (config.owner == this) config = {}; + } +} + +bool mgWarningsFilter::EnsureCapacity(const int amount) { + if (amount <= filters.size) return true; + int newSize = filters.size == 0 ? filters.granularity : filters.size; + while (newSize < amount) newSize += filters.granularity; + warningfilter_t* const replacement = new (std::nothrow) + warningfilter_t[newSize]; + if (replacement == nullptr) return false; + for (int index = 0; index < filters.num; ++index) { + replacement[index] = filters.list[index]; + } + delete[] filters.list; + filters.list = replacement; + filters.size = newSize; + return true; +} + +bool mgWarningsFilter::AddFilter(const char* patternText, + const char* ownerText) { + if (patternText == nullptr || *patternText == '\0') return false; + std::regex* compiled = nullptr; + try { + compiled = new std::regex(patternText, std::regex_constants::extended); + } catch (const std::regex_error&) { + return false; + } + + std::lock_guard lock(filterMutex); + if (!EnsureCapacity(filters.num + 1)) { + delete compiled; + return false; + } + warningfilter_t& filter = filters.list[filters.num++]; + filter.pattern = patternText; + filter.owner = ownerText == nullptr ? "" : ownerText; + filter.reg.re_nsub = 0; + filter.reg.value = compiled; + init = true; + return true; +} + +int mgWarningsFilter::RemoveOwner(const char* ownerText) { + const char* const safeOwner = ownerText == nullptr ? "" : ownerText; + std::lock_guard lock(filterMutex); + int removed = 0; + for (int index = 0; index < filters.num;) { + if (std::strcmp(filters.list[index].owner.c_str(), safeOwner) != 0) { + ++index; + continue; + } + for (int move = index + 1; move < filters.num; ++move) { + filters.list[move - 1] = filters.list[move]; + } + filters.list[filters.num - 1] = warningfilter_t(); + --filters.num; + ++removed; + } + return removed; +} + +void mgWarningsFilter::Clear() { + std::lock_guard lock(filterMutex); + delete[] filters.list; + filters.list = nullptr; + filters.num = 0; + filters.size = 0; + init = false; +} + +bool mgWarningsFilter::IsFiltered(const char* warning, idStr* matchedOwner) const { + if (warning == nullptr || !EnabledFor(this)) return false; + std::lock_guard lock(filterMutex); + for (int index = 0; index < filters.num; ++index) { + const warningfilter_t& filter = filters.list[index]; + const std::regex* const expression = + static_cast(filter.reg.value); + if (expression != nullptr && std::regex_search(warning, *expression)) { + if (matchedOwner != nullptr) *matchedOwner = filter.owner; + return true; + } + } + return false; +} + +const warningfilter_t* mgWarningsFilter::GetFilter(const int index) const { + return index >= 0 && index < filters.num ? filters.list + index : nullptr; +} + +void mgWarningsFilter::SetEnabled(const bool value) { + EnabledFor(this) = value; +} + +bool mgWarningsFilter::IsEnabled() const { + return EnabledFor(this); +} + +int mgWarningsFilter::LoadJSONText(const char* json) { + if (json == nullptr) return 0; + int loaded = 0; + const char* cursor = json; + while ((cursor = std::strchr(cursor, '{')) != nullptr) { + const char* const end = std::strchr(cursor + 1, '}'); + if (end == nullptr) break; + std::string patternText; + std::string ownerText; + if (DecodeJSONString(cursor, end, "pattern", patternText)) { + DecodeJSONString(cursor, end, "owner", ownerText); + if (AddFilter(patternText.c_str(), ownerText.c_str())) ++loaded; + } + cursor = end + 1; + } + return loaded; +} + +bool mgWarningsFilter::LoadJSONFile(const char* filename) { + if (filename == nullptr) return false; + FILE* file = nullptr; + if (fopen_s(&file, filename, "rb") != 0 || file == nullptr) return false; + std::fseek(file, 0, SEEK_END); + const long length = std::ftell(file); + std::fseek(file, 0, SEEK_SET); + if (length < 0 || length > 16 * 1024 * 1024) { + std::fclose(file); + return false; + } + std::string contents(static_cast(length), '\0'); + const std::size_t read = contents.empty() ? 0 + : std::fread(&contents[0], 1, contents.size(), file); + std::fclose(file); + return read == contents.size() && LoadJSONText(contents.c_str()) > 0; +} + +mgWarningsFilter warningsFilter; diff --git a/source/shared/idlib/filter/warningsfilter.h b/source/shared/idlib/filter/warningsfilter.h new file mode 100644 index 0000000..0b22138 --- /dev/null +++ b/source/shared/idlib/filter/warningsfilter.h @@ -0,0 +1,66 @@ +#pragma once + +#include "idlib/text/str.h" + +#include + +struct warningfilter_regex_t { + unsigned int re_nsub; + void* value; +}; + +struct warningfilter_t { + warningfilter_t(); + warningfilter_t(const warningfilter_t& other); + ~warningfilter_t(); + warningfilter_t& operator=(const warningfilter_t& other); + + idStr pattern; + idStr owner; + warningfilter_regex_t reg; +}; + +class mgWarningsFilter { +public: + mgWarningsFilter(); + ~mgWarningsFilter(); + + bool AddFilter(const char* pattern, const char* owner = ""); + int RemoveOwner(const char* owner); + void Clear(); + bool IsFiltered(const char* warning, idStr* owner = nullptr) const; + bool Filter(const char* warning) const { return IsFiltered(warning); } + + bool LoadJSONFile(const char* filename); + int LoadJSONText(const char* json); + int NumFilters() const { return filters.num; } + const warningfilter_t* GetFilter(int index) const; + + void SetEnabled(bool value); + bool IsEnabled() const; + +private: + struct recoveredFilterList_t { + warningfilter_t* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; + } filters; + bool init; + + // Enabled is kept out of the recovered object layout in the .cpp sidecar. + bool EnsureCapacity(int amount); +}; + +extern mgWarningsFilter warningsFilter; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(warningfilter_regex_t) == 8, + "Recovered regex_t facade ABI changed"); +static_assert(sizeof(warningfilter_t) == 72, + "Recovered warningfilter_t ABI changed"); +static_assert(sizeof(mgWarningsFilter) == 20, + "Recovered mgWarningsFilter ABI changed"); +#endif diff --git a/source/shared/idlib/geometry/geometry.cpp b/source/shared/idlib/geometry/geometry.cpp new file mode 100644 index 0000000..a45eec3 --- /dev/null +++ b/source/shared/idlib/geometry/geometry.cpp @@ -0,0 +1,227 @@ +#include "idlib/geometry/geometry.h" + +#include +#include +#include + +namespace { + +constexpr float kSmallestNormal = std::numeric_limits::min(); +constexpr float kParallelEpsilonSqr = 0.0001f; + +float ClampUnit(const float value) { + return std::max(0.0f, std::min(1.0f, value)); +} + +float Cross2D(const idVec2& left, const idVec2& right) { + return left.x * right.y - left.y * right.x; +} + +idVec2 Subtract2D(const idVec2& left, const idVec2& right) { + return idVec2(left.x - right.x, left.y - right.y); +} + +idVec3 NormalizeSafely(const idVec3& vector) { + const float lengthSqr = vector.LengthSqr(); + if (lengthSqr < kSmallestNormal) { + return idVec3(0.0f, 0.0f, 0.0f); + } + return vector * (1.0f / std::sqrt(lengthSqr)); +} + +} // namespace + +float idGeometry::PositionOnLineSegment( + const idVec3& point, + const idVec3& start, + const idVec3& end +) { + const idVec3 segment = end - start; + const float lengthSqr = segment.LengthSqr(); + if (lengthSqr < kSmallestNormal) { + return 0.0f; + } + return (point - start).Dot(segment) / lengthSqr; +} + +bool idGeometry::ClosestPointOnLineSegment( + const idVec3& point, + const idVec3& start, + const idVec3& end, + idVec3& closest +) { + const idVec3 segment = end - start; + const float lengthSqr = segment.LengthSqr(); + if (lengthSqr < kSmallestNormal) { + closest = start; + return false; + } + + const float position = (point - start).Dot(segment) / lengthSqr; + if (position < 0.0f) { + closest = start; + return false; + } + if (position > 1.0f) { + closest = end; + return false; + } + + closest = start + segment * position; + return true; +} + +void idGeometry::ClosestPointOnLine( + const idVec3& point, + const idVec3& start, + const idVec3& dir, + idVec3& closest +) { + closest = start + dir * (point - start).Dot(dir); +} + +idVec3 idGeometry::TriangleNormal( + const idVec3& a, + const idVec3& b, + const idVec3& c +) { + // The recovered winding is intentionally (c-a) x (b-a), opposite the + // common formulation. Renderer-facing callers depend on that sign. + return NormalizeSafely((c - a).Cross(b - a)); +} + +bool idGeometry::IntersectRayWithLineSegment2D( + const idVec2& rayStart, + const idVec2& rayDir, + const idVec2& segStart, + const idVec2& segEnd, + float& dist +) { + const idVec2 segment = Subtract2D(segEnd, segStart); + const idVec2 offset = Subtract2D(segStart, rayStart); + const float denominator = Cross2D(rayDir, segment); + + if (denominator * denominator > kParallelEpsilonSqr) { + const float inverseDenominator = 1.0f / denominator; + const float segmentPosition = -Cross2D(rayDir, offset) + * inverseDenominator; + if (segmentPosition < 0.0f || segmentPosition > 1.0f) { + return false; + } + + const float rayPosition = Cross2D(offset, segment) + * inverseDenominator; + if (rayPosition < 0.0f) { + return false; + } + dist = rayPosition; + return true; + } + + if (Cross2D(rayDir, offset) * Cross2D(rayDir, offset) + > kParallelEpsilonSqr) { + return false; + } + + // The PPC output is difficult to read in this collinear branch. This is + // the equivalent geometric result: the nearest forward overlap measured + // in ray parameter units. Keep it isolated for later binary trace checks. + const float rayLengthSqr = rayDir.x * rayDir.x + rayDir.y * rayDir.y; + if (rayLengthSqr < kSmallestNormal) { + return false; + } + const idVec2 endOffset = Subtract2D(segEnd, rayStart); + const float startPosition = (offset.x * rayDir.x + offset.y * rayDir.y) + / rayLengthSqr; + const float endPosition = (endOffset.x * rayDir.x + endOffset.y * rayDir.y) + / rayLengthSqr; + const float nearPosition = std::min(startPosition, endPosition); + const float farPosition = std::max(startPosition, endPosition); + if (farPosition < 0.0f) { + return false; + } + dist = std::max(0.0f, nearPosition); + return true; +} + +float idGeometry::SquarePointLineSegmentDistance( + const idVec3& point, + const idVec3& start, + const idVec3& end +) { + const idVec3 segment = end - start; + const float lengthSqr = segment.LengthSqr(); + if (lengthSqr < 0.01f) { + return (point - start).LengthSqr(); + } + + const float position = ClampUnit((point - start).Dot(segment) / lengthSqr); + return (point - (start + segment * position)).LengthSqr(); +} + +void idGeometry::SegmentSegmentClosestPoints( + const idVec3& start1, + const idVec3& end1, + const idVec3& start2, + const idVec3& end2, + idVec3& out1, + idVec3& out2, + float& t1, + float& t2, + const bool clampTValues +) { + const idVec3 direction1 = end1 - start1; + const idVec3 direction2 = end2 - start2; + const idVec3 offset = start1 - start2; + const float length1Sqr = direction1.LengthSqr(); + const float length2Sqr = direction2.LengthSqr(); + const float directionsDot = direction1.Dot(direction2); + const float offsetDot1 = direction1.Dot(offset); + const float offsetDot2 = direction2.Dot(offset); + const float denominator = length1Sqr * length2Sqr + - directionsDot * directionsDot; + + if (length1Sqr < kSmallestNormal + || length2Sqr < kSmallestNormal + || denominator < kSmallestNormal) { + out1 = start1; + out2 = start2; + t1 = 1.0f; + t2 = 1.0f; + return; + } + + t1 = (directionsDot * offsetDot2 - offsetDot1 * length2Sqr) + / denominator; + if (clampTValues) { + t1 = ClampUnit(t1); + } + + t2 = (t1 * directionsDot + offsetDot2) / length2Sqr; + if (clampTValues) { + t2 = ClampUnit(t2); + } + + out1 = start1 + direction1 * t1; + out2 = start2 + direction2 * t2; +} + +idVec3 idGeometry::FindNearestPerpendicular( + const idVec3& input, + const idVec3& up, + const idVec3& hint +) { + idVec3 perpendicular = input.Cross(up); + if (perpendicular.Dot(hint) < 0.0f) { + perpendicular = -perpendicular; + } + return NormalizeSafely(perpendicular); +} + +float idGeometry::AreaOfTriangle( + const idVec3& a, + const idVec3& b, + const idVec3& c +) { + return 0.5f * (b - a).Cross(c - a).Length(); +} diff --git a/source/shared/idlib/geometry/geometry.h b/source/shared/idlib/geometry/geometry.h new file mode 100644 index 0000000..0dcd2e5 --- /dev/null +++ b/source/shared/idlib/geometry/geometry.h @@ -0,0 +1,70 @@ +#pragma once + +#include "../math/vector.h" + +class idGeometry { +public: + static float PositionOnLineSegment( + const idVec3& point, + const idVec3& start, + const idVec3& end + ); + + static bool ClosestPointOnLineSegment( + const idVec3& point, + const idVec3& start, + const idVec3& end, + idVec3& closest + ); + + static void ClosestPointOnLine( + const idVec3& point, + const idVec3& start, + const idVec3& dir, + idVec3& closest + ); + + static idVec3 TriangleNormal( + const idVec3& a, + const idVec3& b, + const idVec3& c + ); + + static bool IntersectRayWithLineSegment2D( + const idVec2& rayStart, + const idVec2& rayDir, + const idVec2& segStart, + const idVec2& segEnd, + float& dist + ); + + static float SquarePointLineSegmentDistance( + const idVec3& point, + const idVec3& start, + const idVec3& end + ); + + static void SegmentSegmentClosestPoints( + const idVec3& start1, + const idVec3& end1, + const idVec3& start2, + const idVec3& end2, + idVec3& out1, + idVec3& out2, + float& t1, + float& t2, + bool clampTValues + ); + + static idVec3 FindNearestPerpendicular( + const idVec3& input, + const idVec3& up, + const idVec3& hint + ); + + static float AreaOfTriangle( + const idVec3& a, + const idVec3& b, + const idVec3& c + ); +}; diff --git a/source/shared/idlib/geometry/screenrect.cpp b/source/shared/idlib/geometry/screenrect.cpp new file mode 100644 index 0000000..2cca88c --- /dev/null +++ b/source/shared/idlib/geometry/screenrect.cpp @@ -0,0 +1,16 @@ +#include "screenrect.h" + +void idScreenRect::Clear() { + x1 = 32000; + y1 = 32000; + x2 = -32000; + y2 = -32000; +} + +void idScreenRect::Zero() { + x1 = 0; + y1 = 0; + x2 = 0; + y2 = 0; +} + diff --git a/source/shared/idlib/geometry/screenrect.h b/source/shared/idlib/geometry/screenrect.h new file mode 100644 index 0000000..29e3a9d --- /dev/null +++ b/source/shared/idlib/geometry/screenrect.h @@ -0,0 +1,21 @@ +#pragma once + +// tungsten.exe.h type 12913. Unlike Doom 3 BFG's idScreenRect, this idTech 5 +// build uses four 32-bit coordinates and does not store depth bounds here. +class idScreenRect { +public: + int x1; + int y1; + int x2; + int y2; + + void Clear(); + void Zero(); + + bool IsEmpty() const { + return x1 > x2 || y1 > y2; + } +}; + +static_assert(sizeof(idScreenRect) == 16, "Recovered idScreenRect layout changed"); + diff --git a/source/shared/idlib/hashing/crc16.cpp b/source/shared/idlib/hashing/crc16.cpp new file mode 100644 index 0000000..218f0aa --- /dev/null +++ b/source/shared/idlib/hashing/crc16.cpp @@ -0,0 +1,58 @@ +#include "crc16.h" + +#include +#include +#include + +namespace { + +using CrcTable = std::array; + +CrcTable BuildCrcTable() { + CrcTable table = {}; + + for (std::size_t index = 0; index < table.size(); ++index) { + std::uint16_t value = static_cast(index << 8); + for (int bit = 0; bit < 8; ++bit) { + value = (value & 0x8000u) != 0 + ? static_cast((value << 1) ^ 0x1021u) + : static_cast(value << 1); + } + table[index] = value; + } + + return table; +} + +const CrcTable& GetCrcTable() { + static const CrcTable table = BuildCrcTable(); + return table; +} + +} // namespace + +void CRC16_UpdateChecksum( + std::uint16_t& crcValue, + const void* data, + const int length +) { + assert(length >= 0); + assert(data != nullptr || length == 0); + + if (length <= 0) { + return; + } + + const CrcTable& table = GetCrcTable(); + const auto* bytes = static_cast(data); + + for (int index = 0; index < length; ++index) { + const std::uint8_t tableIndex = static_cast( + (crcValue >> 8) ^ bytes[index] + ); + crcValue = static_cast( + table[tableIndex] ^ static_cast(crcValue << 8) + ); + } +} + diff --git a/source/shared/idlib/hashing/crc16.h b/source/shared/idlib/hashing/crc16.h new file mode 100644 index 0000000..a1a7ada --- /dev/null +++ b/source/shared/idlib/hashing/crc16.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +// Recovered from ?CRC16_UpdateChecksum@@YAXAAGPBXH@Z at 0x82F2BE00. +// The reference parameter and const void pointer match the decorated PDB name; +// Hex-Rays displayed the reference as a pointer in the raw 360 dump. +void CRC16_UpdateChecksum( + std::uint16_t& crcValue, + const void* data, + int length +); + diff --git a/source/shared/idlib/hashing/murmur.cpp b/source/shared/idlib/hashing/murmur.cpp new file mode 100644 index 0000000..05b6345 --- /dev/null +++ b/source/shared/idlib/hashing/murmur.cpp @@ -0,0 +1,137 @@ +#include "murmur.h" + +#include + +namespace { + +constexpr std::uint32_t MURMUR_MULTIPLIER = 0x5BD1E995u; + +std::uint32_t ReadLittleEndian32(const std::uint8_t* data) { + return static_cast(data[0]) + | (static_cast(data[1]) << 8) + | (static_cast(data[2]) << 16) + | (static_cast(data[3]) << 24); +} + +std::uint32_t ReadBigEndian32(const std::uint8_t* data) { + return (static_cast(data[0]) << 24) + | (static_cast(data[1]) << 16) + | (static_cast(data[2]) << 8) + | static_cast(data[3]); +} + +std::uint32_t MixWord(std::uint32_t value) { + value *= MURMUR_MULTIPLIER; + value ^= value >> 24; + value *= MURMUR_MULTIPLIER; + return value; +} + +} // namespace + +std::uint32_t MurMur32_HashData( + const void* key, + const int length, + const std::uint32_t seed +) { + assert(length >= 0); + assert(key != nullptr || length == 0); + + if (length < 0 || (key == nullptr && length != 0)) { + return 0; + } + + const auto* data = static_cast(key); + int remaining = length; + std::uint32_t hash = seed ^ static_cast(length); + + while (remaining >= 4) { + const std::uint32_t word = MixWord(ReadLittleEndian32(data)); + hash = (hash * MURMUR_MULTIPLIER) ^ word; + data += 4; + remaining -= 4; + } + + switch (remaining) { + case 3: + hash ^= static_cast(data[2]) << 16; + // Fall through. + case 2: + hash ^= static_cast(data[1]) << 8; + // Fall through. + case 1: + hash ^= data[0]; + hash *= MURMUR_MULTIPLIER; + break; + default: + break; + } + + hash ^= hash >> 13; + hash *= MURMUR_MULTIPLIER; + hash ^= hash >> 15; + return hash; +} + +std::uint64_t MurMur64_HashData( + const void* key, + const int length, + const std::uint32_t seed +) { + assert(length >= 0); + assert(key != nullptr || length == 0); + + if (length < 0 || (key == nullptr && length != 0)) { + return 0; + } + + const auto* data = static_cast(key); + int remaining = length; + std::uint32_t hash1 = seed ^ static_cast(length); + std::uint32_t hash2 = 0; + + // The recovered 360 function loads full words directly on big-endian PPC, + // but constructs its one-to-three-byte tail explicitly. Preserve that byte + // behavior so hashes continue to match the supplied Xbox resource data. + while (remaining >= 8) { + hash1 = (hash1 * MURMUR_MULTIPLIER) + ^ MixWord(ReadBigEndian32(data)); + hash2 = (hash2 * MURMUR_MULTIPLIER) + ^ MixWord(ReadBigEndian32(data + 4)); + data += 8; + remaining -= 8; + } + + if (remaining >= 4) { + hash1 = (hash1 * MURMUR_MULTIPLIER) + ^ MixWord(ReadBigEndian32(data)); + data += 4; + remaining -= 4; + } + + switch (remaining) { + case 3: + hash2 ^= static_cast(data[2]) << 16; + // Fall through. + case 2: + hash2 ^= static_cast(data[1]) << 8; + // Fall through. + case 1: + hash2 ^= data[0]; + hash2 *= MURMUR_MULTIPLIER; + break; + default: + break; + } + + hash1 ^= hash2 >> 18; + hash1 *= MURMUR_MULTIPLIER; + hash2 ^= hash1 >> 22; + hash2 *= MURMUR_MULTIPLIER; + hash1 ^= hash2 >> 17; + hash1 *= MURMUR_MULTIPLIER; + hash2 ^= hash1 >> 19; + hash2 *= MURMUR_MULTIPLIER; + + return (static_cast(hash1) << 32) | hash2; +} diff --git a/source/shared/idlib/hashing/murmur.h b/source/shared/idlib/hashing/murmur.h new file mode 100644 index 0000000..14fd3d6 --- /dev/null +++ b/source/shared/idlib/hashing/murmur.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +// PDB signatures: +// ?MurMur32_HashData@@YAIPBXHI@Z +// ?MurMur64_HashData@@YA_KPBXHI@Z +std::uint32_t MurMur32_HashData( + const void* key, + int length, + std::uint32_t seed +); + +std::uint64_t MurMur64_HashData( + const void* key, + int length, + std::uint32_t seed +); + diff --git a/source/shared/idlib/index.h b/source/shared/idlib/index.h new file mode 100644 index 0000000..51ab9a0 --- /dev/null +++ b/source/shared/idlib/index.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +template +class idIndex { +public: + idIndex() + : value(static_cast(-1)) { + } + + explicit idIndex(const valueType index) + : value(index) { + } + + bool IsValid() const { + return value != static_cast(-1); + } + + void Invalidate() { + value = static_cast(-1); + } + + valueType Get() const { + return value; + } + + operator valueType() const { + return value; + } + + bool operator==(const idIndex& other) const { return value == other.value; } + bool operator!=(const idIndex& other) const { return value != other.value; } + bool operator<(const idIndex& other) const { return value < other.value; } + +private: + valueType value; +}; + +enum class idRecoveredInvalidIndex : int { invalid = -1 }; +static_assert(sizeof(idIndex) == sizeof(short), + "Recovered idIndex ABI changed"); diff --git a/source/shared/idlib/lib_print.cpp b/source/shared/idlib/lib_print.cpp new file mode 100644 index 0000000..f27ed5f --- /dev/null +++ b/source/shared/idlib/lib_print.cpp @@ -0,0 +1,200 @@ +#include "lib_print.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct warningInfo_t { const char* type; const char* name; }; + +std::recursive_mutex printMutex; +idPrintListener* listeners = nullptr; +std::array warningInfo{}; +int numWarningInfo = 0; +idLibPrint::fatalErrorHandler_t fatalErrorHandler = nullptr; +const std::thread::id mainThread = std::this_thread::get_id(); + +std::string RemoveColors(const std::string& source) { + std::string result; + result.reserve(source.size()); + for (std::size_t index = 0; index < source.size(); ++index) { + if (source[index] == '^' && index + 1 < source.size() + && source[index + 1] >= '0' && source[index + 1] <= '9') { + ++index; + } else { + result.push_back(source[index]); + } + } + return result; +} + +std::string Format(const char* format, va_list args) { + char buffer[4096] = {}; + const int amount = std::vsnprintf(buffer, sizeof(buffer), + format == nullptr ? "" : format, args); + if (amount < 0) { + buffer[sizeof(buffer) - 2] = '\n'; + buffer[sizeof(buffer) - 1] = '\0'; + } + return buffer; +} + +const char* Prefix(const printSeverity_t severity) { + switch (severity) { + case SEV_WARNING: return "^3WARNING: ^1"; + case SEV_ERROR: return "^3ERROR: ^1"; + case SEV_FATAL: return "^3FATAL ERROR: ^1"; + default: return ""; + } +} + +void ThrowMessage(const printSeverity_t severity, const std::string& message) { + throw idRecoveredPrintException(RemoveColors(message).c_str(), + severity == SEV_FATAL); +} + +} // namespace + +idPrintListener::idPrintListener() + : next(nullptr), wantColor(true), threadSafe(false), + minSeverity(SEV_PRINT) {} + +idPrintListener::~idPrintListener() { UnRegisterPrintListener(); } + +void idPrintListener::RegisterPrintListener() { + idLibPrint::RegisterPrintListener(this); +} + +void idPrintListener::UnRegisterPrintListener() { + idLibPrint::UnRegisterPrintListener(this); +} + +bool idLibPrint::PushWarningInfo(const char* type, const char* name) { + std::lock_guard lock(printMutex); + if (numWarningInfo >= static_cast(warningInfo.size())) return false; + warningInfo[static_cast(numWarningInfo++)] = {type, name}; + return true; +} + +void idLibPrint::PopWarningInfo() { + std::lock_guard lock(printMutex); + if (numWarningInfo > 0) --numWarningInfo; +} + +void idLibPrint::RegisterFatalErrorHandler(fatalErrorHandler_t handler) { + std::lock_guard lock(printMutex); + fatalErrorHandler = handler; +} + +void idLibPrint::RegisterPrintListener(idPrintListener* listener) { + if (listener == nullptr) return; + std::lock_guard lock(printMutex); + for (idPrintListener* current = listeners; current != nullptr; + current = current->next) { + if (current == listener) return; + } + listener->next = listeners; + listeners = listener; +} + +void idLibPrint::UnRegisterPrintListener(idPrintListener* listener) { + if (listener == nullptr) return; + std::lock_guard lock(printMutex); + idPrintListener** link = &listeners; + while (*link != nullptr && *link != listener) link = &(*link)->next; + if (*link == listener) { + *link = listener->next; + listener->next = nullptr; + } +} + +void idLibPrint::Dispatch(const printSeverity_t severity, const char* format, + va_list args) { + va_list copy; + va_copy(copy, args); + std::string message = Prefix(severity); + message += Format(format, copy); + va_end(copy); + + std::lock_guard lock(printMutex); + if (severity >= SEV_WARNING) { + for (int index = numWarningInfo - 1; index >= 0; --index) { + message += index == numWarningInfo - 1 + ? " ^7while loading " : " ^8from "; + message += warningInfo[static_cast(index)].type == nullptr + ? "" : warningInfo[static_cast(index)].type; + message.push_back(' '); + message += warningInfo[static_cast(index)].name == nullptr + ? "" : warningInfo[static_cast(index)].name; + } + message.push_back('\n'); + } + + const bool onMainThread = std::this_thread::get_id() == mainThread; + bool delivered = false; + for (idPrintListener* listener = listeners; listener != nullptr; + listener = listener->next) { + if (listener->wantColor && severity >= listener->minSeverity + && (onMainThread || listener->threadSafe)) { + listener->Print(message.c_str()); + delivered = true; + } + } + const std::string plain = RemoveColors(message); + for (idPrintListener* listener = listeners; listener != nullptr; + listener = listener->next) { + if (!listener->wantColor && severity >= listener->minSeverity + && (onMainThread || listener->threadSafe)) { + listener->Print(plain.c_str()); + delivered = true; + } + } + if (!delivered) std::fputs(plain.c_str(), stderr); + + if (severity == SEV_FATAL && fatalErrorHandler != nullptr) { + fatalErrorHandler(plain.c_str()); + } + if (severity >= SEV_ERROR) ThrowMessage(severity, message); +} + +void idLibPrint::Debugf(const char* format, ...) { + va_list args; va_start(args, format); Dispatch(SEV_DEBUG, format, args); va_end(args); +} + +void idLibPrint::Printf(const char* format, ...) { + va_list args; va_start(args, format); Dispatch(SEV_PRINT, format, args); va_end(args); +} + +void idLibPrint::PrintfIf(const bool condition, const char* format, ...) { + if (!condition) return; + va_list args; va_start(args, format); Dispatch(SEV_PRINT, format, args); va_end(args); +} + +void idLibPrint::VPrintf(const char* format, va_list args) { + Dispatch(SEV_PRINT, format, args); +} + +void idLibPrint::Warning(const char* format, ...) { + va_list args; va_start(args, format); Dispatch(SEV_WARNING, format, args); va_end(args); +} + +void idLibPrint::WarningIf(const bool condition, const char* format, ...) { + if (!condition) return; + va_list args; va_start(args, format); Dispatch(SEV_WARNING, format, args); va_end(args); +} + +void idLibPrint::Error(const char* format, ...) { + va_list args; va_start(args, format); Dispatch(SEV_ERROR, format, args); va_end(args); + std::terminate(); +} + +void idLibPrint::FatalError(const char* format, ...) { + va_list args; va_start(args, format); Dispatch(SEV_FATAL, format, args); va_end(args); + std::terminate(); +} diff --git a/source/shared/idlib/lib_print.h b/source/shared/idlib/lib_print.h new file mode 100644 index 0000000..1d17b06 --- /dev/null +++ b/source/shared/idlib/lib_print.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +enum printSeverity_t { + SEV_DEBUG = 0, + SEV_PRINT = 1, + SEV_WARNING = 2, + SEV_ERROR = 3, + SEV_FATAL = 4 +}; + +class idPrintListener { +public: + idPrintListener(); + virtual ~idPrintListener(); + virtual void Print(const char* text) = 0; + + void RegisterPrintListener(); + void UnRegisterPrintListener(); + + idPrintListener* next; + bool wantColor; + bool threadSafe; + printSeverity_t minSeverity; +}; + +class idRecoveredPrintException : public std::runtime_error { +public: + idRecoveredPrintException(const char* message, bool fatal) + : std::runtime_error(message == nullptr ? "" : message), + fatalError(fatal) {} + bool IsFatal() const { return fatalError; } +private: + bool fatalError; +}; + +// PC-side extension for the listener/context facilities added after the BFG +// idLib interface. The six common Printf/Warning/Error entry points continue +// to be supplied by the BFG baseline until the engine-wide Lib.h is replaced. +class idLibPrint { +public: + using fatalErrorHandler_t = void (*)(const char*); + + static bool PushWarningInfo(const char* type, const char* name); + static void PopWarningInfo(); + static void RegisterFatalErrorHandler(fatalErrorHandler_t handler); + + static void Debugf(const char* format, ...); + static void Printf(const char* format, ...); + static void PrintfIf(bool condition, const char* format, ...); + static void VPrintf(const char* format, va_list args); + static void Warning(const char* format, ...); + static void WarningIf(bool condition, const char* format, ...); + [[noreturn]] static void Error(const char* format, ...); + [[noreturn]] static void FatalError(const char* format, ...); + + static void Dispatch(printSeverity_t severity, const char* format, + va_list args); + static void RegisterPrintListener(idPrintListener* listener); + static void UnRegisterPrintListener(idPrintListener* listener); +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idPrintListener) == 16, + "Recovered idPrintListener layout changed"); +#endif diff --git a/source/shared/idlib/lookuptable.cpp b/source/shared/idlib/lookuptable.cpp new file mode 100644 index 0000000..1cbd39f --- /dev/null +++ b/source/shared/idlib/lookuptable.cpp @@ -0,0 +1,321 @@ +#include "lookuptable.h" + +#include +#include + +namespace { + +int PositiveModulo(const int value, const int modulus) { + const int remainder = value % modulus; + return remainder < 0 ? remainder + modulus : remainder; +} + +int FloorDivide(const int value, const int divisor) { + int quotient = value / divisor; + if (value < 0 && value % divisor != 0) { + --quotient; + } + return quotient; +} + +} // namespace + +void idCatmullRomSpline::Sort() { + for (int end = numKnots - 1; end > 0; --end) { + int maximumIndex = 0; + for (int index = 1; index <= end; ++index) { + if (times[index] > times[maximumIndex]) { + maximumIndex = index; + } + } + std::swap(times[maximumIndex], times[end]); + std::swap(values[maximumIndex], values[end]); + } + changed = true; +} + +void idCatmullRomSpline::Normalize(const float totalTime) { + if (numKnots < 1 || times[numKnots - 1] == 0.0f) { + return; + } + + const float scale = totalTime / times[numKnots - 1]; + for (int index = 0; index < numKnots; ++index) { + times[index] *= scale; + } + changed = true; +} + +float idCatmullRomSpline::ClampedTime(const float time) const { + if (boundaryType == CLAMPED) { + if (time < times[0]) { + return times[0]; + } + if (time >= times[numKnots - 1]) { + return times[numKnots - 1]; + } + return time; + } + + if (boundaryType == CLOSED) { + const float period = times[numKnots - 1] + closeTime; + if (period == 0.0f) { + return times[0]; + } + return time - std::floor(time / period) * period; + } + + return time; +} + +int idCatmullRomSpline::IndexForTime( + const float time, + const bool fastSearch +) const { + if (fastSearch && currentIndex >= 0 && currentIndex <= numKnots) { + if (currentIndex == 0) { + if (time <= times[0]) { + return 0; + } + } else if (currentIndex == numKnots) { + if (time > times[numKnots - 1]) { + return currentIndex; + } + } else if (time > times[currentIndex - 1] + && time <= times[currentIndex]) { + return currentIndex; + } else if (currentIndex + 1 <= numKnots + && time > times[currentIndex] + && (currentIndex + 1 == numKnots + || time <= times[currentIndex + 1])) { + ++currentIndex; + return currentIndex; + } + } + + int low = 0; + int high = numKnots; + while (low < high) { + const int middle = low + (high - low) / 2; + if (time <= times[middle]) { + high = middle; + } else { + low = middle + 1; + } + } + + if (fastSearch) { + currentIndex = low; + } + return low; +} + +float idCatmullRomSpline::TimeForIndex(const int index) const { + const int lastIndex = numKnots - 1; + if (index >= 0 && index <= lastIndex) { + return times[index]; + } + + if (boundaryType == CLOSED) { + const float period = times[lastIndex] + closeTime; + const int cycle = FloorDivide(index, numKnots); + return static_cast(cycle) * period + + times[PositiveModulo(index, numKnots)]; + } + + if (index < 0) { + return times[0] + + static_cast(index) * (times[1] - times[0]); + } + return times[lastIndex] + + static_cast(index - lastIndex) + * (times[lastIndex] - times[lastIndex - 1]); +} + +float idCatmullRomSpline::ValueForIndex(const int index) const { + const int lastIndex = numKnots - 1; + if (index >= 0 && index <= lastIndex) { + return values[index]; + } + + if (boundaryType == CLOSED) { + return values[PositiveModulo(index, numKnots)]; + } + if (boundaryType == CLAMPED) { + return index < 0 ? values[0] : values[lastIndex]; + } + + if (index < 0) { + return values[0] + + static_cast(index) * (values[1] - values[0]); + } + return values[lastIndex] + + static_cast(index - lastIndex) + * (values[lastIndex] - values[lastIndex - 1]); +} + +void idCatmullRomSpline::Basis( + const int index, + const float time, + float basisValues[4] +) const { + const float intervalStart = TimeForIndex(index); + const float intervalEnd = TimeForIndex(index + 1); + const float intervalLength = intervalEnd - intervalStart; + const float s = intervalLength == 0.0f + ? 0.0f + : (time - intervalStart) / intervalLength; + + basisValues[0] = 0.5f * (((2.0f - s) * s - 1.0f) * s); + basisValues[1] = 0.5f * ((((3.0f * s - 5.0f) * s) * s) + 2.0f); + basisValues[2] = 0.5f * ((((4.0f - 3.0f * s) * s + 1.0f) * s)); + basisValues[3] = 0.5f * ((s - 1.0f) * s * s); +} + +float idCatmullRomSpline::GetCurrentValue( + const float time, + const bool fastSearch +) const { + if (numKnots == 1) { + return values[0]; + } + + const float adjustedTime = ClampedTime(time); + const int upperIndex = IndexForTime(adjustedTime, fastSearch); + const int basisIndex = upperIndex - 1; + float basisValues[4]; + Basis(basisIndex, adjustedTime, basisValues); + + float result = 0.0f; + for (int index = 0; index < 4; ++index) { + result += ValueForIndex(basisIndex - 1 + index) + * basisValues[index]; + } + return result; +} + +idLookupTable::idLookupTable() + : clamp(false) + , snap(false) + , spline(false) + , minimum(0.0f) + , maximum(1.0f) + , values{} { + values.numKnots = 0; + values.currentIndex = -1; + values.changed = true; + values.boundaryType = idCatmullRomSpline::CLOSED; + values.closeTime = 1.0f; +} + +void idLookupTable::SetSnap(const bool enabled) { + snap = enabled; + if (spline && enabled) { + spline = false; + } +} + +void idLookupTable::SetSpline(const bool enabled) { + spline = enabled; + if (enabled && snap) { + snap = false; + } +} + +void idLookupTable::Finalize() { + values.Sort(); + if (values.numKnots > 1 && values.times[values.numKnots - 1] > 1.0f) { + const float totalTime = clamp + ? static_cast(values.numKnots - 1) + : 1.0f; + values.Normalize(totalTime); + } + if (values.numKnots > 0) { + values.closeTime = 1.0f - values.times[values.numKnots - 1]; + } else { + values.closeTime = 1.0f; + } + values.changed = true; +} + +void idLookupTable::SetClamp(const bool enabled) { + clamp = enabled; + values.changed = true; + values.boundaryType = enabled + ? idCatmullRomSpline::CLAMPED + : idCatmullRomSpline::CLOSED; +} + +void idLookupTable::AddValue(const float time, const float value) { + if (values.numKnots >= 64) { + return; + } + + values.times[values.numKnots] = time; + values.values[values.numKnots] = value; + ++values.numKnots; + values.changed = true; +} + +void idLookupTable::Clear() { + values.numKnots = 0; + values.currentIndex = -1; + values.changed = true; + minimum = 0.0f; + maximum = 1.0f; + clamp = false; + snap = false; + spline = false; + values.boundaryType = idCatmullRomSpline::CLOSED; + values.closeTime = 1.0f; +} + +float idLookupTable::TableLookupNormalized( + const float time, + const bool fastSearch +) const { + if (values.numKnots == 0) { + return 0.0f; + } + if (values.numKnots == 1) { + return values.values[0]; + } + if (spline) { + return values.GetCurrentValue(time, fastSearch); + } + + if (clamp) { + if (time <= values.times[0]) { + return values.values[0]; + } + if (time >= values.times[values.numKnots - 1]) { + return values.values[values.numKnots - 1]; + } + } + + const float adjustedTime = values.ClampedTime(time); + const int upperIndex = values.IndexForTime(adjustedTime, fastSearch); + if (snap) { + return values.ValueForIndex(upperIndex - 1); + } + + const float lowerTime = values.TimeForIndex(upperIndex - 1); + const float upperTime = values.TimeForIndex(upperIndex); + if (lowerTime == upperTime) { + return 0.0f; + } + + const float lowerValue = values.ValueForIndex(upperIndex - 1); + const float upperValue = values.ValueForIndex(upperIndex); + const float fraction = (adjustedTime - lowerTime) + / (upperTime - lowerTime); + return lowerValue + (upperValue - lowerValue) * fraction; +} + +float idLookupTable::TableLookup( + const float time, + const bool fastSearch +) const { + return minimum + (maximum - minimum) + * TableLookupNormalized(time, fastSearch); +} diff --git a/source/shared/idlib/lookuptable.h b/source/shared/idlib/lookuptable.h new file mode 100644 index 0000000..a5e3a99 --- /dev/null +++ b/source/shared/idlib/lookuptable.h @@ -0,0 +1,61 @@ +#pragma once + +template +class idCatmullRomSpline; + +template<> +class idCatmullRomSpline { +public: + enum boundary_t { + FREE = 0, + CLAMPED = 1, + CLOSED = 2 + }; + + float times[64]; + float values[64]; + int numKnots; + mutable int currentIndex; + bool changed; + boundary_t boundaryType; + float closeTime; + + void Sort(); + void Normalize(float totalTime); + float ClampedTime(float time) const; + int IndexForTime(float time, bool fastSearch) const; + float TimeForIndex(int index) const; + float ValueForIndex(int index) const; + void Basis(int index, float time, float basisValues[4]) const; + float GetCurrentValue(float time, bool fastSearch) const; +}; + +static_assert( + sizeof(idCatmullRomSpline) == 532, + "Recovered idCatmullRomSpline layout changed" +); + +class idLookupTable { +public: + idLookupTable(); + + void SetSnap(bool enabled); + void SetSpline(bool enabled); + void Finalize(); + void SetClamp(bool enabled); + void AddValue(float time, float value); + void Clear(); + + float TableLookupNormalized(float time, bool fastSearch) const; + float TableLookup(float time, bool fastSearch) const; + +private: + bool clamp; + bool snap; + bool spline; + float minimum; + float maximum; + idCatmullRomSpline values; +}; + +static_assert(sizeof(idLookupTable) == 544, "idLookupTable ABI changed"); diff --git a/source/shared/idlib/math/basictypes.h b/source/shared/idlib/math/basictypes.h new file mode 100644 index 0000000..2fbff7e --- /dev/null +++ b/source/shared/idlib/math/basictypes.h @@ -0,0 +1,89 @@ +#pragma once + +#include + +class idBoundedIntBase { +public: + virtual ~idBoundedIntBase() = default; + virtual void SetValue(int value) = 0; + virtual int GetValue() const = 0; +}; + +template +class idBoundedInt final : public idBoundedIntBase { +public: + explicit idBoundedInt(const int initialValue = MIN_VALUE) + : value(MIN_VALUE) { + SetValue(initialValue); + } + + void SetValue(const int newValue) override { + value = newValue < MIN_VALUE ? MIN_VALUE + : (newValue > MAX_VALUE ? MAX_VALUE : newValue); + } + + int GetValue() const override { + return value; + } + + operator int() const { + return value; + } + +private: + int value; +}; + +class idBoundedFloatBase { +public: + virtual ~idBoundedFloatBase() = default; + virtual void SetValue(float value) = 0; + virtual float GetValue() const = 0; +}; + +// The original uses four integral template arguments so floating-point bounds +// remain legal in the C++03-era source. Only <0,0,1,0> occurs in tungsten; +// the second and fourth arguments represent the fractional decimal component. +template +class idBoundedFloat final : public idBoundedFloatBase { +public: + explicit idBoundedFloat(const float initialValue = Minimum()) + : value(Minimum()) { + SetValue(initialValue); + } + + void SetValue(const float newValue) override { + value = newValue < Minimum() ? Minimum() + : (newValue > Maximum() ? Maximum() : newValue); + } + + float GetValue() const override { + return value; + } + + operator float() const { + return value; + } + +private: + float value; + + static constexpr float Fraction(const int digits) { + return static_cast(digits) / 1000.0f; + } + + static constexpr float Minimum() { + return static_cast(MIN_WHOLE) + Fraction(MIN_FRACTION); + } + + static constexpr float Maximum() { + return static_cast(MAX_WHOLE) + Fraction(MAX_FRACTION); + } +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idBoundedInt<0, 4>) == 8, + "Recovered idBoundedInt ABI changed"); +static_assert(sizeof(idBoundedFloat<0, 0, 1, 0>) == 8, + "Recovered idBoundedFloat ABI changed"); +#endif diff --git a/source/shared/idlib/math/decay.cpp b/source/shared/idlib/math/decay.cpp new file mode 100644 index 0000000..29ead17 --- /dev/null +++ b/source/shared/idlib/math/decay.cpp @@ -0,0 +1,53 @@ +#include "decay.h" + +#include + +idParametricDecay::idParametricDecay() + : delta(0.0f) + , linear(0.0f) + , t0(0.0f) + , tdelta(0.0f) + , lambda(0.0f) { +} + +void idParametricDecay::Init( + const float newDelta, + const float newLinear, + const float newT0, + const float newTDelta, + const float newLambda +) { + delta = newDelta; + linear = newLinear; + t0 = newT0; + tdelta = newTDelta; + lambda = newLambda; +} + +void idParametricDecay::SetTZero(const float newT0) { + t0 = newT0; +} + +void idParametricDecay::SetDelta(const float newDelta) { + delta = newDelta; +} + +float idParametricDecay::Evaluate(const float t) const { + if (t < t0) { + return delta; + } + + const float elapsed = t - t0; + if (elapsed > tdelta) { + return 0.0f; + } + + const float normalizedTime = elapsed / tdelta; + const float exponential = std::pow( + 0.5f, + elapsed / (lambda * tdelta) + ); + return ((1.0f - linear) * exponential + + (1.0f - normalizedTime) * linear) * delta; +} + diff --git a/source/shared/idlib/math/decay.h b/source/shared/idlib/math/decay.h new file mode 100644 index 0000000..5152b91 --- /dev/null +++ b/source/shared/idlib/math/decay.h @@ -0,0 +1,23 @@ +#pragma once + +// tungsten.exe.h type 12849. +class idParametricDecay { +public: + idParametricDecay(); + + void Init(float delta, float linear, float t0, float tdelta, float lambda); + void SetTZero(float t0); + void SetDelta(float delta); + float Evaluate(float t) const; + +private: + float delta; + float linear; + float t0; + float tdelta; + float lambda; +}; + +static_assert(sizeof(idParametricDecay) == 20, + "Recovered idParametricDecay layout changed"); + diff --git a/source/shared/idlib/math/fader.h b/source/shared/idlib/math/fader.h new file mode 100644 index 0000000..be29061 --- /dev/null +++ b/source/shared/idlib/math/fader.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +class idFader { +public: + enum type_t { + FADE_LINEAR = 0, + FADE_SINE = 1, + FADE_INVERSE_SINE = 2 + }; + + idFader(const type_t fadeType = FADE_LINEAR, const float initialValue = 0.0f) + : type(fadeType), startTime(0), duration(0), + startValue(initialValue), endValue(initialValue) { + } + + float GetValue(const int time) const { + switch (type) { + case FADE_SINE: return GetSine(time); + case FADE_INVERSE_SINE: return GetInverseSine(time); + default: return GetLinear(time); + } + } + + float GetLinear(const int time) const { + return Interpolate(time, LinearFraction(time)); + } + + float GetSine(const int time) const { + const float fraction = LinearFraction(time); + const float shaped = std::sin(fraction * 1.5707963267948966f); + return Interpolate(time, shaped); + } + + float GetInverseSine(const int time) const { + const float fraction = LinearFraction(time); + const float shaped = 1.0f + std::sin( + fraction * 1.5707963267948966f + 4.71238898038469f + ); + return Interpolate(time, shaped); + } + + void FadeTowards(const float newEndValue, const int time, + const int newDuration) { + startValue = GetValue(time); + startTime = time; + endValue = newEndValue; + duration = newDuration < 0 ? 0 : newDuration; + } + + void SetType(const type_t fadeType) { + type = fadeType; + } + +private: + type_t type; + int startTime; + int duration; + float startValue; + float endValue; + + float LinearFraction(const int time) const { + if (time <= startTime) { + return 0.0f; + } + if (duration <= 0) { + return 1.0f; + } + if (time >= startTime + duration) { + return 1.0f; + } + return static_cast(time - startTime) / static_cast(duration); + } + + float Interpolate(const int time, const float fraction) const { + if (time < startTime) { + return startValue; + } + if (duration <= 0 || time >= startTime + duration) { + return endValue; + } + return startValue + fraction * (endValue - startValue); + } +}; + +static_assert(sizeof(idFader) == 20, "Recovered idFader ABI changed"); diff --git a/source/shared/idlib/math/mat3x4.h b/source/shared/idlib/math/mat3x4.h new file mode 100644 index 0000000..7f0dda2 --- /dev/null +++ b/source/shared/idlib/math/mat3x4.h @@ -0,0 +1,82 @@ +#pragma once + +#include "idlib/precompiled.h" + +class idMat3x4 { +public: + idMat3x4() { + Identity(); + } + + idMat3x4(const idMat3& rotation, const idVec3& translation) { + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + mat[row * 4 + column] = rotation[column][row]; + } + mat[row * 4 + 3] = translation[row]; + } + } + + void Identity() { + for (int index = 0; index < 12; ++index) { + mat[index] = 0.0f; + } + mat[0] = mat[5] = mat[10] = 1.0f; + } + + void Transform(idVec3& result, const idVec3& value) const { + result.x = value.x * mat[0] + value.y * mat[1] + + value.z * mat[2] + mat[3]; + result.y = value.x * mat[4] + value.y * mat[5] + + value.z * mat[6] + mat[7]; + result.z = value.x * mat[8] + value.y * mat[9] + + value.z * mat[10] + mat[11]; + } + + void Rotate(idMat3& result, const idMat3& value) const { + for (int column = 0; column < 3; ++column) { + result[column].x = mat[0] * value[column].x + + mat[1] * value[column].y + mat[2] * value[column].z; + result[column].y = mat[4] * value[column].x + + mat[5] * value[column].y + mat[6] * value[column].z; + result[column].z = mat[8] * value[column].x + + mat[9] * value[column].y + mat[10] * value[column].z; + } + } + + void Invert() { + const float old[12] = { + mat[0], mat[1], mat[2], mat[3], + mat[4], mat[5], mat[6], mat[7], + mat[8], mat[9], mat[10], mat[11] + }; + mat[0] = old[0]; mat[1] = old[4]; mat[2] = old[8]; + mat[4] = old[1]; mat[5] = old[5]; mat[6] = old[9]; + mat[8] = old[2]; mat[9] = old[6]; mat[10] = old[10]; + mat[3] = -(mat[0] * old[3] + mat[1] * old[7] + mat[2] * old[11]); + mat[7] = -(mat[4] * old[3] + mat[5] * old[7] + mat[6] * old[11]); + mat[11] = -(mat[8] * old[3] + mat[9] * old[7] + mat[10] * old[11]); + } + + void LeftTransposeMultiply(const idMat3& value) { + float old[12]; + for (int index = 0; index < 12; ++index) { + old[index] = mat[index]; + } + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 4; ++column) { + mat[row * 4 + column] = value[0][row] * old[column] + + value[1][row] * old[4 + column] + + value[2][row] * old[8 + column]; + } + } + } + + float* ToFloatPtr() { return mat; } + const float* ToFloatPtr() const { return mat; } + +private: + float mat[12]; +}; + +static_assert(sizeof(idMat3x4) == 48, "Recovered idMat3x4 ABI changed"); diff --git a/source/shared/idlib/math/mathlib.cpp b/source/shared/idlib/math/mathlib.cpp new file mode 100644 index 0000000..1dc2c3d --- /dev/null +++ b/source/shared/idlib/math/mathlib.cpp @@ -0,0 +1,31 @@ +#include "mathlib.h" + +#include + +int InterleaveBits(const int x, const int y) { + const std::uint32_t xBits = static_cast(x); + const std::uint32_t yBits = static_cast(y); + std::uint32_t interleaved = 0; + + for (int bit = 0; bit < 16; ++bit) { + interleaved |= ((xBits >> bit) & 1u) << (bit * 2); + interleaved |= ((yBits >> bit) & 1u) << (bit * 2 + 1); + } + + return static_cast(interleaved); +} + +void DeInterleaveBits(const int bits, int& x, int& y) { + const std::uint32_t interleaved = static_cast(bits); + std::uint32_t xBits = 0; + std::uint32_t yBits = 0; + + for (int bit = 0; bit < 16; ++bit) { + xBits |= ((interleaved >> (bit * 2)) & 1u) << bit; + yBits |= ((interleaved >> (bit * 2 + 1)) & 1u) << bit; + } + + x = static_cast(xBits); + y = static_cast(yBits); +} + diff --git a/source/shared/idlib/math/mathlib.h b/source/shared/idlib/math/mathlib.h new file mode 100644 index 0000000..01100a8 --- /dev/null +++ b/source/shared/idlib/math/mathlib.h @@ -0,0 +1,8 @@ +#pragma once + +// Recovered PDB signatures: +// ?InterleaveBits@@YAHHH@Z +// ?DeInterleaveBits@@YAXHAAH0@Z +int InterleaveBits(int x, int y); +void DeInterleaveBits(int bits, int& x, int& y); + diff --git a/source/shared/idlib/math/spatialmat.cpp b/source/shared/idlib/math/spatialmat.cpp new file mode 100644 index 0000000..455327d --- /dev/null +++ b/source/shared/idlib/math/spatialmat.cpp @@ -0,0 +1,391 @@ +#include "spatialmat.h" + +#include +#include +#include +#include + +namespace { + +constexpr std::size_t SPATIAL_MAT_FLOATS = + idSpatialMat::MAX_ROWS * idSpatialMat::ROW_STRIDE; +constexpr std::size_t SPATIAL_MAT_BYTES = + SPATIAL_MAT_FLOATS * sizeof(float); + +float* AllocSpatialMat() { + return static_cast(_aligned_malloc(SPATIAL_MAT_BYTES, 16)); +} + +bool IsValidSize(const int rows, const int columns) { + return rows >= 0 && rows <= idSpatialMat::MAX_ROWS + && columns >= 0 && columns <= idSpatialMat::MAX_COLUMNS; +} + +} // namespace + +idSpatialMat::idSpatialMat() + : numRows(0), numColumns(0), allocatedRows(0), mat(nullptr) { +} + +idSpatialMat::idSpatialMat(const int rows, const int columns) + : idSpatialMat() { + SetSize(rows, columns); +} + +idSpatialMat::idSpatialMat(const idSpatialMat& other) + : idSpatialMat() { + *this = other; +} + +idSpatialMat::~idSpatialMat() { + if (mat != nullptr && allocatedRows > 0) { + _aligned_free(mat); + } +} + +idSpatialMat& idSpatialMat::operator=(const idSpatialMat& other) { + if (this == &other) { + return *this; + } + SetSize(other.numRows, other.numColumns); + if (mat != nullptr && other.mat != nullptr) { + std::memcpy(mat, other.mat, SPATIAL_MAT_BYTES); + } + return *this; +} + +void idSpatialMat::SetSize(const int rows, const int columns) { + if (!IsValidSize(rows, columns)) { + numRows = 0; + numColumns = 0; + return; + } + if (mat == nullptr) { + mat = AllocSpatialMat(); + if (mat == nullptr) { + numRows = 0; + numColumns = 0; + allocatedRows = 0; + return; + } + allocatedRows = MAX_ROWS; + std::memset(mat, 0, SPATIAL_MAT_BYTES); + } + numRows = rows; + numColumns = columns; + ClearPadding(); +} + +void idSpatialMat::ChangeNumRows(const int rows) { + if (rows < 0 || rows > MAX_ROWS || mat == nullptr) { + return; + } + if (rows != numRows) { + const int firstClearedRow = std::min(rows, numRows); + const int rowCount = std::max(rows, numRows) - firstClearedRow; + if (rowCount > 0) { + std::memset(mat + firstClearedRow * ROW_STRIDE, 0, + static_cast(rowCount * ROW_STRIDE) * sizeof(float)); + } + numRows = rows; + } +} + +void idSpatialMat::Zero(const int rows, const int columns) { + SetSize(rows, columns); + Zero(); +} + +void idSpatialMat::Zero() { + if (mat != nullptr) { + std::memset(mat, 0, SPATIAL_MAT_BYTES); + } +} + +void idSpatialMat::Set(const idMat3& m1, const idMat3& m2) { + SetSize(3, 6); + if (mat == nullptr) { + return; + } + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + (*this)(row, column) = m1[row][column]; + (*this)(row, column + 3) = m2[row][column]; + } + } +} + +void idSpatialMat::Set(const idMat3& m1, const idMat3& m2, + const idMat3& m3, const idMat3& m4) { + SetSize(6, 6); + if (mat == nullptr) { + return; + } + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + (*this)(row, column) = m1[row][column]; + (*this)(row, column + 3) = m2[row][column]; + (*this)(row + 3, column) = m3[row][column]; + (*this)(row + 3, column + 3) = m4[row][column]; + } + } +} + +void idSpatialMat::SetData(const int rows, const int columns, float* data) { + if (!IsValidSize(rows, columns) || data == nullptr) { + return; + } + if (mat != nullptr && allocatedRows > 0) { + _aligned_free(mat); + } + numRows = rows; + numColumns = columns; + allocatedRows = -MAX_ROWS; + mat = data; + ClearPadding(); +} + +void idSpatialMat::ClearPadding() { + if (mat == nullptr) { + return; + } + for (int row = 0; row < MAX_ROWS; ++row) { + const int first = row < numRows ? numColumns : 0; + std::fill(mat + row * ROW_STRIDE + first, + mat + (row + 1) * ROW_STRIDE, 0.0f); + } +} + +void idSpatialMat::Negate() { + if (mat == nullptr) { + return; + } + for (int row = 0; row < numRows; ++row) { + for (int column = 0; column < ROW_STRIDE; ++column) { + (*this)[row][column] = -(*this)[row][column]; + } + } +} + +void idSpatialMat::Transpose(idSpatialMat& dst) const { + float values[MAX_ROWS][MAX_COLUMNS] = {}; + for (int row = 0; row < numRows; ++row) { + for (int column = 0; column < numColumns; ++column) { + values[column][row] = (*this)(row, column); + } + } + dst.Zero(numColumns, numRows); + for (int row = 0; row < numColumns; ++row) { + for (int column = 0; column < numRows; ++column) { + dst(row, column) = values[row][column]; + } + } +} + +void idSpatialMat::Subtract(const idSpatialMat& other) { + if (mat == nullptr || other.mat == nullptr + || numRows != other.numRows || numColumns != other.numColumns) { + return; + } + for (int index = 0; index < MAX_ROWS * ROW_STRIDE; ++index) { + mat[index] -= other.mat[index]; + } +} + +void idSpatialMat::Multiply(idSpatialVec& dst, const idSpatialVec& vec) const { + float result[MAX_ROWS] = {}; + const int terms = std::min(numColumns, vec.GetSize()); + for (int row = 0; row < numRows; ++row) { + for (int column = 0; column < terms; ++column) { + result[row] += (*this)(row, column) * vec[column]; + } + } + dst.SetSize(numRows); + for (int row = 0; row < numRows; ++row) { + dst[row] = result[row]; + } +} + +void idSpatialMat::MultiplyAdd(idSpatialVec& dst, const idSpatialVec& vec) const { + if (dst.GetSize() < numRows) { + dst.SetSize(numRows); + } + const int terms = std::min(numColumns, vec.GetSize()); + for (int row = 0; row < numRows; ++row) { + float value = 0.0f; + for (int column = 0; column < terms; ++column) { + value += (*this)(row, column) * vec[column]; + } + dst[row] += value; + } +} + +void idSpatialMat::MultiplySub(idSpatialVec& dst, const idSpatialVec& vec) const { + if (dst.GetSize() < numRows) { + dst.SetSize(numRows); + } + const int terms = std::min(numColumns, vec.GetSize()); + for (int row = 0; row < numRows; ++row) { + float value = 0.0f; + for (int column = 0; column < terms; ++column) { + value += (*this)(row, column) * vec[column]; + } + dst[row] -= value; + } +} + +void idSpatialMat::TransposeMultiplyAdd(idSpatialVec& dst, + const idSpatialVec& vec) const { + if (dst.GetSize() < numColumns) { + dst.SetSize(numColumns); + } + const int terms = std::min(numRows, vec.GetSize()); + for (int column = 0; column < numColumns; ++column) { + float value = 0.0f; + for (int row = 0; row < terms; ++row) { + value += (*this)(row, column) * vec[row]; + } + dst[column] += value; + } +} + +void idSpatialMat::TransposeMultiplySub(idSpatialVec& dst, + const idSpatialVec& vec) const { + if (dst.GetSize() < numColumns) { + dst.SetSize(numColumns); + } + const int terms = std::min(numRows, vec.GetSize()); + for (int column = 0; column < numColumns; ++column) { + float value = 0.0f; + for (int row = 0; row < terms; ++row) { + value += (*this)(row, column) * vec[row]; + } + dst[column] -= value; + } +} + +void idSpatialMat::Multiply(idSpatialMat& dst, + const idSpatialMat& other) const { + if (numColumns != other.numRows) { + dst.Zero(0, 0); + return; + } + float values[MAX_ROWS][MAX_COLUMNS] = {}; + for (int row = 0; row < numRows; ++row) { + for (int column = 0; column < other.numColumns; ++column) { + for (int term = 0; term < numColumns; ++term) { + values[row][column] += + (*this)(row, term) * other(term, column); + } + } + } + dst.Zero(numRows, other.numColumns); + for (int row = 0; row < numRows; ++row) { + for (int column = 0; column < other.numColumns; ++column) { + dst(row, column) = values[row][column]; + } + } +} + +void idSpatialMat::TransposeMultiply(idSpatialMat& dst, + const idSpatialMat& other) const { + if (numRows != other.numRows) { + dst.Zero(0, 0); + return; + } + float values[MAX_ROWS][MAX_COLUMNS] = {}; + for (int row = 0; row < numColumns; ++row) { + for (int column = 0; column < other.numColumns; ++column) { + for (int term = 0; term < numRows; ++term) { + values[row][column] += + (*this)(term, row) * other(term, column); + } + } + } + dst.Zero(numColumns, other.numColumns); + for (int row = 0; row < numColumns; ++row) { + for (int column = 0; column < other.numColumns; ++column) { + dst(row, column) = values[row][column]; + } + } +} + +bool idSpatialMat::Inverse(idSpatialMat& dst) const { + if (numRows != numColumns || numRows < 1 || numRows > MAX_ROWS) { + return false; + } + switch (numRows) { + case 1: return Inverse1x1(dst); + case 2: return Inverse2x2(dst); + case 3: return Inverse3x3(dst); + case 4: return Inverse4x4(dst); + case 5: return Inverse5x5(dst); + case 6: return Inverse6x6(dst); + default: return false; + } +} + +bool idSpatialMat::InverseNxN(idSpatialMat& dst, const int dimension) const { + double work[MAX_ROWS][MAX_ROWS * 2] = {}; + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + work[row][column] = (*this)(row, column); + } + work[row][dimension + row] = 1.0; + } + + for (int pivotColumn = 0; pivotColumn < dimension; ++pivotColumn) { + int pivotRow = pivotColumn; + for (int row = pivotColumn + 1; row < dimension; ++row) { + if (std::fabs(work[row][pivotColumn]) + > std::fabs(work[pivotRow][pivotColumn])) { + pivotRow = row; + } + } + if (std::fabs(work[pivotRow][pivotColumn]) < 1.0e-14) { + return false; + } + if (pivotRow != pivotColumn) { + for (int column = 0; column < dimension * 2; ++column) { + std::swap(work[pivotRow][column], work[pivotColumn][column]); + } + } + const double reciprocal = 1.0 / work[pivotColumn][pivotColumn]; + for (int column = 0; column < dimension * 2; ++column) { + work[pivotColumn][column] *= reciprocal; + } + for (int row = 0; row < dimension; ++row) { + if (row == pivotColumn) { + continue; + } + const double scale = work[row][pivotColumn]; + for (int column = 0; column < dimension * 2; ++column) { + work[row][column] -= scale * work[pivotColumn][column]; + } + } + } + + dst.Zero(dimension, dimension); + for (int row = 0; row < dimension; ++row) { + for (int column = 0; column < dimension; ++column) { + dst(row, column) = static_cast(work[row][dimension + column]); + } + } + return true; +} + +bool idSpatialMat::Inverse1x1(idSpatialMat& dst) const { return InverseNxN(dst, 1); } +bool idSpatialMat::Inverse2x2(idSpatialMat& dst) const { return InverseNxN(dst, 2); } +bool idSpatialMat::Inverse3x3(idSpatialMat& dst) const { return InverseNxN(dst, 3); } +bool idSpatialMat::Inverse4x4(idSpatialMat& dst) const { return InverseNxN(dst, 4); } +bool idSpatialMat::Inverse5x5(idSpatialMat& dst) const { return InverseNxN(dst, 5); } +bool idSpatialMat::Inverse6x6(idSpatialMat& dst) const { return InverseNxN(dst, 6); } + +idSpatialVec idSpatialMat::SubSpatialVec(const int row) const { + idSpatialVec result; + if (mat != nullptr && row >= 0 && row < numRows) { + result.SetData(MAX_COLUMNS, mat + row * ROW_STRIDE); + } + return result; +} + diff --git a/source/shared/idlib/math/spatialmat.h b/source/shared/idlib/math/spatialmat.h new file mode 100644 index 0000000..7065017 --- /dev/null +++ b/source/shared/idlib/math/spatialmat.h @@ -0,0 +1,77 @@ +#pragma once + +#include "idlib/precompiled.h" + +#include "spatialvec.h" + +// Tungsten stores every spatial matrix in a six-row, eight-float-stride slab. +// The two padding floats per row are intentional: the Xenon implementation +// loads complete VMX vectors from each half-row. +class idSpatialMat { +public: + static const int MAX_ROWS = 6; + static const int MAX_COLUMNS = 6; + static const int ROW_STRIDE = 8; + + idSpatialMat(); + idSpatialMat(int rows, int columns); + idSpatialMat(const idSpatialMat& other); + ~idSpatialMat(); + + idSpatialMat& operator=(const idSpatialMat& other); + + void SetSize(int rows, int columns); + void ChangeNumRows(int rows); + void Zero(int rows, int columns); + void Zero(); + void Set(const idMat3& m1, const idMat3& m2); + void Set(const idMat3& m1, const idMat3& m2, + const idMat3& m3, const idMat3& m4); + void SetData(int rows, int columns, float* data); + void Negate(); + + void Transpose(idSpatialMat& dst) const; + void Subtract(const idSpatialMat& other); + + void Multiply(idSpatialVec& dst, const idSpatialVec& vec) const; + void MultiplyAdd(idSpatialVec& dst, const idSpatialVec& vec) const; + void MultiplySub(idSpatialVec& dst, const idSpatialVec& vec) const; + void TransposeMultiplyAdd(idSpatialVec& dst, const idSpatialVec& vec) const; + void TransposeMultiplySub(idSpatialVec& dst, const idSpatialVec& vec) const; + void Multiply(idSpatialMat& dst, const idSpatialMat& other) const; + void TransposeMultiply(idSpatialMat& dst, const idSpatialMat& other) const; + + bool Inverse(idSpatialMat& dst) const; + idSpatialVec SubSpatialVec(int row) const; + + int GetNumRows() const { return numRows; } + int GetNumColumns() const { return numColumns; } + int GetAllocatedRows() const { return allocatedRows; } + float* ToFloatPtr() { return mat; } + const float* ToFloatPtr() const { return mat; } + + float* operator[](int row) { return mat + row * ROW_STRIDE; } + const float* operator[](int row) const { return mat + row * ROW_STRIDE; } + float& operator()(int row, int column) { return mat[row * ROW_STRIDE + column]; } + float operator()(int row, int column) const { return mat[row * ROW_STRIDE + column]; } + +private: + bool InverseNxN(idSpatialMat& dst, int dimension) const; + bool Inverse1x1(idSpatialMat& dst) const; + bool Inverse2x2(idSpatialMat& dst) const; + bool Inverse3x3(idSpatialMat& dst) const; + bool Inverse4x4(idSpatialMat& dst) const; + bool Inverse5x5(idSpatialMat& dst) const; + bool Inverse6x6(idSpatialMat& dst) const; + void ClearPadding(); + + int numRows; + int numColumns; + int allocatedRows; + float* mat; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idSpatialMat) == 16, "Recovered idSpatialMat ABI changed"); +#endif + diff --git a/source/shared/idlib/math/spatialvec.h b/source/shared/idlib/math/spatialvec.h new file mode 100644 index 0000000..9fde3a2 --- /dev/null +++ b/source/shared/idlib/math/spatialvec.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include + +class idSpatialVec { +public: + idSpatialVec() + : size(0), allocated(0), p(nullptr) { + } + + explicit idSpatialVec(const int length) + : idSpatialVec() { + SetSize(length); + } + + idSpatialVec(const idSpatialVec& other) + : idSpatialVec() { + SetSize(other.size); + if (p != nullptr && other.p != nullptr) { + std::memcpy(p, other.p, sizeof(float) * other.size); + } + } + + idSpatialVec(idSpatialVec&& other) noexcept + : size(other.size), allocated(other.allocated), p(other.p) { + other.size = 0; + other.allocated = 0; + other.p = nullptr; + } + + ~idSpatialVec() { + if (allocated > 0) { + std::free(p); + } + } + + idSpatialVec& operator=(const idSpatialVec& other) { + if (this != &other) { + SetSize(other.size); + if (p != nullptr && other.p != nullptr) { + std::memcpy(p, other.p, sizeof(float) * other.size); + } + } + return *this; + } + + idSpatialVec& operator=(idSpatialVec&& other) noexcept { + if (this != &other) { + if (allocated > 0) { + std::free(p); + } + size = other.size; + allocated = other.allocated; + p = other.p; + other.size = 0; + other.allocated = 0; + other.p = nullptr; + } + return *this; + } + + void SetData(const int length, float* data) { + if (allocated > 0) { + std::free(p); + } + p = data; + size = static_cast(std::max(0, length)); + allocated = static_cast(-std::max(8, length)); + if (p != nullptr) { + for (int index = size; index < -allocated; ++index) { + p[index] = 0.0f; + } + } + } + + bool SetSize(const int newSize) { + if (newSize < 0 || newSize > 32767) { + return false; + } + const int capacity = allocated < 0 ? -allocated : allocated; + if (p == nullptr || newSize > capacity) { + const int newCapacity = std::max(8, (newSize + 7) & ~7); + float* const replacement = static_cast( + std::calloc(static_cast(newCapacity), sizeof(float)) + ); + if (replacement == nullptr) { + return false; + } + if (p != nullptr) { + std::memcpy(replacement, p, + sizeof(float) * static_cast(std::min(size, newSize))); + } + if (allocated > 0) { + std::free(p); + } + p = replacement; + allocated = static_cast(newCapacity); + } else if (newSize > size) { + std::memset(p + size, 0, + sizeof(float) * static_cast(newSize - size)); + } + size = static_cast(newSize); + return true; + } + + void ChangeSize(const int newSize) { + SetSize(newSize); + } + + void Zero() { + if (p != nullptr) { + std::memset(p, 0, sizeof(float) * static_cast(size)); + } + } + + void Clamp(const float minimum, const float maximum) { + for (int index = 0; index < size; ++index) { + p[index] = std::max(minimum, std::min(maximum, p[index])); + } + } + + float LengthSqr() const { + float sum = 0.0f; + for (int index = 0; index < size; ++index) { + sum += p[index] * p[index]; + } + return sum; + } + + float Length() const { + return std::sqrt(LengthSqr()); + } + + int GetSize() const { return size; } + float* ToFloatPtr() { return p; } + const float* ToFloatPtr() const { return p; } + + float& operator[](const int index) { return p[index]; } + float operator[](const int index) const { return p[index]; } + +private: + std::int16_t size; + std::int16_t allocated; + float* p; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idSpatialVec) == 8, "Recovered idSpatialVec ABI changed"); +#endif diff --git a/source/shared/idlib/math/spring.h b/source/shared/idlib/math/spring.h new file mode 100644 index 0000000..8c64129 --- /dev/null +++ b/source/shared/idlib/math/spring.h @@ -0,0 +1,117 @@ +#pragma once + +#include "vector.h" + +#include +#include + +template +struct idSpringDimension; + +template<> struct idSpringDimension { static constexpr int value = 1; }; +template<> struct idSpringDimension { static constexpr int value = 2; }; +template<> struct idSpringDimension { static constexpr int value = 3; }; + +template +class idSpring { +public: + idSpring() + : maxSpeed(0.0f), hasPMax(false), hasPMin(false), + k(1.0f), c(2.0f), m(1.0f), restLength(0.0f) { + p0.Zero(); + p1.Zero(); + vel.Zero(); + pMin.Zero(); + pMax.Zero(); + } + + void SetConstants(float springConstant, const float dampingConstant) { + k = std::min(10000.0f, std::max(0.0f, springConstant)); + c = dampingConstant < 0.0f ? 2.0f * std::sqrt(m * k) + : dampingConstant; + } + + void SetMass(const float mass) { m = mass > 0.0f ? mass : 1.0f; } + void SetRestLength(const float length) { restLength = std::max(0.0f, length); } + void SetMaxSpeed(const float speed) { maxSpeed = speed; } + void SetAnchor(const vectorType& anchor) { p0 = anchor; } + void SetPosition(const vectorType& position) { p1 = position; } + void SetVelocity(const vectorType& velocity) { vel = velocity; } + void SetMinimum(const vectorType& minimum) { pMin = minimum; hasPMin = true; } + void SetMaximum(const vectorType& maximum) { pMax = maximum; hasPMax = true; } + void ClearMinimum() { hasPMin = false; } + void ClearMaximum() { hasPMax = false; } + + const vectorType& GetPosition() const { return p1; } + const vectorType& GetVelocity() const { return vel; } + + void Update(float deltaTime) { + while (deltaTime > 0.0f) { + const float step = std::min(deltaTime, 0.0085f); + deltaTime -= step; + + float distanceSquared = 0.0f; + float difference[idSpringDimension::value]; + for (int index = 0; index < idSpringDimension::value; ++index) { + difference[index] = p1[index] - p0[index]; + distanceSquared += difference[index] * difference[index]; + } + const float distance = std::sqrt(distanceSquared); + const float inverseDistance = distance > 0.00001f ? 1.0f / distance : 0.0f; + const float springForce = -(distance - restLength) * k; + for (int index = 0; index < idSpringDimension::value; ++index) { + const float force = difference[index] * inverseDistance * springForce + - vel[index] * c; + vel[index] += (force / m) * step; + } + + float speedSquared = 0.0f; + for (int index = 0; index < idSpringDimension::value; ++index) { + speedSquared += vel[index] * vel[index]; + } + const float speed = std::sqrt(speedSquared); + if (maxSpeed > 0.0f && speed > maxSpeed) { + const float scale = maxSpeed / speed; + for (int index = 0; index < idSpringDimension::value; ++index) { + vel[index] *= scale; + } + } + if (speed < 0.00001f) { + vel.Zero(); + } + for (int index = 0; index < idSpringDimension::value; ++index) { + p1[index] += vel[index] * step; + } + if (distance < 0.00001f) { + p1 = p0; + } + } + + for (int index = 0; index < idSpringDimension::value; ++index) { + if (hasPMin) { + p1[index] = std::max(p1[index], pMin[index]); + } + if (hasPMax) { + p1[index] = std::min(p1[index], pMax[index]); + } + } + } + +private: + vectorType p0; + vectorType p1; + vectorType vel; + float maxSpeed; + vectorType pMin; + vectorType pMax; + bool hasPMax; + bool hasPMin; + float k; + float c; + float m; + float restLength; +}; + +static_assert(sizeof(idSpring) == 44, "Recovered idSpring ABI changed"); +static_assert(sizeof(idSpring) == 64, "Recovered idSpring ABI changed"); +static_assert(sizeof(idSpring) == 84, "Recovered idSpring ABI changed"); diff --git a/source/shared/idlib/math/vector.h b/source/shared/idlib/math/vector.h new file mode 100644 index 0000000..49ff996 --- /dev/null +++ b/source/shared/idlib/math/vector.h @@ -0,0 +1,329 @@ +#pragma once + +#include +#include +#include +#include + +class idVec1 { +public: + float x; + + idVec1() = default; + explicit idVec1(const float newX) : x(newX) {} + void Zero() { x = 0.0f; } + float operator[](const int) const { return x; } + float& operator[](const int) { return x; } +}; + +static_assert(sizeof(idVec1) == 4, "Recovered idVec1 layout changed"); + +// Minimal recovered ABI surface for tungsten's idVec2. More vector operations +// will move here as their out-of-line idTech 5 implementations are activated. +class idVec2 { +public: + float x; + float y; + + idVec2() = default; + + idVec2(const float newX, const float newY) + : x(newX) + , y(newY) { + } + + void Set(const float newX, const float newY) { + x = newX; + y = newY; + } + + void Zero() { + x = 0.0f; + y = 0.0f; + } + + float operator[](const int index) const { + assert(index >= 0 && index < 2); + return (&x)[index]; + } + + float& operator[](const int index) { + assert(index >= 0 && index < 2); + return (&x)[index]; + } +}; + +static_assert(sizeof(idVec2) == 8, "Recovered idVec2 layout changed"); + +// Minimal recovered ABI surface for tungsten's idVec3. The class deliberately +// stays a three-float POD layout; Xbox-only SIMD assumptions belong in the PC +// portability layer rather than in this type. +class idVec3 { +public: + float x; + float y; + float z; + + idVec3() = default; + + idVec3(const float newX, const float newY, const float newZ) + : x(newX) + , y(newY) + , z(newZ) { + } + + void Set(const float newX, const float newY, const float newZ) { + x = newX; + y = newY; + z = newZ; + } + + void Zero() { + x = 0.0f; + y = 0.0f; + z = 0.0f; + } + + float operator[](const int index) const { + assert(index >= 0 && index < 3); + return (&x)[index]; + } + + float& operator[](const int index) { + assert(index >= 0 && index < 3); + return (&x)[index]; + } + + idVec3 operator-() const { + return idVec3(-x, -y, -z); + } + + idVec3 operator+(const idVec3& other) const { + return idVec3(x + other.x, y + other.y, z + other.z); + } + + idVec3 operator-(const idVec3& other) const { + return idVec3(x - other.x, y - other.y, z - other.z); + } + + idVec3 operator*(const float scale) const { + return idVec3(x * scale, y * scale, z * scale); + } + + float Dot(const idVec3& other) const { + return x * other.x + y * other.y + z * other.z; + } + + idVec3 Cross(const idVec3& other) const { + return idVec3( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + float LengthSqr() const { + return Dot(*this); + } + + float Length() const { + return std::sqrt(LengthSqr()); + } +}; + +static_assert(sizeof(idVec3) == 12, "Recovered idVec3 layout changed"); + +class idMat3 { +public: + idVec3 mat[3]; + + idMat3() = default; + explicit idMat3(float diagonal) { + mat[0].Set(diagonal, 0.0f, 0.0f); + mat[1].Set(0.0f, diagonal, 0.0f); + mat[2].Set(0.0f, 0.0f, diagonal); + } + + idVec3& operator[](const int index) { return mat[index]; } + const idVec3& operator[](const int index) const { return mat[index]; } +}; + +static_assert(sizeof(idMat3) == 36, "Recovered idMat3 layout changed"); + +class idVec4 { +public: + float x; + float y; + float z; + float w; + + idVec4() = default; + + idVec4( + const float newX, + const float newY, + const float newZ, + const float newW + ) + : x(newX) + , y(newY) + , z(newZ) + , w(newW) { + } + + void Set( + const float newX, + const float newY, + const float newZ, + const float newW + ) { + x = newX; + y = newY; + z = newZ; + w = newW; + } + + float operator[](const int index) const { + assert(index >= 0 && index < 4); + return (&x)[index]; + } + + float& operator[](const int index) { + assert(index >= 0 && index < 4); + return (&x)[index]; + } +}; + +static_assert(sizeof(idVec4) == 16, "Recovered idVec4 layout changed"); + +class idAngles { +public: + float pitch; + float yaw; + float roll; + + idAngles() = default; + idAngles(const float newPitch, const float newYaw, const float newRoll) + : pitch(newPitch), yaw(newYaw), roll(newRoll) { + } + + float operator[](const int index) const { return (&pitch)[index]; } + float& operator[](const int index) { return (&pitch)[index]; } +}; + +static_assert(sizeof(idAngles) == 12, "Recovered idAngles layout changed"); + +class idQuat { +public: + float x; + float y; + float z; + float w; + + idQuat() = default; + idQuat(const float newX, const float newY, const float newZ, const float newW) + : x(newX), y(newY), z(newZ), w(newW) { + } + + float operator[](const int index) const { return (&x)[index]; } + float& operator[](const int index) { return (&x)[index]; } +}; + +static_assert(sizeof(idQuat) == 16, "Recovered idQuat layout changed"); + +// The Xbox 360 type-information stream serializes the dynamic math types by +// their three/four-field facades. Keep these definitions allocation-simple on +// the standalone recovery targets; the complete idLib target uses BFG's +// layout-compatible implementations. +class idVecX { +public: + idVecX() : size(0), alloced(0), p(nullptr) {} + explicit idVecX(const int newSize) : idVecX() { SetSize(newSize); } + idVecX(const idVecX& other) : idVecX() { + SetSize(other.size); + if (size > 0) std::memcpy(p, other.p, sizeof(float) * size); + } + ~idVecX() { std::free(p); } + + idVecX& operator=(const idVecX& other) { + if (this != &other) { + SetSize(other.size); + if (size > 0) std::memcpy(p, other.p, sizeof(float) * size); + } + return *this; + } + + void SetSize(const int newSize) { + const int safeSize = newSize > 0 ? newSize : 0; + if (safeSize > alloced) { + float* const replacement = static_cast( + std::realloc(p, sizeof(float) * safeSize)); + if (replacement == nullptr) return; + p = replacement; + alloced = safeSize; + } + size = safeSize; + } + int GetSize() const { return size; } + float& operator[](const int index) { return p[index]; } + float operator[](const int index) const { return p[index]; } + +private: + int size; + int alloced; + float* p; +}; + +static_assert(sizeof(idVecX) == 12, "Recovered idVecX layout changed"); + +class idMatX { +public: + idMatX() : numRows(0), numColumns(0), alloced(0), mat(nullptr) {} + idMatX(const int rows, const int columns) : idMatX() { + SetSize(rows, columns); + } + idMatX(const idMatX& other) : idMatX() { + SetSize(other.numRows, other.numColumns); + const int count = numRows * numColumns; + if (count > 0) std::memcpy(mat, other.mat, sizeof(float) * count); + } + ~idMatX() { std::free(mat); } + + idMatX& operator=(const idMatX& other) { + if (this != &other) { + SetSize(other.numRows, other.numColumns); + const int count = numRows * numColumns; + if (count > 0) std::memcpy(mat, other.mat, sizeof(float) * count); + } + return *this; + } + + void SetSize(const int rows, const int columns) { + const int safeRows = rows > 0 ? rows : 0; + const int safeColumns = columns > 0 ? columns : 0; + const int count = safeRows * safeColumns; + if (count > alloced) { + float* const replacement = static_cast( + std::realloc(mat, sizeof(float) * count)); + if (replacement == nullptr) return; + mat = replacement; + alloced = count; + } + numRows = safeRows; + numColumns = safeColumns; + } + int GetNumRows() const { return numRows; } + int GetNumColumns() const { return numColumns; } + float* operator[](const int row) { return mat + row * numColumns; } + const float* operator[](const int row) const { + return mat + row * numColumns; + } + +private: + int numRows; + int numColumns; + int alloced; + float* mat; +}; + +static_assert(sizeof(idMatX) == 16, "Recovered idMatX layout changed"); diff --git a/source/shared/idlib/metrics/metricrecord.cpp b/source/shared/idlib/metrics/metricrecord.cpp new file mode 100644 index 0000000..291d140 --- /dev/null +++ b/source/shared/idlib/metrics/metricrecord.cpp @@ -0,0 +1,84 @@ +#include "metricrecord.h" + +#include "metricsframework.h" + +#include +#include + +namespace { + +FILE* MetricStream(idMetricFile* metricFile) { + return metricFile == nullptr ? nullptr + : static_cast(metricFile->fileHandle); +} + +void WriteU32(FILE* stream, const std::uint32_t value) { + unsigned char bytes[4] = { + static_cast(value >> 0), + static_cast(value >> 8), + static_cast(value >> 16), + static_cast(value >> 24) + }; + std::fwrite(bytes, 1, sizeof(bytes), stream); +} + +void WriteU64(FILE* stream, const std::uint64_t value) { + WriteU32(stream, static_cast(value)); + WriteU32(stream, static_cast(value >> 32)); +} + +void WriteString(FILE* stream, const char* text) { + const char* const safeText = text == nullptr ? "" : text; + const std::uint32_t length = static_cast(std::strlen(safeText)); + WriteU32(stream, length); + if (length > 0) std::fwrite(safeText, 1, length, stream); +} + +} // namespace + +idMetricRecord::idMetricRecord(const idStr& recordName) + : name(recordName), baseName(recordName) { +} + +void idMetricRecord::WriteHeader(idMetricFile* metricFile) { + FILE* const stream = MetricStream(metricFile); + if (stream == nullptr) return; + WriteU32(stream, 1); + WriteString(stream, "entryTime"); + WriteU32(stream, 0); +} + +void idMetricRecord::SerializeEntry(idMetricFile* metricFile) { + FILE* const stream = MetricStream(metricFile); + if (stream == nullptr) return; + const std::uint64_t now = metricsFrameworkLocal.GetCurrentSystemTime(); + WriteU32(stream, static_cast(now - metricFile->startTime)); +} + +void idMetricRecord::AppendToName(const idStr& suffix) { + name = baseName; + name.Append(suffix); +} + +void idMetricFile::WriteFileInfo(const idStr& metricName) { + FILE* const stream = MetricStream(this); + if (stream == nullptr) return; + startTime = metricsFrameworkLocal.GetCurrentSystemTime(); + WriteString(stream, "IDMETRIC"); + WriteU32(stream, 1); + WriteString(stream, metricName.c_str()); + WriteU64(stream, startTime); +} + +void idMetricRecord::Serialize() { + if (!metricsFrameworkLocal.IsRecording()) return; + idMetricFile* const metricFile = metricsFrameworkLocal.GetFileHandle(name); + if (metricFile == nullptr || metricFile->fileHandle == nullptr) return; + if (!metricFile->headerWritten) { + metricFile->headerWritten = true; + metricFile->WriteFileInfo(name); + WriteHeader(metricFile); + } + SerializeEntry(metricFile); +} + diff --git a/source/shared/idlib/metrics/metricrecord.h b/source/shared/idlib/metrics/metricrecord.h new file mode 100644 index 0000000..043381d --- /dev/null +++ b/source/shared/idlib/metrics/metricrecord.h @@ -0,0 +1,38 @@ +#pragma once + +#include "idlib/text/str.h" + +#include + +struct idMetricFile { + idMetricFile() : fileHandle(nullptr), headerWritten(false), startTime(0) {} + void WriteFileInfo(const idStr& metricName); + + void* fileHandle; + bool headerWritten; + std::uint64_t startTime; +}; + +class idMetricRecord { +public: + explicit idMetricRecord(const idStr& recordName); + virtual ~idMetricRecord() = default; + + virtual void WriteHeader(idMetricFile* metricFile); + virtual void SerializeEntry(idMetricFile* metricFile); + + void AppendToName(const idStr& suffix); + void Serialize(); + const idStr& GetName() const { return name; } + +protected: + idStr name; + idStr baseName; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idMetricFile) == 16, "Recovered idMetricFile ABI changed"); +static_assert(sizeof(idMetricRecord) == 68, + "Recovered idMetricRecord ABI changed"); +#endif + diff --git a/source/shared/idlib/metrics/metrics.cpp b/source/shared/idlib/metrics/metrics.cpp new file mode 100644 index 0000000..c62e807 --- /dev/null +++ b/source/shared/idlib/metrics/metrics.cpp @@ -0,0 +1,83 @@ +#include "metrics.h" + +#include "metricsframework.h" + +#include +#include +#include + +namespace { + +FILE* MetricStream(idMetricFile* metricFile) { + return metricFile == nullptr ? nullptr + : static_cast(metricFile->fileHandle); +} + +void WriteU32(FILE* stream, const std::uint32_t value) { + std::fwrite(&value, 1, sizeof(value), stream); +} + +void WriteFloat(FILE* stream, const float value) { + std::fwrite(&value, 1, sizeof(value), stream); +} + +void WriteString(FILE* stream, const char* text) { + const std::uint32_t length = static_cast(std::strlen(text)); + WriteU32(stream, length); + std::fwrite(text, 1, length, stream); +} + +} // namespace + +idGaugeMetric::idGaugeMetric(const idStr& shortName, const idStr&) + : idMetricRecord(shortName), count(0), min(0.0f), max(0.0f), mean(0.0f), + mean2(0.0f), m2(0.0f), handle(-1), lastPushFrame(0), gameState(-1) { +} + +bool idGaugeMetric::CheckPushState() { + const int pushFrame = metricsFrameworkLocal.GetPushFrame(); + if (pushFrame <= lastPushFrame || count == 0) return false; + Serialize(); + lastPushFrame = pushFrame; + count = 0; + min = max = mean = mean2 = m2 = 0.0f; + return true; +} + +void idGaugeMetric::Log(const float value, const bool skipPush) { + if (count == 0) min = max = value; + else { + if (value < min) min = value; + if (value > max) max = value; + } + const float delta = value - mean; + ++count; + mean += delta / static_cast(count); + m2 += delta * (value - mean); + mean2 += value * value; + if (!skipPush) CheckPushState(); +} + +void idGaugeMetric::WriteHeader(idMetricFile* metricFile) { + FILE* const stream = MetricStream(metricFile); + if (stream == nullptr) return; + idMetricRecord::WriteHeader(metricFile); + WriteU32(stream, 5); + const char* const names[] = { "count", "min", "max", "mean", "stdDev" }; + for (int index = 0; index < 5; ++index) { + WriteString(stream, names[index]); + WriteU32(stream, index == 0 ? 0u : 1u); + } +} + +void idGaugeMetric::SerializeEntry(idMetricFile* metricFile) { + FILE* const stream = MetricStream(metricFile); + if (stream == nullptr) return; + idMetricRecord::SerializeEntry(metricFile); + WriteU32(stream, static_cast(count)); + WriteFloat(stream, min); + WriteFloat(stream, max); + WriteFloat(stream, mean); + WriteFloat(stream, count > 0 ? std::sqrt(mean2 / count) : 0.0f); +} + diff --git a/source/shared/idlib/metrics/metrics.h b/source/shared/idlib/metrics/metrics.h new file mode 100644 index 0000000..823c23e --- /dev/null +++ b/source/shared/idlib/metrics/metrics.h @@ -0,0 +1,35 @@ +#pragma once + +#include "metricrecord.h" + +class idGaugeMetric : public idMetricRecord { +public: + idGaugeMetric(const idStr& shortName, const idStr& description = idStr()); + + bool CheckPushState(); + void Log(float value, bool skipPush = false); + void WriteHeader(idMetricFile* metricFile) override; + void SerializeEntry(idMetricFile* metricFile) override; + + int GetCount() const { return count; } + float GetMin() const { return min; } + float GetMax() const { return max; } + float GetMean() const { return mean; } + +private: + int count; + float min; + float max; + float mean; + float mean2; + float m2; + int handle; + int lastPushFrame; + int gameState; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idGaugeMetric) == 104, + "Recovered idGaugeMetric ABI changed"); +#endif + diff --git a/source/shared/idlib/metrics/metricsframework.cpp b/source/shared/idlib/metrics/metricsframework.cpp new file mode 100644 index 0000000..8443727 --- /dev/null +++ b/source/shared/idlib/metrics/metricsframework.cpp @@ -0,0 +1,201 @@ +#include "metricsframework.h" + +#include "idlib/sys/sys_time.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include + +namespace { + +struct metricSlot_t { + idMetricsFramework* owner; + char name[128]; + char path[MAX_PATH]; + idMetricFile file; +}; + +struct frameworkConfig_t { + idMetricsFramework* owner; + char directory[MAX_PATH]; + int heartbeatMilliseconds; +}; + +metricSlot_t metricSlots[64] = {}; +frameworkConfig_t frameworkConfigs[8] = {}; + +frameworkConfig_t& ConfigFor(idMetricsFramework* owner) { + for (frameworkConfig_t& config : frameworkConfigs) { + if (config.owner == owner) return config; + } + for (frameworkConfig_t& config : frameworkConfigs) { + if (config.owner == nullptr) { + config.owner = owner; + std::strcpy(config.directory, "metrics"); + config.heartbeatMilliseconds = 250; + return config; + } + } + return frameworkConfigs[0]; +} + +void WriteU32(FILE* stream, const std::uint32_t value) { + std::fwrite(&value, 1, sizeof(value), stream); +} + +void WriteString(FILE* stream, const char* text) { + const std::uint32_t length = static_cast(std::strlen(text)); + WriteU32(stream, length); + std::fwrite(text, 1, length, stream); +} + +void SanitizeName(const char* source, char* destination, const int capacity) { + int out = 0; + for (const char* cursor = source; *cursor != '\0' && out + 1 < capacity; + ++cursor) { + const unsigned char value = static_cast(*cursor); + destination[out++] = (value >= 'a' && value <= 'z') + || (value >= 'A' && value <= 'Z') + || (value >= '0' && value <= '9') || value == '-' || value == '_' + ? static_cast(value) : '_'; + } + destination[out] = '\0'; +} + +} // namespace + +idMetricsFramework metricsFrameworkLocal; + +idMetricsFramework::MachineInfo::MachineInfo() + : idMetricRecord(idStr("MachineInfo")) { +} + +void idMetricsFramework::MachineInfo::WriteHeader(idMetricFile* metricFile) { + FILE* const stream = metricFile == nullptr ? nullptr + : static_cast(metricFile->fileHandle); + if (stream == nullptr) return; + idMetricRecord::WriteHeader(metricFile); + WriteU32(stream, 1); + WriteString(stream, "BuildInfo"); + WriteU32(stream, 6); +} + +void idMetricsFramework::MachineInfo::SerializeEntry(idMetricFile* metricFile) { + FILE* const stream = metricFile == nullptr ? nullptr + : static_cast(metricFile->fileHandle); + if (stream == nullptr) return; + idMetricRecord::SerializeEntry(metricFile); + WriteString(stream, "tech5-recovery-pc"); +} + +idMetricsFramework::idMetricsFramework() + : currentPushTime(0), currentWriteTime(0), currentPushFrame(0), + lastCheckedPushFrame(0), currentState(0), baseHandle(0), + isRecording(false), fileTable{nullptr, 256, 0, 255}, stateStream() { + ConfigFor(this); +} + +idMetricsFramework::~idMetricsFramework() { + MetricsStop(); + for (frameworkConfig_t& config : frameworkConfigs) { + if (config.owner == this) { + std::memset(&config, 0, sizeof(config)); + break; + } + } +} + +std::uint64_t idMetricsFramework::GetCurrentSystemTime() const { + constexpr std::uint64_t WINDOWS_TO_UNIX_100NS = 116444736000000000ULL; + const std::uint64_t systemTime = Sys_CurrentSystemTime(); + return systemTime >= WINDOWS_TO_UNIX_100NS + ? (systemTime - WINDOWS_TO_UNIX_100NS) / 10000ULL : systemTime / 10000ULL; +} + +int idMetricsFramework::GetPushFrame() { + const std::uint64_t now = GetCurrentSystemTime(); + const int heartbeat = ConfigFor(this).heartbeatMilliseconds; + ++lastCheckedPushFrame; + if (currentPushTime == 0 || now - currentPushTime >= + static_cast(std::max(1, heartbeat))) { + currentPushTime = now; + currentPushFrame = lastCheckedPushFrame; + } + return currentPushFrame; +} + +void idMetricsFramework::SetOutputDirectory(const char* directory) { + frameworkConfig_t& config = ConfigFor(this); + std::strncpy(config.directory, + directory == nullptr || directory[0] == '\0' ? "metrics" : directory, + sizeof(config.directory) - 1); + config.directory[sizeof(config.directory) - 1] = '\0'; +} + +void idMetricsFramework::SetHeartbeatMilliseconds(const int milliseconds) { + ConfigFor(this).heartbeatMilliseconds = std::max(1, milliseconds); +} + +void idMetricsFramework::MetricsRecord() { + isRecording = true; + MachineInfo machineInfo; + machineInfo.Serialize(); +} + +void idMetricsFramework::MetricsStop() { + if (!isRecording && fileTable.numEntries == 0) return; + for (metricSlot_t& slot : metricSlots) { + if (slot.owner == this) { + if (slot.file.fileHandle != nullptr) { + std::fclose(static_cast(slot.file.fileHandle)); + } + slot = metricSlot_t{}; + } + } + fileTable.numEntries = 0; + isRecording = false; + ++baseHandle; +} + +void idMetricsFramework::OpenMetricFile(const idStr& filename, + idMetricFile& metricFile) { + frameworkConfig_t& config = ConfigFor(this); + CreateDirectoryA(config.directory, nullptr); + char safeName[128] = {}; + SanitizeName(filename.c_str(), safeName, sizeof(safeName)); + char path[MAX_PATH] = {}; + std::snprintf(path, sizeof(path), "%s\\%s.metric", config.directory, + safeName[0] == '\0' ? "unnamed" : safeName); + FILE* stream = nullptr; + if (fopen_s(&stream, path, "wb") == 0) metricFile.fileHandle = stream; +} + +idMetricFile* idMetricsFramework::GetFileHandle(const idStr& filename) { + for (metricSlot_t& slot : metricSlots) { + if (slot.owner == this && std::strcmp(slot.name, filename.c_str()) == 0) { + return &slot.file; + } + } + for (metricSlot_t& slot : metricSlots) { + if (slot.owner == nullptr) { + slot.owner = this; + std::strncpy(slot.name, filename.c_str(), sizeof(slot.name) - 1); + OpenMetricFile(filename, slot.file); + if (slot.file.fileHandle == nullptr) { + slot = metricSlot_t{}; + return nullptr; + } + ++fileTable.numEntries; + return &slot.file; + } + } + return nullptr; +} diff --git a/source/shared/idlib/metrics/metricsframework.h b/source/shared/idlib/metrics/metricsframework.h new file mode 100644 index 0000000..40311d0 --- /dev/null +++ b/source/shared/idlib/metrics/metricsframework.h @@ -0,0 +1,54 @@ +#pragma once + +#include "metricrecord.h" + +class idMetricsFramework { +public: + class MachineInfo : public idMetricRecord { + public: + MachineInfo(); + void WriteHeader(idMetricFile* metricFile) override; + void SerializeEntry(idMetricFile* metricFile) override; + }; + + idMetricsFramework(); + ~idMetricsFramework(); + + std::uint64_t GetCurrentSystemTime() const; + int GetPushFrame(); + void MetricsRecord(); + void MetricsStop(); + idMetricFile* GetFileHandle(const idStr& filename); + bool IsRecording() const { return isRecording; } + void SetOutputDirectory(const char* directory); + void SetHeartbeatMilliseconds(int milliseconds); + +private: + void OpenMetricFile(const idStr& filename, idMetricFile& metricFile); + + std::uint64_t currentPushTime; + std::uint64_t currentWriteTime; + int currentPushFrame; + int lastCheckedPushFrame; + int currentState; + int baseHandle; + bool isRecording; + + struct recoveredHashTable_t { + void** heads; + int tableSize; + int numEntries; + int tableSizeMask; + } fileTable; + idMetricFile stateStream; +}; + +extern idMetricsFramework metricsFrameworkLocal; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idMetricsFramework::MachineInfo) == 68, + "Recovered MachineInfo ABI changed"); +static_assert(sizeof(idMetricsFramework) == 72, + "Recovered idMetricsFramework ABI changed"); +#endif + diff --git a/source/shared/idlib/metrics/plog.cpp b/source/shared/idlib/metrics/plog.cpp new file mode 100644 index 0000000..2d06871 --- /dev/null +++ b/source/shared/idlib/metrics/plog.cpp @@ -0,0 +1,200 @@ +#include "plog.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include + +namespace { + +std::int64_t ClockTicks() { + LARGE_INTEGER counter = {}; + QueryPerformanceCounter(&counter); + return counter.QuadPart; +} + +double TicksPerMillisecond() { + static double value = 0.0; + if (value == 0.0) { + LARGE_INTEGER frequency = {}; + QueryPerformanceFrequency(&frequency); + value = static_cast(frequency.QuadPart) / 1000.0; + } + return value; +} + +float TicksToMilliseconds(const std::int64_t ticks) { + return static_cast(static_cast(ticks) / TicksPerMillisecond()); +} + +} // namespace + +idPLog pLog; + +idPLog::idPLog() + : treeEntries(64), logEntries(64), lastEntry(0), overHeadTicks(-1), + groupMask(0) { + Clear(); +} + +void idPLog::Clear() { + treeEntries.Clear(); + logEntries.Clear(); + logEntry_t* const root = logEntries.Alloc(); + if (root != nullptr) { + root->label = "root"; + root->parent = 0; + root->totalTicks = 0; + } + lastEntry = 0; +} + +void idPLog::EnsureEntries() { + logEntries.Reserve(4096); + treeEntries.Reserve(4096); +} + +std::int64_t idPLog::GetOverHeadTicks() { + if (overHeadTicks >= 0) return overHeadTicks; + std::int64_t best = INT64_MAX; + for (int pass = 0; pass < 4; ++pass) { + const std::int64_t start = ClockTicks(); + for (int index = 0; index < 1000; ++index) { + const std::int64_t end = ClockTicks(); + (void)end; + } + best = std::min(best, ClockTicks() - start); + } + overHeadTicks = static_cast(best / 1000); + return overHeadTicks; +} + +void idPLog::SubtractOverhead() { + const std::int64_t overhead = GetOverHeadTicks(); + for (int index = 1; index < logEntries.Num(); ++index) { + const int parent = logEntries[index].parent; + if (parent >= 0 && parent < logEntries.Num()) { + logEntries[parent].totalTicks = std::max(0, + logEntries[parent].totalTicks - overhead); + } + } +} + +float idPLog::BuildLogData(const float thresholdMS, + const std::int64_t adjustByTicks) { + treeEntries.Clear(); + treeEntry_t* const root = treeEntries.Alloc(); + if (root == nullptr) return 0.0f; + root->label = "root"; + root->depth = 0; + root->numHits = 1; + root->parent = -1; + root->firstChild = -1; + root->nextChild = -1; + + for (int index = 1; index < logEntries.Num(); ++index) { + const logEntry_t& source = logEntries[index]; + int treeParent = 0; + if (source.parent > 0 && source.parent < index) { + const char* const parentLabel = logEntries[source.parent].label; + for (int candidate = treeEntries.Num() - 1; candidate > 0; --candidate) { + if (treeEntries[candidate].label == parentLabel + || std::strcmp(treeEntries[candidate].label, parentLabel) == 0) { + treeParent = candidate; + break; + } + } + } + + int destination = -1; + for (int candidate = 1; candidate < treeEntries.Num(); ++candidate) { + if (treeEntries[candidate].parent == treeParent + && (treeEntries[candidate].label == source.label + || std::strcmp(treeEntries[candidate].label, source.label) == 0)) { + destination = candidate; + break; + } + } + if (destination < 0) { + treeEntry_t* const entry = treeEntries.Alloc(); + if (entry == nullptr) break; + destination = treeEntries.Num() - 1; + entry->label = source.label; + entry->depth = treeEntries[treeParent].depth + 1; + entry->parent = treeParent; + entry->firstChild = -1; + entry->nextChild = treeEntries[treeParent].firstChild; + treeEntries[treeParent].firstChild = destination; + } + treeEntry_t& entry = treeEntries[destination]; + const std::int64_t adjusted = std::max(0, + source.totalTicks - adjustByTicks); + entry.totalTicks += adjusted; + ++entry.numHits; + root->totalTicks += adjusted; + } + + if (thresholdMS > 0.0f) { + // Keep the recovered table intact; the threshold only controls output. + } + return TicksToMilliseconds(root->totalTicks); +} + +void idPLog::ShowUniqueEntries(const float thresholdMS, + const std::int64_t adjustByTicks) { + BuildLogData(thresholdMS, adjustByTicks); + for (int index = 1; index < treeEntries.Num(); ++index) { + const treeEntry_t& entry = treeEntries[index]; + const float milliseconds = TicksToMilliseconds(entry.totalTicks); + if (milliseconds >= thresholdMS) { + std::printf("%-40s %6d %10.3f ms\n", entry.label, + entry.numHits, milliseconds); + } + } +} + +void idPLog::ShowCallGraph(const float thresholdMS, + const std::int64_t adjustByTicks) { + BuildLogData(thresholdMS, adjustByTicks); + for (int index = 1; index < treeEntries.Num(); ++index) { + const treeEntry_t& entry = treeEntries[index]; + const float milliseconds = TicksToMilliseconds(entry.totalTicks); + if (milliseconds >= thresholdMS) { + std::printf("%*s%s: %.3f ms (%d)\n", entry.depth * 2, "", + entry.label, milliseconds, entry.numHits); + } + } +} + +idPLogScope::idPLogScope(idPLog& log, const std::uint64_t mask, + const char* label) + : logIndex(-1), pLog(&log) { + if (!log.IsGroupEnabled(mask)) return; + idPLog::logEntry_t* const entry = log.logEntries.Alloc(); + if (entry == nullptr) return; + entry->label = label == nullptr ? "" : label; + entry->parent = log.lastEntry; + entry->totalTicks = ClockTicks(); + logIndex = log.logEntries.Num() - 1; + log.lastEntry = logIndex; +} + +idPLogScope::~idPLogScope() { + End(); +} + +void idPLogScope::End(std::int64_t* totalTicks) { + if (logIndex < 0 || pLog == nullptr) return; + idPLog::logEntry_t& entry = pLog->logEntries[logIndex]; + entry.totalTicks = ClockTicks() - entry.totalTicks; + if (totalTicks != nullptr) *totalTicks = entry.totalTicks; + pLog->lastEntry = entry.parent; + logIndex = -1; +} diff --git a/source/shared/idlib/metrics/plog.h b/source/shared/idlib/metrics/plog.h new file mode 100644 index 0000000..39bbab0 --- /dev/null +++ b/source/shared/idlib/metrics/plog.h @@ -0,0 +1,81 @@ +#pragma once + +#include "idlib/containers/recoveredlist.h" + +#include + +#pragma pack(push, 4) +class idPLog { +public: + struct treeEntry_t { + const char* label; + int depth; + int numHits; + std::int64_t totalTicks; + int parent; + int firstChild; + int nextChild; + bool allocationOccured; + }; + + struct logEntry_t { + const char* label; + int parent; + std::int64_t totalTicks; + }; + + idPLog(); + + void Clear(); + void EnsureEntries(); + void SetGroupMask(std::uint64_t mask) { groupMask = mask; } + void EnableGroups(std::uint64_t mask) { groupMask |= mask; } + void DisableGroups(std::uint64_t mask) { groupMask &= ~mask; } + std::uint64_t GetGroupMask() const { return groupMask; } + bool IsGroupEnabled(std::uint64_t mask) const { return (groupMask & mask) != 0; } + + std::int64_t GetOverHeadTicks(); + void SubtractOverhead(); + float BuildLogData(float thresholdMS = 0.0f, + std::int64_t adjustByTicks = 0); + void ShowUniqueEntries(float thresholdMS = 0.0f, + std::int64_t adjustByTicks = 0); + void ShowCallGraph(float thresholdMS = 0.0f, + std::int64_t adjustByTicks = 0); + + int NumLogEntries() const { return logEntries.Num(); } + int NumTreeEntries() const { return treeEntries.Num(); } + const logEntry_t& GetLogEntry(int index) const { return logEntries[index]; } + +private: + friend class idPLogScope; + + idRecoveredList treeEntries; + idRecoveredList logEntries; + int lastEntry; + int overHeadTicks; + std::uint64_t groupMask; +}; + +class idPLogScope { +public: + idPLogScope(idPLog& log, std::uint64_t groupMask, const char* label); + ~idPLogScope(); + void End(std::int64_t* totalTicks = nullptr); + +private: + int logIndex; + idPLog* pLog; +}; +#pragma pack(pop) + +extern idPLog pLog; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idPLog::treeEntry_t) == 36, + "Recovered idPLog tree entry ABI changed"); +static_assert(sizeof(idPLog::logEntry_t) == 16, + "Recovered idPLog log entry ABI changed"); +static_assert(sizeof(idPLog) == 48, "Recovered idPLog ABI changed"); +static_assert(sizeof(idPLogScope) == 8, "Recovered idPLogScope ABI changed"); +#endif diff --git a/source/shared/idlib/networking/amqp/mqclient.cpp b/source/shared/idlib/networking/amqp/mqclient.cpp new file mode 100644 index 0000000..1bdd0b8 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqclient.cpp @@ -0,0 +1,98 @@ +#include "mqmessaging.h" + +#ifdef nullptr +#undef nullptr +#endif +#ifdef snprintf +#undef snprintf +#endif + +#include +#include +#include +#include + +idMQGraphiteClient::idMQGraphiteClient(const idStr& exchangeName, + const idStr& metricPrefix) + : idMQClientThread(), reportTime(0), channel(nullptr), outgoingEvents(), + exchange(exchangeName), prefix(metricPrefix), threadMutex() { +} + +idMQGraphiteClient::~idMQGraphiteClient() { + StopThread(true); + for (int index = 0; index < outgoingEvents.Num(); ++index) { + std::free(const_cast(outgoingEvents[index].eventName)); + } +} + +void idMQGraphiteClient::StartMessageSystem() { + StartThread("AMQP Graphite Client"); +} + +void idMQGraphiteClient::PreRun() { + channel = connection.GetChannel(); + if (channel != nullptr) { + channel->ExchangeDeclare(exchange, idStr("topic"), true, false); + } + reportTime = static_cast(GetTickCount()) + 10000; +} + +void idMQGraphiteClient::OnThreadTerminate() { channel = nullptr; } + +void idMQGraphiteClient::LogEvent(const idStr& eventName, + const float duration) { + idScopedCriticalSection lock(threadMutex); + for (int index = 0; index < outgoingEvents.Num(); ++index) { + graphiteEvent_t& event = outgoingEvents[index]; + if (_stricmp(event.eventName, eventName.c_str()) == 0) { + ++event.count; + event.duration += duration; + event.mean += (duration - event.mean) / event.count; + event.mean2 += duration * duration; + return; + } + } + graphiteEvent_t event = {}; + event.eventName = _strdup(eventName.c_str()); + event.count = 1; + event.duration = duration; + event.mean = duration; + event.mean2 = duration * duration; + outgoingEvents.Append(event); +} + +void idMQGraphiteClient::ThreadSlice() { + if (static_cast(GetTickCount()) < reportTime) { + Sleep(10); + return; + } + idMQList events; + { + idScopedCriticalSection lock(threadMutex); + events = outgoingEvents; + outgoingEvents.Clear(); + } + const long long timestamp = static_cast(std::time(nullptr)); + static const char* const names[] = { "rate", "avg", "std" }; + for (int index = 0; index < events.Num(); ++index) { + const graphiteEvent_t& event = events[index]; + const float values[] = { + event.duration, + event.mean, + event.count == 0 ? 0.0f + : std::sqrt(event.mean2 / static_cast(event.count)) + }; + for (int metric = 0; metric < 3; ++metric) { + char line[2048]; + const int amount = std::snprintf(line, sizeof(line), + "%s.%s.%s %g %lld", prefix.c_str(), event.eventName, + names[metric], values[metric], timestamp); + if (channel != nullptr && amount > 0) { + channel->BasicPublish(exchange, prefix, false, false, line, + static_cast(amount)); + } + } + std::free(const_cast(event.eventName)); + } + reportTime = static_cast(GetTickCount()) + 10000; +} diff --git a/source/shared/idlib/networking/amqp/mqcommander.cpp b/source/shared/idlib/networking/amqp/mqcommander.cpp new file mode 100644 index 0000000..9ae3896 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqcommander.cpp @@ -0,0 +1,58 @@ +#include "mqmessaging.h" + +idMQCommand::idMQCommand() + : method(nullptr), contentHeader(nullptr), complete(false), contentBody(16), + remainingBodyBytes(0) { +} + +idMQCommand::~idMQCommand() = default; + +void idMQCommand::Reset() { + delete method; + delete contentHeader; + method = nullptr; + contentHeader = nullptr; + complete = false; + contentBody.Clear(); + remainingBodyBytes = 0; +} + +idMQCommandBuilder::idMQCommandBuilder() + : state(MQ_CMDSTATE_NEED_METHOD), remainingBytes(0) { +} + +MQErrors_t idMQCommandBuilder::HandleFrame(idMQCommand& command, + idMQFrame& frame) { + if (state == MQ_CMDSTATE_NEED_METHOD) { + if (frame.type != AMQP_FRAME_METHOD) return MQERROR_INVALID_FRAME; + command.method = idMQChannel::DecodeMethod(frame); + if (command.method == nullptr) return MQERROR_UNEXPECTED_METHOD; + state = command.method->HasContent() + ? MQ_CMDSTATE_NEED_HEADER : MQ_CMDSTATE_COMPLETE; + } else if (state == MQ_CMDSTATE_NEED_HEADER) { + if (frame.type != AMQP_FRAME_HEADER) return MQERROR_INVALID_FRAME; + command.contentHeader = idMQChannel::DecodeContentHeader(frame); + if (command.contentHeader == nullptr) return MQERROR_UNEXPECTED_CLASS; + command.remainingBodyBytes = command.contentHeader->bodyLength; + state = command.remainingBodyBytes == 0 + ? MQ_CMDSTATE_COMPLETE : MQ_CMDSTATE_NEED_BODY; + } else if (state == MQ_CMDSTATE_NEED_BODY) { + if (frame.type != AMQP_FRAME_BODY) return MQERROR_INVALID_FRAME; + if (static_cast(frame.data.Num()) + > command.remainingBodyBytes) return MQERROR_FRAME_OVERFLOW; + for (int index = 0; index < frame.data.Num(); ++index) { + command.contentBody.Append(frame.data[index]); + } + command.remainingBodyBytes -= frame.data.Num(); + if (command.remainingBodyBytes == 0) state = MQ_CMDSTATE_COMPLETE; + } else { + return MQERROR_UNKNOWN; + } + + command.complete = state == MQ_CMDSTATE_COMPLETE; + if (command.complete) { + state = MQ_CMDSTATE_NEED_METHOD; + remainingBytes = 0; + } + return MQERROR_NONE; +} diff --git a/source/shared/idlib/networking/amqp/mqcommon.cpp b/source/shared/idlib/networking/amqp/mqcommon.cpp new file mode 100644 index 0000000..ffa7cb8 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqcommon.cpp @@ -0,0 +1,94 @@ +#include "mqcommon.h" + +#ifdef nullptr +#undef nullptr +#endif + +#include +#include + +namespace { + +unsigned short Swap16(const unsigned short value) { + return static_cast((value >> 8) | (value << 8)); +} + +unsigned int Swap32(const unsigned int value) { + return ((value & 0x000000FFu) << 24) + | ((value & 0x0000FF00u) << 8) + | ((value & 0x00FF0000u) >> 8) + | ((value & 0xFF000000u) >> 24); +} + +} // namespace + +amqpEndpoint_t::amqpEndpoint_t() + : host("localhost"), port(5672), username("guest"), password("guest"), + vhost("/"), channelMax(0), frameMax(0), heartbeat(0), + nonBlocking(true), silent(true), minorVersion(0), majorVersion(8) { +} + +idMQTCP::idMQTCP() = default; +idMQTCP::~idMQTCP() { Close(); } + +bool idMQTCP::Connect(const char* host, const unsigned short port, + const bool nonBlocking, const bool silent) { + return tcp.Connect(host, port, nonBlocking, silent, false); +} + +void idMQTCP::Close() { tcp.Close(); } +bool idMQTCP::IsOpen() const { return tcp.IsOpen(); } + +int idMQTCP::Read(void* data, const int size, const bool blocking, + const int timeoutMS) { + return blocking ? tcp.ReadBlocking(data, size, timeoutMS) + : tcp.Read(data, size); +} + +int idMQTCP::Write(const void* data, const int size, const bool blocking, + const int timeoutMS) { + return blocking ? tcp.WriteBlocking(data, size, timeoutMS) + : tcp.Write(data, size); +} + +int idMQTCP::ReadByte(unsigned char& value, const bool blocking, + const int timeoutMS) { + return Read(&value, sizeof(value), blocking, timeoutMS); +} + +int idMQTCP::ReadUInt16(unsigned short& value, const bool blocking, + const int timeoutMS) { + const int amount = Read(&value, sizeof(value), blocking, timeoutMS); + if (amount == sizeof(value)) value = Swap16(value); + return amount; +} + +int idMQTCP::ReadUInt32(unsigned int& value, const bool blocking, + const int timeoutMS) { + const int amount = Read(&value, sizeof(value), blocking, timeoutMS); + if (amount == sizeof(value)) value = Swap32(value); + return amount; +} + +idMQBuffer::idMQBuffer() : body(), readPos(0) {} + +void idMQBuffer::Clear() { + body.Clear(); + readPos = 0; +} + +void idMQBuffer::WriteData(const void* source, const int size) { + if (source == nullptr || size <= 0) return; + const unsigned char* bytes = static_cast(source); + for (int index = 0; index < size; ++index) body.Append(bytes[index]); +} + +void idMQBuffer::WriteUInt16(unsigned short value, const bool bigEndian) { + if (bigEndian) value = Swap16(value); + WriteData(&value, sizeof(value)); +} + +void idMQBuffer::WriteUInt32(unsigned int value, const bool bigEndian) { + if (bigEndian) value = Swap32(value); + WriteData(&value, sizeof(value)); +} diff --git a/source/shared/idlib/networking/amqp/mqcommon.h b/source/shared/idlib/networking/amqp/mqcommon.h new file mode 100644 index 0000000..cb75a3f --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqcommon.h @@ -0,0 +1,494 @@ +#pragma once + +// Recovered AMQP 0-8 surface used by tungsten's telemetry clients. The wire +// protocol remains AMQP compatible; only the old Xbox socket/thread plumbing +// is replaced by the portable idTCP and Win32 primitives already in idLib. +#include "idlib/precompiled.h" +#include "idlib/sys/sys_networking.h" + +#ifdef nullptr +#undef nullptr +#endif +#ifdef _stricmp +#undef _stricmp +#endif + +#include +#include +#include + +template +class idMQList { +public: + explicit idMQList(int initialGranularity = 16) + : list(nullptr), num(0), size(0), + granularity(static_cast(initialGranularity)), memTag(5), + listStatic(0) {} + idMQList(const idMQList& other) : idMQList(other.granularity) { + *this = other; + } + ~idMQList() { delete[] list; } + idMQList& operator=(const idMQList& other) { + if (this == &other) return *this; + SetNum(other.num); + for (int index = 0; index < num; ++index) list[index] = other[index]; + return *this; + } + void Clear() { num = 0; } + int Num() const { return num; } + T* Ptr() { return list; } + const T* Ptr() const { return list; } + T& operator[](int index) { return list[index]; } + const T& operator[](int index) const { return list[index]; } + void SetNum(int newNum) { + if (newNum > size) Resize(newNum); + if (newNum >= 0 && newNum <= size) num = newNum; + } + int Append(const T& value) { + if (num == size && !Resize(num + 1)) return -1; + list[num] = value; + return num++; + } + bool RemoveIndex(int index) { + if (index < 0 || index >= num) return false; + for (int i = index; i + 1 < num; ++i) list[i] = list[i + 1]; + --num; + return true; + } +private: + bool Resize(int required) { + const int step = granularity > 0 ? granularity : 16; + const int newSize = ((required + step - 1) / step) * step; + T* replacement = new (std::nothrow) T[newSize]; + if (replacement == nullptr) return false; + for (int index = 0; index < num; ++index) replacement[index] = list[index]; + delete[] list; + list = replacement; + size = newSize; + return true; + } + T* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; +}; + +class idMQKeyValue { +public: + idMQKeyValue() : key(), value() {} + idMQKeyValue(const char* keyText, const char* valueText) + : key(keyText), value(valueText) {} + const idStr& GetKey() const { return key; } + const idStr& GetValue() const { return value; } + idStr key; + idStr value; +}; + +class idMQTable { +public: + idMQTable() : args(), hash{} {} + void Clear() { args.Clear(); } + int GetNumKeyVals() const { return args.Num(); } + const idMQKeyValue* GetKeyVal(int index) const { + return index >= 0 && index < args.Num() ? &args[index] : nullptr; + } + void Set(const char* key, const char* value) { + const char* safeKey = key == nullptr ? "" : key; + for (int index = 0; index < args.Num(); ++index) { + if (_stricmp(args[index].key.c_str(), safeKey) == 0) { + args[index].value = value == nullptr ? "" : value; + return; + } + } + args.Append(idMQKeyValue(safeKey, value == nullptr ? "" : value)); + } + const char* GetString(const char* key, const char* defaultValue = "") const { + const char* safeKey = key == nullptr ? "" : key; + for (int index = 0; index < args.Num(); ++index) { + if (_stricmp(args[index].key.c_str(), safeKey) == 0) { + return args[index].value.c_str(); + } + } + return defaultValue; + } +private: + idMQList args; + std::uint32_t hash[8]; +}; + +enum MQErrors_t { + MQERROR_NONE = 0, + MQERROR_UNKNOWN, + MQERROR_INVALID_FRAME, + MQERROR_FRAME_OVERFLOW, + MQERROR_SESSION_NOT_FOUND, + MQERROR_STREAM_ERROR, + MQERROR_CONNECTION_START, + MQERROR_BAD_SPEC_VERSION, + MQERROR_AUTH_FAILED, + MQERROR_UNEXPECTED_CLASS, + MQERROR_UNEXPECTED_METHOD, + MQERROR_SERVER_CLOSED +}; + +enum MQAssemblerState_t { + MQ_CMDSTATE_NEED_METHOD = 0, + MQ_CMDSTATE_NEED_HEADER, + MQ_CMDSTATE_NEED_BODY, + MQ_CMDSTATE_COMPLETE +}; + +enum amqpClassId_t { + AMQP_CLASS_CONNECTION = 10, + AMQP_CLASS_CHANNEL = 20, + AMQP_CLASS_ACCESS = 30, + AMQP_CLASS_EXCHANGE = 40, + AMQP_CLASS_QUEUE = 50, + AMQP_CLASS_BASIC = 60 +}; + +enum amqpFrameType_t { + AMQP_FRAME_METHOD = 1, + AMQP_FRAME_HEADER = 2, + AMQP_FRAME_BODY = 3, + AMQP_FRAME_HEARTBEAT = 8, + AMQP_FRAME_END = 0xCE +}; + +struct amqpEndpoint_t { + amqpEndpoint_t(); + + idStr host; + unsigned short port; + idStr username; + idStr password; + idStr vhost; + unsigned short channelMax; + unsigned short frameMax; + unsigned short heartbeat; + bool nonBlocking; + bool silent; + int minorVersion; + int majorVersion; +}; + +struct amqpShutdownReason_t { + amqpShutdownReason_t() + : replyCode(0), cId(0), mId(0), error(MQERROR_NONE), replyText("") {} + unsigned short replyCode; + unsigned short cId; + unsigned short mId; + MQErrors_t error; + const char* replyText; +}; + +class idMQFrame; + +class idMQMethod { +public: + virtual ~idMQMethod() = default; + virtual int GetClassId() const = 0; + virtual int GetMethodId() const = 0; + virtual bool HasContent() const { return false; } + virtual bool IsAsync() const { return false; } + virtual void Populate(idMQFrame& frame) = 0; + virtual void ToFrame(idMQFrame& frame) = 0; +}; + +class idMQFrame { +public: + idMQFrame(); + idMQFrame(unsigned char frameType, unsigned short channelNumber); + + void Clear(); + void ReadData(void* destination, int length); + void WriteData(const void* source, int length); + unsigned char ReadByte(); + unsigned short ReadUInt16(); + unsigned int ReadUInt32(); + std::uint64_t ReadUInt64(); + bool ReadBool(); + idStr ReadShortString(); + void ReadString(idMQList& value); + void ReadTable(idMQTable& value); + bool ReadPropertyPresence(); + void FinalizeReadPresence(); + + void WriteByte(unsigned char value); + void WriteUInt16(unsigned short value); + void WriteUInt32(unsigned int value); + void WriteUInt64(std::uint64_t value); + void WriteBool(bool value); + void WriteShortString(const idStr& value); + void WriteString(const idMQList& value); + void WriteTable(const idMQTable& value); + void WritePropertyPresence(bool present); + void FinalizeWritePresence(); + void FlushWriteBitBuffer(); + void FinalizeFrame(); + + int ReadFromStream(class idMQTCP& stream); + int WriteToStream(class idMQTCP& stream); + + unsigned char type; + unsigned short channel; + idMQList data; + int readPos; + bool locked; + bool resetReadBitBuffer; + unsigned char readBitBuffer; + unsigned int readBitMask; + bool flushWriteBitBuffer; + unsigned char writeBitBuffer; + unsigned int writeBitMask; + unsigned short readPresenceBuffer; + unsigned short readPresencePosition; + unsigned short writePresenceBuffer; + unsigned short writePresencePosition; +}; + +class idMQTCP { +public: + idMQTCP(); + ~idMQTCP(); + bool Connect(const char* host, unsigned short port, bool nonBlocking, + bool silent); + void Close(); + bool IsOpen() const; + int Read(void* data, int size, bool blocking = true, + int timeoutMS = 5000); + int Write(const void* data, int size, bool blocking = true, + int timeoutMS = 5000); + int ReadByte(unsigned char& value, bool blocking = true, + int timeoutMS = 5000); + int ReadUInt16(unsigned short& value, bool blocking = true, + int timeoutMS = 5000); + int ReadUInt32(unsigned int& value, bool blocking = true, + int timeoutMS = 5000); + + idTCP tcp; +}; + +class idMQContentHeader { +public: + idMQContentHeader() : bodyLength(0) {} + virtual ~idMQContentHeader() = default; + virtual int GetProtocolId() const = 0; + virtual void Populate(idMQFrame& frame) = 0; + virtual void ToFrame(idMQFrame& frame) = 0; + + std::uint64_t bodyLength; +}; + +class idMQBuffer { +public: + idMQBuffer(); + void Clear(); + void WriteData(const void* data, int size); + void WriteUInt16(unsigned short value, bool bigEndian = true); + void WriteUInt32(unsigned int value, bool bigEndian = true); + + idMQList body; + int readPos; +}; + +class idMQFrameHandler { +public: + explicit idMQFrameHandler(const amqpEndpoint_t& endpointValue); + ~idMQFrameHandler(); + bool Connect(); + void Close(); + int SendFrame(idMQFrame& frame); + int ReadFrame(idMQFrame& frame); + bool SendHeader(); + + amqpEndpoint_t endpoint; + idSysMutex readLock; + idSysMutex writeLock; + idMQTCP tcp; +}; + +template +class idDeferredResult { +public: + idDeferredResult() : value(), filled(false), signal(false) {} + + void SetValue(const T& newValue) { + value = newValue; + filled = true; + signal.Raise(); + } + bool GetValue(T& result) { + if (!signal.Wait(5000) || !filled) return false; + result = value; + filled = false; + return true; + } + bool GetValue() { + if (!signal.Wait(5000) || !filled) return false; + filled = false; + return true; + } + void Clear() { + filled = false; + signal.Clear(); + } + + T value; + bool filled; + idSysSignal signal; +}; + +class idMQCommand { +public: + idMQCommand(); + ~idMQCommand(); + void Reset(); + + idMQMethod* method; + idMQContentHeader* contentHeader; + bool complete; + idMQList contentBody; + std::uint64_t remainingBodyBytes; +}; + +class idMQCommandBuilder { +public: + idMQCommandBuilder(); + MQErrors_t HandleFrame(idMQCommand& command, idMQFrame& frame); + + MQAssemblerState_t state; + std::uint64_t remainingBytes; +}; + +class idMQChannel; +class idMQSessionManager; + +class idMQConnection { +public: + idMQConnection(); + ~idMQConnection(); + void Init(amqpEndpoint_t& endpoint); + bool Connect(); + void Close(); + void Close(const char* reason, MQErrors_t error); + void Close(amqpShutdownReason_t& reason); + void SendFrame(idMQFrame& frame); + idMQChannel* GetChannel(); + + unsigned int frameMaxSize; + idMQList knownHosts; + idMQFrameHandler* framer; + idMQSessionManager* sessionManager; + class idMQSession* sessionZero; + volatile bool terminateConnection; + volatile bool connectionTerminated; + volatile bool isClosing; + unsigned int threadHandle; + amqpShutdownReason_t shutdownReason; + +private: + bool OpenCommunications(); + void CreateThread(); + void MessageThread(); + static unsigned int StaticThread(void* connection); +}; + +class idMQSession { +public: + idMQSession(int channelNumber, idMQConnection* connection); + ~idMQSession(); + void Init(bool connectionChannel = false); + void Close(); + void Close(const char* reason, MQErrors_t error, + bool notifyServer = true); + void HandleFrame(idMQFrame& frame); + void SendCommand(idMQCommand& command); + + idMQConnection* connection; + idMQCommandBuilder cmdBuilder; + idMQChannel* protocol; + idMQCommand currentCommand; + int channelNumber; + bool isOpen; +}; + +class idMQSessionManager { +public: + explicit idMQSessionManager(idMQConnection* connection); + ~idMQSessionManager(); + void Init(int maximumSessions); + void Shutdown(); + idMQSession* CreateSession(); + idMQSession* Lookup(int channelNumber); + + bool initialized; + idMQConnection* connection; + int maxSessions; + idMQList sessionList; +}; + +class idMQClientThread { +public: + idMQClientThread(); + virtual ~idMQClientThread(); + virtual void StartMessageSystem() { StartThread("AMQP Client"); } + virtual void StopMessageSystem() { StopThread(true); } + virtual void PreRun() {} + virtual void ThreadSlice() { Sys_Yield(); } + virtual void OnThreadTerminate() {} + + void StartThread(const char* threadName); + void StopThread(bool waitForStop); + + unsigned int handle; + volatile bool signalQuit; + volatile bool terminated; + idMQConnection connection; + int retryTime; + +protected: + static void Connect(idMQClientThread* thread); + static unsigned int Thread(void* thread); +}; + +class idMQClient : public idMQClientThread {}; + +class idMQGraphiteClient : public idMQClientThread { +public: + struct graphiteEvent_t { + const char* eventName; + int count; + float duration; + float mean; + float mean2; + }; + + idMQGraphiteClient(const idStr& exchangeName, const idStr& metricPrefix); + ~idMQGraphiteClient() override; + void StartMessageSystem() override; + void PreRun() override; + void ThreadSlice() override; + void OnThreadTerminate() override; + void LogEvent(const idStr& eventName, float duration); + + int reportTime; + idMQChannel* channel; + idMQList outgoingEvents; + idStr exchange; + idStr prefix; + idSysMutex threadMutex; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idMQTCP) == 20, "Recovered idMQTCP ABI changed"); +static_assert(sizeof(idMQList) == 16, + "Recovered AMQP list ABI changed"); +static_assert(sizeof(idMQTable) == 48, "Recovered AMQP table ABI changed"); +static_assert(sizeof(idMQBuffer) == 20, "Recovered idMQBuffer ABI changed"); +static_assert(sizeof(idMQMethod) == 4, "Recovered idMQMethod ABI changed"); +static_assert(sizeof(idMQContentHeader) == 16, + "Recovered idMQContentHeader ABI changed"); +#endif diff --git a/source/shared/idlib/networking/amqp/mqconnection.cpp b/source/shared/idlib/networking/amqp/mqconnection.cpp new file mode 100644 index 0000000..d5f41f8 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqconnection.cpp @@ -0,0 +1,181 @@ +#include "mqmessaging.h" + +#ifdef nullptr +#undef nullptr +#endif + +#include + +idMQConnection::idMQConnection() + : frameMaxSize(131072), knownHosts(), framer(nullptr), + sessionManager(nullptr), sessionZero(nullptr), terminateConnection(false), + connectionTerminated(true), isClosing(false), threadHandle(0), + shutdownReason() { +} + +idMQConnection::~idMQConnection() { + Close(); + delete sessionZero; + delete sessionManager; + delete framer; +} + +void idMQConnection::Init(amqpEndpoint_t& endpoint) { + Close(); + delete sessionZero; + delete sessionManager; + delete framer; + framer = new idMQFrameHandler(endpoint); + sessionManager = new idMQSessionManager(this); + sessionZero = new idMQSession(0, this); + sessionZero->Init(true); + frameMaxSize = endpoint.frameMax == 0 ? 131072 : endpoint.frameMax; + shutdownReason = amqpShutdownReason_t(); +} + +void idMQConnection::CreateThread() { + terminateConnection = false; + connectionTerminated = false; + threadHandle = static_cast(Sys_CreateThread( + StaticThread, this, THREAD_LOWEST, "AMQP Connection", CORE_ANY, + 0x20000, false)); +} + +unsigned int idMQConnection::StaticThread(void* value) { + static_cast(value)->MessageThread(); + return 0; +} + +void idMQConnection::MessageThread() { + while (!terminateConnection && framer != nullptr && framer->tcp.IsOpen()) { + idMQFrame frame; + if (framer->ReadFrame(frame) < 0) break; + idMQSession* session = frame.channel == 0 ? sessionZero + : sessionManager == nullptr ? nullptr + : sessionManager->Lookup(frame.channel); + if (session == nullptr) { + shutdownReason.error = MQERROR_SESSION_NOT_FOUND; + shutdownReason.replyText = "AMQP session not found"; + break; + } + session->HandleFrame(frame); + } + connectionTerminated = true; +} + +bool idMQConnection::OpenCommunications() { + if (framer == nullptr || sessionZero == nullptr || sessionZero->protocol == nullptr + || !framer->SendHeader()) return false; + AMQPConnectionStart start; + if (!sessionZero->protocol->deferredConnectionStart.GetValue(start)) { + shutdownReason.error = MQERROR_CONNECTION_START; + shutdownReason.replyText = "Unable to get ConnectionStart details"; + return false; + } + if (start.versionMajor != 8 || start.versionMinor != 0) { + shutdownReason.error = MQERROR_BAD_SPEC_VERSION; + shutdownReason.replyText = "AMQP protocol version mismatch"; + return false; + } + + idMQList response(16); + response.Append(0); + const char* username = framer->endpoint.username.c_str(); + for (int i = 0; username[i] != 0; ++i) response.Append(username[i]); + response.Append(0); + const char* password = framer->endpoint.password.c_str(); + for (int i = 0; password[i] != 0; ++i) response.Append(password[i]); + + AMQPConnectionTune tune; + sessionZero->protocol->ConnectionStartOk(tune, idStr("PLAIN"), response, + idStr("en_US")); + if (tune.frameMax == 0 && tune.channelMax == 0 && tune.heartbeat == 0) { + shutdownReason.error = MQERROR_AUTH_FAILED; + shutdownReason.replyText = "AMQP authentication/tuning failed"; + return false; + } + const unsigned short channelMax = framer->endpoint.channelMax == 0 + ? tune.channelMax : framer->endpoint.channelMax; + const unsigned int frameMax = framer->endpoint.frameMax == 0 + ? tune.frameMax : framer->endpoint.frameMax; + const unsigned short heartbeat = framer->endpoint.heartbeat == 0 + ? tune.heartbeat : framer->endpoint.heartbeat; + frameMaxSize = frameMax == 0 ? 131072 : frameMax; + sessionManager->Init(channelMax); + sessionZero->protocol->ConnectionTuneOk(channelMax, frameMax, heartbeat); + idStr hosts; + sessionZero->protocol->ConnectionOpen(hosts, framer->endpoint.vhost, + idStr(""), false); + if (connectionTerminated) return false; + knownHosts.Clear(); + const char* cursor = hosts.c_str(); + while (*cursor != 0) { + while (*cursor == ' ') ++cursor; + const char* begin = cursor; + while (*cursor != 0 && *cursor != ' ') ++cursor; + if (cursor != begin) { + idStr host; + host.Append(begin, static_cast(cursor - begin)); + knownHosts.Append(host); + } + } + return true; +} + +bool idMQConnection::Connect() { + if (framer == nullptr || !framer->Connect()) return false; + CreateThread(); + if (!OpenCommunications()) { + Close(shutdownReason); + return false; + } + connectionTerminated = false; + return true; +} + +void idMQConnection::SendFrame(idMQFrame& frame) { + if (framer == nullptr || framer->SendFrame(frame) < 0) { + shutdownReason.error = MQERROR_STREAM_ERROR; + shutdownReason.replyText = "AMQP stream write failed"; + connectionTerminated = true; + } +} + +idMQChannel* idMQConnection::GetChannel() { + if (sessionManager == nullptr || connectionTerminated) return nullptr; + idMQSession* session = sessionManager->CreateSession(); + return session == nullptr ? nullptr : session->protocol; +} + +void idMQConnection::Close(const char* reason, const MQErrors_t error) { + amqpShutdownReason_t value; + value.error = error; + value.replyText = reason == nullptr ? "" : reason; + Close(value); +} + +void idMQConnection::Close(amqpShutdownReason_t& reason) { + if (isClosing) return; + isClosing = true; + shutdownReason = reason; + if (!connectionTerminated && sessionZero != nullptr + && sessionZero->protocol != nullptr && reason.error == MQERROR_NONE) { + sessionZero->protocol->ConnectionClose(200, idStr("Goodbye"), 0, 0); + } + terminateConnection = true; + if (framer != nullptr) framer->Close(); + if (threadHandle != 0 + && Sys_GetCurrentThreadID() != static_cast(threadHandle)) { + Sys_WaitForThread(static_cast(threadHandle)); + Sys_DestroyThread(static_cast(threadHandle)); + } + threadHandle = 0; + if (sessionManager != nullptr) sessionManager->Shutdown(); + connectionTerminated = true; + isClosing = false; +} + +void idMQConnection::Close() { + amqpShutdownReason_t reason; + Close(reason); +} diff --git a/source/shared/idlib/networking/amqp/mqconsumer.h b/source/shared/idlib/networking/amqp/mqconsumer.h new file mode 100644 index 0000000..9df3f59 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqconsumer.h @@ -0,0 +1,31 @@ +#pragma once + +#include "mqcommon.h" + +class AMQPBasicProperties; + +class idMQConsumer { +public: + idMQConsumer() : consumerTag(), isConsuming(false), mutex() {} + virtual ~idMQConsumer() = default; + virtual void ProcessBasicConsumeOk(const idStr& tag) { + idScopedCriticalSection lock(mutex); + consumerTag = tag; + isConsuming = true; + } + virtual void ProcessBasicCancel(const idStr&) { + idScopedCriticalSection lock(mutex); + isConsuming = false; + } + virtual void ProcessBasicCancelOk(const idStr& tag) { + ProcessBasicCancel(tag); + } + virtual void ProcessBasicDeliver(const idStr& consumerTag, + std::uint64_t deliveryTag, bool redelivered, const idStr& exchange, + const idStr& routingKey, const AMQPBasicProperties* properties, + const idMQList& body) = 0; + + idStr consumerTag; + bool isConsuming; + idSysMutex mutex; +}; diff --git a/source/shared/idlib/networking/amqp/mqframing.cpp b/source/shared/idlib/networking/amqp/mqframing.cpp new file mode 100644 index 0000000..2140f49 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqframing.cpp @@ -0,0 +1,330 @@ +#include "mqcommon.h" + +#ifdef nullptr +#undef nullptr +#endif +#ifdef snprintf +#undef snprintf +#endif + +#include +#include +#include +#include + +namespace { + +void StoreUInt32BE(unsigned char* destination, const unsigned int value) { + destination[0] = static_cast(value >> 24); + destination[1] = static_cast(value >> 16); + destination[2] = static_cast(value >> 8); + destination[3] = static_cast(value); +} + +} // namespace + +idMQFrame::idMQFrame() + : type(0), channel(0), data(), readPos(0), locked(false), + resetReadBitBuffer(true), readBitBuffer(0), readBitMask(0), + flushWriteBitBuffer(false), writeBitBuffer(0), writeBitMask(1), + readPresenceBuffer(0), readPresencePosition(0), writePresenceBuffer(0), + writePresencePosition(0) { +} + +idMQFrame::idMQFrame(const unsigned char frameType, + const unsigned short channelNumber) + : idMQFrame() { + type = frameType; + channel = channelNumber; +} + +void idMQFrame::Clear() { + type = 0; + channel = 0; + data.Clear(); + readPos = 0; + locked = false; + resetReadBitBuffer = true; + readBitBuffer = 0; + readBitMask = 0; + flushWriteBitBuffer = false; + writeBitBuffer = 0; + writeBitMask = 1; + readPresenceBuffer = readPresencePosition = 0; + writePresenceBuffer = writePresencePosition = 0; +} + +void idMQFrame::ReadData(void* destination, const int length) { + resetReadBitBuffer = true; + if (destination == nullptr || length <= 0) return; + const int available = std::max(0, data.Num() - readPos); + const int amount = std::min(length, available); + if (amount > 0) std::memcpy(destination, data.Ptr() + readPos, amount); + if (amount < length) { + std::memset(static_cast(destination) + amount, 0, + length - amount); + } + readPos += amount; +} + +void idMQFrame::FlushWriteBitBuffer() { + if (!flushWriteBitBuffer) return; + data.Append(writeBitBuffer); + flushWriteBitBuffer = false; + writeBitBuffer = 0; + writeBitMask = 1; +} + +void idMQFrame::WriteData(const void* source, const int length) { + FlushWriteBitBuffer(); + if (source == nullptr || length <= 0) return; + const unsigned char* bytes = static_cast(source); + for (int index = 0; index < length; ++index) data.Append(bytes[index]); +} + +unsigned char idMQFrame::ReadByte() { + unsigned char value = 0; + ReadData(&value, sizeof(value)); + return value; +} + +unsigned short idMQFrame::ReadUInt16() { + unsigned char bytes[2] = {}; + ReadData(bytes, sizeof(bytes)); + return static_cast((bytes[0] << 8) | bytes[1]); +} + +unsigned int idMQFrame::ReadUInt32() { + unsigned char bytes[4] = {}; + ReadData(bytes, sizeof(bytes)); + return (static_cast(bytes[0]) << 24) + | (static_cast(bytes[1]) << 16) + | (static_cast(bytes[2]) << 8) + | static_cast(bytes[3]); +} + +std::uint64_t idMQFrame::ReadUInt64() { + const std::uint64_t high = ReadUInt32(); + const std::uint64_t low = ReadUInt32(); + return (high << 32) | low; +} + +bool idMQFrame::ReadBool() { + if (resetReadBitBuffer || readBitMask == 0 || readBitMask > 0x80) { + if (readPos >= data.Num()) return false; + readBitBuffer = data[readPos++]; + readBitMask = 1; + resetReadBitBuffer = false; + } + const bool result = (readBitBuffer & readBitMask) != 0; + readBitMask <<= 1; + return result; +} + +idStr idMQFrame::ReadShortString() { + const int length = ReadByte(); + idStr result; + for (int index = 0; index < length && readPos < data.Num(); ++index) { + result.Append(static_cast(data[readPos++])); + } + resetReadBitBuffer = true; + return result; +} + +void idMQFrame::ReadString(idMQList& value) { + const unsigned int length = ReadUInt32(); + value.Clear(); + for (unsigned int index = 0; index < length && readPos < data.Num(); + ++index) value.Append(data[readPos++]); +} + +void idMQFrame::WriteByte(const unsigned char value) { + WriteData(&value, sizeof(value)); +} + +void idMQFrame::WriteUInt16(const unsigned short value) { + const unsigned char bytes[2] = { + static_cast(value >> 8), + static_cast(value) + }; + WriteData(bytes, sizeof(bytes)); +} + +void idMQFrame::WriteUInt32(const unsigned int value) { + unsigned char bytes[4]; + StoreUInt32BE(bytes, value); + WriteData(bytes, sizeof(bytes)); +} + +void idMQFrame::WriteUInt64(const std::uint64_t value) { + WriteUInt32(static_cast(value >> 32)); + WriteUInt32(static_cast(value)); +} + +void idMQFrame::WriteBool(const bool value) { + if (writeBitMask == 0 || writeBitMask > 0x80) FlushWriteBitBuffer(); + if (value) writeBitBuffer |= static_cast(writeBitMask); + flushWriteBitBuffer = true; + writeBitMask <<= 1; +} + +void idMQFrame::WriteShortString(const idStr& value) { + const int length = std::min(value.Length(), 255); + WriteByte(static_cast(length)); + WriteData(value.c_str(), length); +} + +void idMQFrame::WriteString(const idMQList& value) { + WriteUInt32(value.Num()); + if (value.Num() > 0) WriteData(value.Ptr(), value.Num()); +} + +void idMQFrame::WriteTable(const idMQTable& value) { + FlushWriteBitBuffer(); + const int lengthOffset = data.Num(); + WriteUInt32(0); + const int tableStart = data.Num(); + for (int index = 0; index < value.GetNumKeyVals(); ++index) { + const idMQKeyValue* entry = value.GetKeyVal(index); + WriteShortString(entry->GetKey()); + WriteByte('S'); + const idStr& text = entry->GetValue(); + WriteUInt32(text.Length()); + WriteData(text.c_str(), text.Length()); + } + StoreUInt32BE(data.Ptr() + lengthOffset, data.Num() - tableStart); +} + +void idMQFrame::ReadTable(idMQTable& value) { + value.Clear(); + const unsigned int length = ReadUInt32(); + const int end = std::min(data.Num(), readPos + static_cast(length)); + while (readPos < end) { + const idStr key = ReadShortString(); + const unsigned char fieldType = ReadByte(); + idStr text; + if (fieldType == 'S') { + const unsigned int stringLength = ReadUInt32(); + for (unsigned int i = 0; i < stringLength && readPos < end; ++i) { + text.Append(static_cast(data[readPos++])); + } + } else if (fieldType == 't') { + text = ReadByte() != 0 ? "1" : "0"; + } else if (fieldType == 'I') { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%d", + static_cast(ReadUInt32())); + text = buffer; + } else if (fieldType == 'l') { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%llu", + static_cast(ReadUInt64())); + text = buffer; + } else if (fieldType == 'V') { + text.Clear(); + } else { + // Unknown table values cannot be sized safely without the full + // AMQP type grammar, so consume the remaining declared table. + readPos = end; + break; + } + value.Set(key.c_str(), text.c_str()); + } + readPos = end; + resetReadBitBuffer = true; +} + +void idMQFrame::WritePropertyPresence(const bool present) { + if (writePresencePosition < 15 && present) { + writePresenceBuffer |= static_cast( + 0x8000u >> writePresencePosition); + } + ++writePresencePosition; +} + +void idMQFrame::FinalizeWritePresence() { + WriteUInt16(writePresenceBuffer); + writePresenceBuffer = 0; + writePresencePosition = 0; +} + +bool idMQFrame::ReadPropertyPresence() { + if (readPresencePosition == 0) readPresenceBuffer = ReadUInt16(); + const bool result = readPresencePosition < 15 + && (readPresenceBuffer & (0x8000u >> readPresencePosition)) != 0; + ++readPresencePosition; + return result; +} + +void idMQFrame::FinalizeReadPresence() { + readPresenceBuffer = 0; + readPresencePosition = 0; +} + +void idMQFrame::FinalizeFrame() { FlushWriteBitBuffer(); } + +int idMQFrame::ReadFromStream(idMQTCP& stream) { + Clear(); + unsigned int payloadSize = 0; + if (stream.ReadByte(type) != 1 || stream.ReadUInt16(channel) != 2 + || stream.ReadUInt32(payloadSize) != 4) return -1; + if (payloadSize > 128u * 1024u * 1024u) return -1; + data.SetNum(static_cast(payloadSize)); + if (payloadSize != 0 && stream.Read(data.Ptr(), payloadSize, true, 5000) + != static_cast(payloadSize)) return -1; + unsigned char frameEnd = 0; + if (stream.ReadByte(frameEnd) != 1 || frameEnd != AMQP_FRAME_END) return -1; + return static_cast(payloadSize) + 8; +} + +int idMQFrame::WriteToStream(idMQTCP& stream) { + FinalizeFrame(); + unsigned char header[7] = { + type, + static_cast(channel >> 8), + static_cast(channel), + 0, 0, 0, 0 + }; + StoreUInt32BE(header + 3, data.Num()); + if (stream.Write(header, sizeof(header), true, 5000) != sizeof(header)) { + return -1; + } + if (data.Num() > 0 && stream.Write(data.Ptr(), data.Num(), true, 5000) + != data.Num()) return -1; + const unsigned char frameEnd = AMQP_FRAME_END; + if (stream.Write(&frameEnd, 1, true, 5000) != 1) return -1; + return data.Num() + 8; +} + +idMQFrameHandler::idMQFrameHandler(const amqpEndpoint_t& endpointValue) + : endpoint(endpointValue), readLock(), writeLock(), tcp() { +} + +idMQFrameHandler::~idMQFrameHandler() { Close(); } + +bool idMQFrameHandler::Connect() { + return tcp.Connect(endpoint.host.c_str(), endpoint.port, + endpoint.nonBlocking, endpoint.silent); +} + +void idMQFrameHandler::Close() { tcp.Close(); } + +int idMQFrameHandler::SendFrame(idMQFrame& frame) { + idScopedCriticalSection lock(writeLock); + return frame.WriteToStream(tcp); +} + +int idMQFrameHandler::ReadFrame(idMQFrame& frame) { + idScopedCriticalSection lock(readLock); + return frame.ReadFromStream(tcp); +} + +bool idMQFrameHandler::SendHeader() { + const unsigned char protocol[8] = { + 'A', 'M', 'Q', 'P', 1, 1, + static_cast(endpoint.majorVersion), + static_cast(endpoint.minorVersion) + }; + return tcp.Write(protocol, sizeof(protocol), true, 5000) + == sizeof(protocol); +} diff --git a/source/shared/idlib/networking/amqp/mqmessaging.cpp b/source/shared/idlib/networking/amqp/mqmessaging.cpp new file mode 100644 index 0000000..b30e609 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqmessaging.cpp @@ -0,0 +1,506 @@ +#include "mqmessaging.h" + +#ifdef nullptr +#undef nullptr +#endif + +#define EMPTY_METHOD(name) \ + void name::Populate(idMQFrame&) {} \ + void name::ToFrame(idMQFrame&) {} + +void AMQPConnectionStart::Populate(idMQFrame& frame) { + versionMajor = frame.ReadByte(); + versionMinor = frame.ReadByte(); + frame.ReadTable(serverProperties); + frame.ReadString(mechanisms); + frame.ReadString(locales); +} +void AMQPConnectionStart::ToFrame(idMQFrame& frame) { + frame.WriteByte(versionMajor); + frame.WriteByte(versionMinor); + frame.WriteTable(serverProperties); + frame.WriteString(mechanisms); + frame.WriteString(locales); +} + +void AMQPConnectionStartOk::Populate(idMQFrame& frame) { + frame.ReadTable(clientProperties); + mechanism = frame.ReadShortString(); + frame.ReadString(response); + locale = frame.ReadShortString(); +} +void AMQPConnectionStartOk::ToFrame(idMQFrame& frame) { + frame.WriteTable(clientProperties); + frame.WriteShortString(mechanism); + frame.WriteString(response); + frame.WriteShortString(locale); +} + +void AMQPConnectionSecure::Populate(idMQFrame& frame) { + frame.ReadString(challenge); +} +void AMQPConnectionSecure::ToFrame(idMQFrame& frame) { + frame.WriteString(challenge); +} +void AMQPConnectionSecureOk::Populate(idMQFrame& frame) { + frame.ReadString(response); +} +void AMQPConnectionSecureOk::ToFrame(idMQFrame& frame) { + frame.WriteString(response); +} + +void AMQPConnectionTune::Populate(idMQFrame& frame) { + channelMax = frame.ReadUInt16(); + frameMax = frame.ReadUInt32(); + heartbeat = frame.ReadUInt16(); +} +void AMQPConnectionTune::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(channelMax); + frame.WriteUInt32(frameMax); + frame.WriteUInt16(heartbeat); +} +void AMQPConnectionTuneOk::Populate(idMQFrame& frame) { + channelMax = frame.ReadUInt16(); + frameMax = frame.ReadUInt32(); + heartbeat = frame.ReadUInt16(); +} +void AMQPConnectionTuneOk::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(channelMax); + frame.WriteUInt32(frameMax); + frame.WriteUInt16(heartbeat); +} + +void AMQPConnectionOpen::Populate(idMQFrame& frame) { + virtualHost = frame.ReadShortString(); + capabilities = frame.ReadShortString(); + insist = frame.ReadBool(); +} +void AMQPConnectionOpen::ToFrame(idMQFrame& frame) { + frame.WriteShortString(virtualHost); + frame.WriteShortString(capabilities); + frame.WriteBool(insist); +} +void AMQPConnectionOpenOk::Populate(idMQFrame& frame) { + knownHosts = frame.ReadShortString(); +} +void AMQPConnectionOpenOk::ToFrame(idMQFrame& frame) { + frame.WriteShortString(knownHosts); +} +void AMQPConnectionRedirect::Populate(idMQFrame& frame) { + host = frame.ReadShortString(); + knownHosts = frame.ReadShortString(); +} +void AMQPConnectionRedirect::ToFrame(idMQFrame& frame) { + frame.WriteShortString(host); + frame.WriteShortString(knownHosts); +} +void AMQPConnectionClose::Populate(idMQFrame& frame) { + replyCode = frame.ReadUInt16(); + replyText = frame.ReadShortString(); + cId = frame.ReadUInt16(); + mId = frame.ReadUInt16(); +} +void AMQPConnectionClose::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(replyCode); + frame.WriteShortString(replyText); + frame.WriteUInt16(cId); + frame.WriteUInt16(mId); +} +EMPTY_METHOD(AMQPConnectionCloseOk) + +void AMQPChannelOpen::Populate(idMQFrame& frame) { + oob = frame.ReadShortString(); +} +void AMQPChannelOpen::ToFrame(idMQFrame& frame) { + frame.WriteShortString(oob); +} +EMPTY_METHOD(AMQPChannelOpenOk) +void AMQPChannelFlow::Populate(idMQFrame& frame) { active = frame.ReadBool(); } +void AMQPChannelFlow::ToFrame(idMQFrame& frame) { frame.WriteBool(active); } +void AMQPChannelFlowOk::Populate(idMQFrame& frame) { active = frame.ReadBool(); } +void AMQPChannelFlowOk::ToFrame(idMQFrame& frame) { frame.WriteBool(active); } +void AMQPChannelAlert::Populate(idMQFrame& frame) { + replyCode = frame.ReadUInt16(); + replyText = frame.ReadShortString(); + frame.ReadTable(details); +} +void AMQPChannelAlert::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(replyCode); + frame.WriteShortString(replyText); + frame.WriteTable(details); +} +void AMQPChannelClose::Populate(idMQFrame& frame) { + replyCode = frame.ReadUInt16(); + replyText = frame.ReadShortString(); + cId = frame.ReadUInt16(); + mId = frame.ReadUInt16(); +} +void AMQPChannelClose::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(replyCode); + frame.WriteShortString(replyText); + frame.WriteUInt16(cId); + frame.WriteUInt16(mId); +} +EMPTY_METHOD(AMQPChannelCloseOk) + +void AMQPExchangeDeclare::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + exchange = frame.ReadShortString(); + type = frame.ReadShortString(); + passive = frame.ReadBool(); + durable = frame.ReadBool(); + autoDelete = frame.ReadBool(); + isInternal = frame.ReadBool(); + noWait = frame.ReadBool(); + frame.ReadTable(arguments); +} +void AMQPExchangeDeclare::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(exchange); + frame.WriteShortString(type); + frame.WriteBool(passive); + frame.WriteBool(durable); + frame.WriteBool(autoDelete); + frame.WriteBool(isInternal); + frame.WriteBool(noWait); + frame.WriteTable(arguments); +} +EMPTY_METHOD(AMQPExchangeDeclareOk) +void AMQPExchangeDelete::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + exchange = frame.ReadShortString(); + unused = frame.ReadBool(); + noWait = frame.ReadBool(); +} +void AMQPExchangeDelete::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(exchange); + frame.WriteBool(unused); + frame.WriteBool(noWait); +} +EMPTY_METHOD(AMQPExchangeDeleteOk) + +void AMQPQueueDeclare::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + passive = frame.ReadBool(); + durable = frame.ReadBool(); + exclusive = frame.ReadBool(); + autoDelete = frame.ReadBool(); + noWait = frame.ReadBool(); + frame.ReadTable(arguments); +} +void AMQPQueueDeclare::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteBool(passive); + frame.WriteBool(durable); + frame.WriteBool(exclusive); + frame.WriteBool(autoDelete); + frame.WriteBool(noWait); + frame.WriteTable(arguments); +} +void AMQPQueueDeclareOk::Populate(idMQFrame& frame) { + queue = frame.ReadShortString(); + messageCount = frame.ReadUInt32(); + consumerCount = frame.ReadUInt32(); +} +void AMQPQueueDeclareOk::ToFrame(idMQFrame& frame) { + frame.WriteShortString(queue); + frame.WriteUInt32(messageCount); + frame.WriteUInt32(consumerCount); +} +void AMQPQueueBind::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); + noWait = frame.ReadBool(); + frame.ReadTable(arguments); +} +void AMQPQueueBind::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); + frame.WriteBool(noWait); + frame.WriteTable(arguments); +} +EMPTY_METHOD(AMQPQueueBindOk) +void AMQPQueueUnbind::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); + frame.ReadTable(arguments); +} +void AMQPQueueUnbind::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); + frame.WriteTable(arguments); +} +EMPTY_METHOD(AMQPQueueUnbindOk) +void AMQPQueuePurge::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + noWait = frame.ReadBool(); +} +void AMQPQueuePurge::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteBool(noWait); +} +void AMQPQueuePurgeOk::Populate(idMQFrame& frame) { + messageCount = frame.ReadUInt32(); +} +void AMQPQueuePurgeOk::ToFrame(idMQFrame& frame) { + frame.WriteUInt32(messageCount); +} +void AMQPQueueDelete::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + unused = frame.ReadBool(); + empty = frame.ReadBool(); + noWait = frame.ReadBool(); +} +void AMQPQueueDelete::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteBool(unused); + frame.WriteBool(empty); + frame.WriteBool(noWait); +} +void AMQPQueueDeleteOk::Populate(idMQFrame& frame) { + messageCount = frame.ReadUInt32(); +} +void AMQPQueueDeleteOk::ToFrame(idMQFrame& frame) { + frame.WriteUInt32(messageCount); +} + +void AMQPBasicQos::Populate(idMQFrame& frame) { + prefetchSize = frame.ReadUInt32(); + prefetchCount = frame.ReadUInt16(); + isGlobal = frame.ReadBool(); +} +void AMQPBasicQos::ToFrame(idMQFrame& frame) { + frame.WriteUInt32(prefetchSize); + frame.WriteUInt16(prefetchCount); + frame.WriteBool(isGlobal); +} +EMPTY_METHOD(AMQPBasicQosOk) +void AMQPBasicConsume::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + consumerTag = frame.ReadShortString(); + noLocal = frame.ReadBool(); + noAck = frame.ReadBool(); + exclusive = frame.ReadBool(); + noWait = frame.ReadBool(); +} +void AMQPBasicConsume::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteShortString(consumerTag); + frame.WriteBool(noLocal); + frame.WriteBool(noAck); + frame.WriteBool(exclusive); + frame.WriteBool(noWait); +} +void AMQPBasicConsumeOk::Populate(idMQFrame& frame) { + consumerTag = frame.ReadShortString(); +} +void AMQPBasicConsumeOk::ToFrame(idMQFrame& frame) { + frame.WriteShortString(consumerTag); +} +void AMQPBasicCancel::Populate(idMQFrame& frame) { + consumerTag = frame.ReadShortString(); + noWait = frame.ReadBool(); +} +void AMQPBasicCancel::ToFrame(idMQFrame& frame) { + frame.WriteShortString(consumerTag); + frame.WriteBool(noWait); +} +void AMQPBasicCancelOk::Populate(idMQFrame& frame) { + consumerTag = frame.ReadShortString(); +} +void AMQPBasicCancelOk::ToFrame(idMQFrame& frame) { + frame.WriteShortString(consumerTag); +} +void AMQPBasicPublish::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); + mandatory = frame.ReadBool(); + immediate = frame.ReadBool(); +} +void AMQPBasicPublish::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); + frame.WriteBool(mandatory); + frame.WriteBool(immediate); +} +void AMQPBasicReturn::Populate(idMQFrame& frame) { + replyCode = frame.ReadUInt16(); + replyText = frame.ReadShortString(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); +} +void AMQPBasicReturn::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(replyCode); + frame.WriteShortString(replyText); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); +} +void AMQPBasicDeliver::Populate(idMQFrame& frame) { + consumerTag = frame.ReadShortString(); + deliveryTag = frame.ReadUInt64(); + redelivered = frame.ReadBool(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); +} +void AMQPBasicDeliver::ToFrame(idMQFrame& frame) { + frame.WriteShortString(consumerTag); + frame.WriteUInt64(deliveryTag); + frame.WriteBool(redelivered); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); +} +void AMQPBasicGet::Populate(idMQFrame& frame) { + ticket = frame.ReadUInt16(); + queue = frame.ReadShortString(); + noAck = frame.ReadBool(); +} +void AMQPBasicGet::ToFrame(idMQFrame& frame) { + frame.WriteUInt16(ticket); + frame.WriteShortString(queue); + frame.WriteBool(noAck); +} +void AMQPBasicGetOk::Populate(idMQFrame& frame) { + deliveryTag = frame.ReadUInt64(); + redelivered = frame.ReadBool(); + exchange = frame.ReadShortString(); + routingKey = frame.ReadShortString(); + messageCount = frame.ReadUInt32(); +} +void AMQPBasicGetOk::ToFrame(idMQFrame& frame) { + frame.WriteUInt64(deliveryTag); + frame.WriteBool(redelivered); + frame.WriteShortString(exchange); + frame.WriteShortString(routingKey); + frame.WriteUInt32(messageCount); +} +void AMQPBasicGetEmpty::Populate(idMQFrame& frame) { + clusterId = frame.ReadShortString(); +} +void AMQPBasicGetEmpty::ToFrame(idMQFrame& frame) { + frame.WriteShortString(clusterId); +} +void AMQPBasicAck::Populate(idMQFrame& frame) { + deliveryTag = frame.ReadUInt64(); + multiple = frame.ReadBool(); +} +void AMQPBasicAck::ToFrame(idMQFrame& frame) { + frame.WriteUInt64(deliveryTag); + frame.WriteBool(multiple); +} +void AMQPBasicReject::Populate(idMQFrame& frame) { + deliveryTag = frame.ReadUInt64(); + requeue = frame.ReadBool(); +} +void AMQPBasicReject::ToFrame(idMQFrame& frame) { + frame.WriteUInt64(deliveryTag); + frame.WriteBool(requeue); +} +void AMQPBasicRecoverAsync::Populate(idMQFrame& frame) { + requeue = frame.ReadBool(); +} +void AMQPBasicRecoverAsync::ToFrame(idMQFrame& frame) { + frame.WriteBool(requeue); +} +void AMQPBasicRecover::Populate(idMQFrame& frame) { + requeue = frame.ReadBool(); +} +void AMQPBasicRecover::ToFrame(idMQFrame& frame) { + frame.WriteBool(requeue); +} +EMPTY_METHOD(AMQPBasicRecoverOk) + +AMQPBasicProperties::AMQPBasicProperties() + : idMQContentHeader(), contentType(), contentEncoding(), headers(), + deliveryMode(0), priority(0), correlationId(), replyTo(), expiration(), + messageId(), timestamp(0), type(), userId(), appId(), clusterId(), + b_contentType(false), b_contentEncoding(false), b_headers(false), + b_deliveryMode(false), b_priority(false), b_correlationId(false), + b_replyTo(false), b_expiration(false), b_messageId(false), + b_timestamp(false), b_type(false), b_userId(false), b_appId(false), + b_clusterId(false) { +} + +void AMQPBasicProperties::Populate(idMQFrame& frame) { + b_contentType = frame.ReadPropertyPresence(); + b_contentEncoding = frame.ReadPropertyPresence(); + b_headers = frame.ReadPropertyPresence(); + b_deliveryMode = frame.ReadPropertyPresence(); + b_priority = frame.ReadPropertyPresence(); + b_correlationId = frame.ReadPropertyPresence(); + b_replyTo = frame.ReadPropertyPresence(); + b_expiration = frame.ReadPropertyPresence(); + b_messageId = frame.ReadPropertyPresence(); + b_timestamp = frame.ReadPropertyPresence(); + b_type = frame.ReadPropertyPresence(); + b_userId = frame.ReadPropertyPresence(); + b_appId = frame.ReadPropertyPresence(); + b_clusterId = frame.ReadPropertyPresence(); + frame.FinalizeReadPresence(); + if (b_contentType) contentType = frame.ReadShortString(); + if (b_contentEncoding) contentEncoding = frame.ReadShortString(); + if (b_headers) frame.ReadTable(headers); + if (b_deliveryMode) deliveryMode = frame.ReadByte(); + if (b_priority) priority = frame.ReadByte(); + if (b_correlationId) correlationId = frame.ReadShortString(); + if (b_replyTo) replyTo = frame.ReadShortString(); + if (b_expiration) expiration = frame.ReadShortString(); + if (b_messageId) messageId = frame.ReadShortString(); + if (b_timestamp) timestamp = frame.ReadUInt64(); + if (b_type) type = frame.ReadShortString(); + if (b_userId) userId = frame.ReadShortString(); + if (b_appId) appId = frame.ReadShortString(); + if (b_clusterId) clusterId = frame.ReadShortString(); +} + +void AMQPBasicProperties::ToFrame(idMQFrame& frame) { + frame.WritePropertyPresence(b_contentType); + frame.WritePropertyPresence(b_contentEncoding); + frame.WritePropertyPresence(b_headers); + frame.WritePropertyPresence(b_deliveryMode); + frame.WritePropertyPresence(b_priority); + frame.WritePropertyPresence(b_correlationId); + frame.WritePropertyPresence(b_replyTo); + frame.WritePropertyPresence(b_expiration); + frame.WritePropertyPresence(b_messageId); + frame.WritePropertyPresence(b_timestamp); + frame.WritePropertyPresence(b_type); + frame.WritePropertyPresence(b_userId); + frame.WritePropertyPresence(b_appId); + frame.WritePropertyPresence(b_clusterId); + frame.FinalizeWritePresence(); + if (b_contentType) frame.WriteShortString(contentType); + if (b_contentEncoding) frame.WriteShortString(contentEncoding); + if (b_headers) frame.WriteTable(headers); + if (b_deliveryMode) frame.WriteByte(deliveryMode); + if (b_priority) frame.WriteByte(priority); + if (b_correlationId) frame.WriteShortString(correlationId); + if (b_replyTo) frame.WriteShortString(replyTo); + if (b_expiration) frame.WriteShortString(expiration); + if (b_messageId) frame.WriteShortString(messageId); + if (b_timestamp) frame.WriteUInt64(timestamp); + if (b_type) frame.WriteShortString(type); + if (b_userId) frame.WriteShortString(userId); + if (b_appId) frame.WriteShortString(appId); + if (b_clusterId) frame.WriteShortString(clusterId); +} + +#undef EMPTY_METHOD diff --git a/source/shared/idlib/networking/amqp/mqmessaging.h b/source/shared/idlib/networking/amqp/mqmessaging.h new file mode 100644 index 0000000..fdc8335 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqmessaging.h @@ -0,0 +1,597 @@ +#pragma once + +#include "mqcommon.h" + +enum amqpConnectionMethodId_t { + AMQP_METHOD_CONNECTION_START = 10, + AMQP_METHOD_CONNECTION_STARTOK = 11, + AMQP_METHOD_CONNECTION_SECURE = 20, + AMQP_METHOD_CONNECTION_SECUREOK = 21, + AMQP_METHOD_CONNECTION_TUNE = 30, + AMQP_METHOD_CONNECTION_TUNEOK = 31, + AMQP_METHOD_CONNECTION_OPEN = 40, + AMQP_METHOD_CONNECTION_OPENOK = 41, + AMQP_METHOD_CONNECTION_REDIRECT = 50, + AMQP_METHOD_CONNECTION_CLOSE = 60, + AMQP_METHOD_CONNECTION_CLOSEOK = 61 +}; + +enum amqpChannelMethodId_t { + AMQP_METHOD_CHANNEL_OPEN = 10, + AMQP_METHOD_CHANNEL_OPENOK = 11, + AMQP_METHOD_CHANNEL_FLOW = 20, + AMQP_METHOD_CHANNEL_FLOWOK = 21, + AMQP_METHOD_CHANNEL_ALERT = 30, + AMQP_METHOD_CHANNEL_CLOSE = 40, + AMQP_METHOD_CHANNEL_CLOSEOK = 41 +}; + +enum amqpExchangeMethodId_t { + AMQP_METHOD_EXCHANGE_DECLARE = 10, + AMQP_METHOD_EXCHANGE_DECLAREOK = 11, + AMQP_METHOD_EXCHANGE_DELETE = 20, + AMQP_METHOD_EXCHANGE_DELETEOK = 21 +}; + +enum amqpQueueMethodId_t { + AMQP_METHOD_QUEUE_DECLARE = 10, + AMQP_METHOD_QUEUE_DECLAREOK = 11, + AMQP_METHOD_QUEUE_BIND = 20, + AMQP_METHOD_QUEUE_BINDOK = 21, + AMQP_METHOD_QUEUE_PURGE = 30, + AMQP_METHOD_QUEUE_PURGEOK = 31, + AMQP_METHOD_QUEUE_DELETE = 40, + AMQP_METHOD_QUEUE_DELETEOK = 41, + AMQP_METHOD_QUEUE_UNBIND = 50, + AMQP_METHOD_QUEUE_UNBINDOK = 51 +}; + +enum amqpBasicMethodId_t { + AMQP_METHOD_BASIC_QOS = 10, + AMQP_METHOD_BASIC_QOSOK = 11, + AMQP_METHOD_BASIC_CONSUME = 20, + AMQP_METHOD_BASIC_CONSUMEOK = 21, + AMQP_METHOD_BASIC_CANCEL = 30, + AMQP_METHOD_BASIC_CANCELOK = 31, + AMQP_METHOD_BASIC_PUBLISH = 40, + AMQP_METHOD_BASIC_RETURN = 50, + AMQP_METHOD_BASIC_DELIVER = 60, + AMQP_METHOD_BASIC_GET = 70, + AMQP_METHOD_BASIC_GETOK = 71, + AMQP_METHOD_BASIC_GETEMPTY = 72, + AMQP_METHOD_BASIC_ACK = 80, + AMQP_METHOD_BASIC_REJECT = 90, + AMQP_METHOD_BASIC_RECOVERASYNC = 100, + AMQP_METHOD_BASIC_RECOVER = 110, + AMQP_METHOD_BASIC_RECOVEROK = 111 +}; + +#define ID_AMQP_METHOD_META(classIdValue, methodIdValue, contentValue, asyncValue) \ + int GetClassId() const override { return classIdValue; } \ + int GetMethodId() const override { return methodIdValue; } \ + bool HasContent() const override { return contentValue; } \ + bool IsAsync() const override { return asyncValue; } \ + void Populate(idMQFrame& frame) override; \ + void ToFrame(idMQFrame& frame) override + +class AMQPConnectionStart : public idMQMethod { +public: + AMQPConnectionStart() : versionMajor(0), versionMinor(0) {} + ID_AMQP_METHOD_META(10, 10, false, true); + unsigned char versionMajor; + unsigned char versionMinor; + idMQTable serverProperties; + idMQList mechanisms; + idMQList locales; +}; + +class AMQPConnectionStartOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 11, false, false); + idMQTable clientProperties; + idStr mechanism; + idMQList response; + idStr locale; +}; + +class AMQPConnectionSecure : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 20, false, true); + idMQList challenge; +}; + +class AMQPConnectionSecureOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 21, false, false); + idMQList response; +}; + +class AMQPConnectionTune : public idMQMethod { +public: + AMQPConnectionTune() : channelMax(0), frameMax(0), heartbeat(0) {} + ID_AMQP_METHOD_META(10, 30, false, true); + unsigned short channelMax; + unsigned int frameMax; + unsigned short heartbeat; +}; + +class AMQPConnectionTuneOk : public idMQMethod { +public: + AMQPConnectionTuneOk() : channelMax(0), frameMax(0), heartbeat(0) {} + ID_AMQP_METHOD_META(10, 31, false, false); + unsigned short channelMax; + unsigned int frameMax; + unsigned short heartbeat; +}; + +class AMQPConnectionOpen : public idMQMethod { +public: + AMQPConnectionOpen() : insist(false) {} + ID_AMQP_METHOD_META(10, 40, false, false); + idStr virtualHost; + idStr capabilities; + bool insist; +}; + +class AMQPConnectionOpenOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 41, false, true); + idStr knownHosts; +}; + +class AMQPConnectionRedirect : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 50, false, true); + idStr host; + idStr knownHosts; +}; + +class AMQPConnectionClose : public idMQMethod { +public: + AMQPConnectionClose() : replyCode(0), cId(0), mId(0) {} + ID_AMQP_METHOD_META(10, 60, false, true); + unsigned short replyCode; + idStr replyText; + unsigned short cId; + unsigned short mId; +}; + +class AMQPConnectionCloseOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(10, 61, false, true); +}; + +class AMQPChannelOpen : public idMQMethod { +public: + ID_AMQP_METHOD_META(20, 10, false, false); + idStr oob; +}; + +class AMQPChannelOpenOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(20, 11, false, true); +}; + +class AMQPChannelFlow : public idMQMethod { +public: + AMQPChannelFlow() : active(false) {} + ID_AMQP_METHOD_META(20, 20, false, true); + bool active; +}; + +class AMQPChannelFlowOk : public idMQMethod { +public: + AMQPChannelFlowOk() : active(false) {} + ID_AMQP_METHOD_META(20, 21, false, true); + bool active; +}; + +class AMQPChannelAlert : public idMQMethod { +public: + AMQPChannelAlert() : replyCode(0) {} + ID_AMQP_METHOD_META(20, 30, false, true); + unsigned short replyCode; + idStr replyText; + idMQTable details; +}; + +class AMQPChannelClose : public idMQMethod { +public: + AMQPChannelClose() : replyCode(0), cId(0), mId(0) {} + ID_AMQP_METHOD_META(20, 40, false, true); + unsigned short replyCode; + idStr replyText; + unsigned short cId; + unsigned short mId; +}; + +class AMQPChannelCloseOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(20, 41, false, true); +}; + +class AMQPExchangeDeclare : public idMQMethod { +public: + AMQPExchangeDeclare() : ticket(0), passive(false), durable(false), + autoDelete(false), isInternal(false), noWait(false) {} + ID_AMQP_METHOD_META(40, 10, false, false); + unsigned short ticket; + idStr exchange; + idStr type; + bool passive; + bool durable; + bool autoDelete; + bool isInternal; + bool noWait; + idMQTable arguments; +}; + +class AMQPExchangeDeclareOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(40, 11, false, true); +}; + +class AMQPExchangeDelete : public idMQMethod { +public: + AMQPExchangeDelete() : ticket(0), unused(false), noWait(false) {} + ID_AMQP_METHOD_META(40, 20, false, false); + unsigned short ticket; + idStr exchange; + bool unused; + bool noWait; +}; + +class AMQPExchangeDeleteOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(40, 21, false, true); +}; + +class AMQPQueueDeclare : public idMQMethod { +public: + AMQPQueueDeclare() : ticket(0), passive(false), durable(false), + exclusive(false), autoDelete(false), noWait(false) {} + ID_AMQP_METHOD_META(50, 10, false, false); + unsigned short ticket; + idStr queue; + bool passive; + bool durable; + bool exclusive; + bool autoDelete; + bool noWait; + idMQTable arguments; +}; + +class AMQPQueueDeclareOk : public idMQMethod { +public: + AMQPQueueDeclareOk() : messageCount(0), consumerCount(0) {} + ID_AMQP_METHOD_META(50, 11, false, true); + idStr queue; + unsigned int messageCount; + unsigned int consumerCount; +}; + +class AMQPQueueBind : public idMQMethod { +public: + AMQPQueueBind() : ticket(0), noWait(false) {} + ID_AMQP_METHOD_META(50, 20, false, false); + unsigned short ticket; + idStr queue; + idStr exchange; + idStr routingKey; + bool noWait; + idMQTable arguments; +}; + +class AMQPQueueBindOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(50, 21, false, true); +}; + +class AMQPQueueUnbind : public idMQMethod { +public: + AMQPQueueUnbind() : ticket(0) {} + ID_AMQP_METHOD_META(50, 50, false, false); + unsigned short ticket; + idStr queue; + idStr exchange; + idStr routingKey; + idMQTable arguments; +}; + +class AMQPQueueUnbindOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(50, 51, false, true); +}; + +class AMQPQueuePurge : public idMQMethod { +public: + AMQPQueuePurge() : ticket(0), noWait(false) {} + ID_AMQP_METHOD_META(50, 30, false, false); + unsigned short ticket; + idStr queue; + bool noWait; +}; + +class AMQPQueuePurgeOk : public idMQMethod { +public: + AMQPQueuePurgeOk() : messageCount(0) {} + ID_AMQP_METHOD_META(50, 31, false, true); + unsigned int messageCount; +}; + +class AMQPQueueDelete : public idMQMethod { +public: + AMQPQueueDelete() : ticket(0), unused(false), empty(false), noWait(false) {} + ID_AMQP_METHOD_META(50, 40, false, false); + unsigned short ticket; + idStr queue; + bool unused; + bool empty; + bool noWait; +}; + +class AMQPQueueDeleteOk : public idMQMethod { +public: + AMQPQueueDeleteOk() : messageCount(0) {} + ID_AMQP_METHOD_META(50, 41, false, true); + unsigned int messageCount; +}; + +class AMQPBasicQos : public idMQMethod { +public: + AMQPBasicQos() : prefetchSize(0), prefetchCount(0), isGlobal(false) {} + ID_AMQP_METHOD_META(60, 10, false, false); + unsigned int prefetchSize; + unsigned short prefetchCount; + bool isGlobal; +}; + +class AMQPBasicQosOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(60, 11, false, true); +}; + +class AMQPBasicConsume : public idMQMethod { +public: + AMQPBasicConsume() : ticket(0), noLocal(false), noAck(false), + exclusive(false), noWait(false) {} + ID_AMQP_METHOD_META(60, 20, false, false); + unsigned short ticket; + idStr queue; + idStr consumerTag; + bool noLocal; + bool noAck; + bool exclusive; + bool noWait; +}; + +class AMQPBasicConsumeOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(60, 21, false, true); + idStr consumerTag; +}; + +class AMQPBasicCancel : public idMQMethod { +public: + AMQPBasicCancel() : noWait(false) {} + ID_AMQP_METHOD_META(60, 30, false, false); + idStr consumerTag; + bool noWait; +}; + +class AMQPBasicCancelOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(60, 31, false, true); + idStr consumerTag; +}; + +class AMQPBasicPublish : public idMQMethod { +public: + AMQPBasicPublish() : ticket(0), mandatory(false), immediate(false) {} + ID_AMQP_METHOD_META(60, 40, true, false); + unsigned short ticket; + idStr exchange; + idStr routingKey; + bool mandatory; + bool immediate; +}; + +class AMQPBasicReturn : public idMQMethod { +public: + AMQPBasicReturn() : replyCode(0) {} + ID_AMQP_METHOD_META(60, 50, true, true); + unsigned short replyCode; + idStr replyText; + idStr exchange; + idStr routingKey; +}; + +class AMQPBasicDeliver : public idMQMethod { +public: + AMQPBasicDeliver() : deliveryTag(0), redelivered(false) {} + ID_AMQP_METHOD_META(60, 60, true, true); + idStr consumerTag; + std::uint64_t deliveryTag; + bool redelivered; + idStr exchange; + idStr routingKey; +}; + +class AMQPBasicGet : public idMQMethod { +public: + AMQPBasicGet() : ticket(0), noAck(false) {} + ID_AMQP_METHOD_META(60, 70, false, false); + unsigned short ticket; + idStr queue; + bool noAck; +}; + +class AMQPBasicGetOk : public idMQMethod { +public: + AMQPBasicGetOk() : deliveryTag(0), redelivered(false), messageCount(0) {} + ID_AMQP_METHOD_META(60, 71, true, true); + std::uint64_t deliveryTag; + bool redelivered; + idStr exchange; + idStr routingKey; + unsigned int messageCount; +}; + +class AMQPBasicGetEmpty : public idMQMethod { +public: + ID_AMQP_METHOD_META(60, 72, false, true); + idStr clusterId; +}; + +class AMQPBasicAck : public idMQMethod { +public: + AMQPBasicAck() : deliveryTag(0), multiple(false) {} + ID_AMQP_METHOD_META(60, 80, false, true); + std::uint64_t deliveryTag; + bool multiple; +}; + +class AMQPBasicReject : public idMQMethod { +public: + AMQPBasicReject() : deliveryTag(0), requeue(false) {} + ID_AMQP_METHOD_META(60, 90, false, true); + std::uint64_t deliveryTag; + bool requeue; +}; + +class AMQPBasicRecoverAsync : public idMQMethod { +public: + AMQPBasicRecoverAsync() : requeue(false) {} + ID_AMQP_METHOD_META(60, 100, false, false); + bool requeue; +}; + +class AMQPBasicRecover : public idMQMethod { +public: + AMQPBasicRecover() : requeue(false) {} + ID_AMQP_METHOD_META(60, 110, false, false); + bool requeue; +}; + +class AMQPBasicRecoverOk : public idMQMethod { +public: + ID_AMQP_METHOD_META(60, 111, false, true); +}; + +class AMQPBasicProperties : public idMQContentHeader { +public: + AMQPBasicProperties(); + int GetProtocolId() const override { return AMQP_CLASS_BASIC; } + void Populate(idMQFrame& frame) override; + void ToFrame(idMQFrame& frame) override; + + idStr contentType; + idStr contentEncoding; + idMQTable headers; + unsigned char deliveryMode; + unsigned char priority; + idStr correlationId; + idStr replyTo; + idStr expiration; + idStr messageId; + std::uint64_t timestamp; + idStr type; + idStr userId; + idStr appId; + idStr clusterId; + bool b_contentType; + bool b_contentEncoding; + bool b_headers; + bool b_deliveryMode; + bool b_priority; + bool b_correlationId; + bool b_replyTo; + bool b_expiration; + bool b_messageId; + bool b_timestamp; + bool b_type; + bool b_userId; + bool b_appId; + bool b_clusterId; +}; + +class idMQConsumer; + +struct amqpBasicGetResultOk_t { + amqpBasicGetResultOk_t() + : deliveryTag(0), redelivered(false), exchange(), routingKey(), + messageCount(0), header(nullptr), body() {} + amqpBasicGetResultOk_t(const amqpBasicGetResultOk_t& other); + amqpBasicGetResultOk_t& operator=(const amqpBasicGetResultOk_t& other); + ~amqpBasicGetResultOk_t(); + std::uint64_t deliveryTag; + bool redelivered; + idStr exchange; + idStr routingKey; + unsigned int messageCount; + AMQPBasicProperties* header; + idMQList body; +}; + +class idMQChannel { +public: + explicit idMQChannel(idMQSession* session); + ~idMQChannel(); + void Init(); + void Close(); + MQErrors_t HandleCommand(idMQCommand& command); + + static idMQMethod* DecodeMethod(idMQFrame& frame); + static idMQContentHeader* DecodeContentHeader(idMQFrame& frame); + + void ConnectionStartOk(AMQPConnectionTune& tune, + const idStr& mechanism, const idMQList& response, + const idStr& locale); + void ConnectionTuneOk(unsigned short channelMax, unsigned int frameMax, + unsigned short heartbeat); + void ConnectionOpen(idStr& knownHosts, const idStr& vhost, + const idStr& capabilities, bool insist); + void ConnectionClose(unsigned short replyCode, const idStr& replyText, + unsigned short classId, unsigned short methodId); + void ChannelOpen(const idStr& outOfBand = idStr("")); + void ExchangeDeclare(const idStr& exchange, const idStr& type, + bool durable); + void ExchangeDeclare(const idStr& exchange, const idStr& type, + bool durable, bool autoDelete); + void BasicPublish(const idStr& exchange, const idStr& routingKey, + const idMQList& body); + void BasicPublish(const idStr& exchange, const idStr& routingKey, + bool mandatory, bool immediate, const void* data, unsigned int size); + + idMQSession* session; + idSysMutex protocolWait; + idDeferredResult deferredConnectionStart; + idDeferredResult deferredConnectionTune; + idDeferredResult deferredConnectionOpenOk; + idDeferredResult deferredChannelOpenOk; + idDeferredResult deferredChannelFlow; + idDeferredResult deferredChannelFlowOk; + idDeferredResult deferredExchangeDeclareOk; + idDeferredResult deferredExchangeDeleteOk; + idDeferredResult deferredQueueDeclareOk; + idDeferredResult deferredQueueBindOk; + idDeferredResult deferredQueuePurgeOk; + idDeferredResult deferredQueueDeleteOk; + idDeferredResult deferredBasicConsumeOk; + idDeferredResult deferredBasicCancelOk; + idDeferredResult deferredBasicGetResultOk; + idDeferredResult deferredAMQPBasicRecoverOk; + idMQList consumers; + +private: + void ExchangeDeclareInternal(const idStr& exchange, const idStr& type, + bool passive, bool durable, bool autoDelete, bool internal, + bool noWait); + void BasicPublishInternal(const idStr& exchange, const idStr& routingKey, + bool mandatory, bool immediate, + const idMQList& body); + idMQConsumer* LookupConsumer(const idStr& tag); + bool RemoveConsumer(const idStr& tag); +}; + +#undef ID_AMQP_METHOD_META diff --git a/source/shared/idlib/networking/amqp/mqprotocol.cpp b/source/shared/idlib/networking/amqp/mqprotocol.cpp new file mode 100644 index 0000000..6022024 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqprotocol.cpp @@ -0,0 +1,421 @@ +#include "mqmessaging.h" +#include "mqconsumer.h" + +#ifdef nullptr +#undef nullptr +#endif + +#include + +namespace { + +template +idMQMethod* Decode(idMQFrame& frame) { + T* value = new T(); + value->Populate(frame); + return value; +} + +void SendMethod(idMQSession* session, idMQMethod& method) { + if (session == nullptr) return; + idMQCommand command; + command.method = &method; + session->SendCommand(command); +} + +} // namespace + +amqpBasicGetResultOk_t::amqpBasicGetResultOk_t( + const amqpBasicGetResultOk_t& other) + : amqpBasicGetResultOk_t() { + *this = other; +} + +amqpBasicGetResultOk_t& amqpBasicGetResultOk_t::operator=( + const amqpBasicGetResultOk_t& other) { + if (this == &other) return *this; + deliveryTag = other.deliveryTag; + redelivered = other.redelivered; + exchange = other.exchange; + routingKey = other.routingKey; + messageCount = other.messageCount; + delete header; + header = other.header == nullptr ? nullptr + : new AMQPBasicProperties(*other.header); + body = other.body; + return *this; +} + +amqpBasicGetResultOk_t::~amqpBasicGetResultOk_t() { delete header; } + +idMQChannel::idMQChannel(idMQSession* sessionValue) + : session(sessionValue), protocolWait(), deferredConnectionStart(), + deferredConnectionTune(), deferredConnectionOpenOk(), + deferredChannelOpenOk(), deferredChannelFlow(), deferredChannelFlowOk(), + deferredExchangeDeclareOk(), deferredExchangeDeleteOk(), + deferredQueueDeclareOk(), deferredQueueBindOk(), deferredQueuePurgeOk(), + deferredQueueDeleteOk(), deferredBasicConsumeOk(), + deferredBasicCancelOk(), deferredBasicGetResultOk(), + deferredAMQPBasicRecoverOk(), consumers() { +} + +idMQChannel::~idMQChannel() = default; + +void idMQChannel::Init() { + deferredConnectionStart.Clear(); + deferredConnectionTune.Clear(); + deferredConnectionOpenOk.Clear(); + deferredChannelOpenOk.Clear(); +} + +idMQMethod* idMQChannel::DecodeMethod(idMQFrame& frame) { + const int classId = frame.ReadUInt16(); + const int methodId = frame.ReadUInt16(); + switch (classId) { + case AMQP_CLASS_CONNECTION: + switch (methodId) { + case 10: return Decode(frame); + case 11: return Decode(frame); + case 20: return Decode(frame); + case 21: return Decode(frame); + case 30: return Decode(frame); + case 31: return Decode(frame); + case 40: return Decode(frame); + case 41: return Decode(frame); + case 50: return Decode(frame); + case 60: return Decode(frame); + case 61: return Decode(frame); + } + break; + case AMQP_CLASS_CHANNEL: + switch (methodId) { + case 10: return Decode(frame); + case 11: return Decode(frame); + case 20: return Decode(frame); + case 21: return Decode(frame); + case 30: return Decode(frame); + case 40: return Decode(frame); + case 41: return Decode(frame); + } + break; + case AMQP_CLASS_EXCHANGE: + switch (methodId) { + case 10: return Decode(frame); + case 11: return Decode(frame); + case 20: return Decode(frame); + case 21: return Decode(frame); + } + break; + case AMQP_CLASS_QUEUE: + switch (methodId) { + case 10: return Decode(frame); + case 11: return Decode(frame); + case 20: return Decode(frame); + case 21: return Decode(frame); + case 30: return Decode(frame); + case 31: return Decode(frame); + case 40: return Decode(frame); + case 41: return Decode(frame); + case 50: return Decode(frame); + case 51: return Decode(frame); + } + break; + case AMQP_CLASS_BASIC: + switch (methodId) { + case 10: return Decode(frame); + case 11: return Decode(frame); + case 20: return Decode(frame); + case 21: return Decode(frame); + case 30: return Decode(frame); + case 31: return Decode(frame); + case 40: return Decode(frame); + case 50: return Decode(frame); + case 60: return Decode(frame); + case 70: return Decode(frame); + case 71: return Decode(frame); + case 72: return Decode(frame); + case 80: return Decode(frame); + case 90: return Decode(frame); + case 100: return Decode(frame); + case 110: return Decode(frame); + case 111: return Decode(frame); + } + break; + } + return nullptr; +} + +idMQContentHeader* idMQChannel::DecodeContentHeader(idMQFrame& frame) { + const int classId = frame.ReadUInt16(); + frame.ReadUInt16(); // weight, reserved and always zero + const std::uint64_t bodyLength = frame.ReadUInt64(); + if (classId != AMQP_CLASS_BASIC) return nullptr; + AMQPBasicProperties* header = new AMQPBasicProperties(); + header->bodyLength = bodyLength; + header->Populate(frame); + return header; +} + +idMQConsumer* idMQChannel::LookupConsumer(const idStr& tag) { + for (int index = 0; index < consumers.Num(); ++index) { + if (consumers[index] != nullptr + && _stricmp(consumers[index]->consumerTag.c_str(), + tag.c_str()) == 0) { + return consumers[index]; + } + } + return nullptr; +} + +bool idMQChannel::RemoveConsumer(const idStr& tag) { + for (int index = 0; index < consumers.Num(); ++index) { + if (consumers[index] != nullptr + && _stricmp(consumers[index]->consumerTag.c_str(), + tag.c_str()) == 0) { + return consumers.RemoveIndex(index); + } + } + return false; +} + +MQErrors_t idMQChannel::HandleCommand(idMQCommand& command) { + if (command.method == nullptr) return MQERROR_UNEXPECTED_METHOD; + const int classId = command.method->GetClassId(); + const int methodId = command.method->GetMethodId(); + if (classId == AMQP_CLASS_CONNECTION) { + switch (methodId) { + case 10: deferredConnectionStart.SetValue( + *static_cast(command.method)); break; + case 30: deferredConnectionTune.SetValue( + *static_cast(command.method)); break; + case 41: deferredConnectionOpenOk.SetValue( + *static_cast(command.method)); break; + case 60: { + AMQPConnectionCloseOk response; + SendMethod(session, response); + return MQERROR_SERVER_CLOSED; + } + case 61: break; + default: return MQERROR_UNEXPECTED_METHOD; + } + return MQERROR_NONE; + } + if (classId == AMQP_CLASS_CHANNEL) { + switch (methodId) { + case 11: deferredChannelOpenOk.SetValue( + *static_cast(command.method)); break; + case 20: { + AMQPChannelFlow& flow = *static_cast(command.method); + deferredChannelFlow.SetValue(flow); + AMQPChannelFlowOk response; + response.active = flow.active; + SendMethod(session, response); + break; + } + case 21: deferredChannelFlowOk.SetValue( + *static_cast(command.method)); break; + case 40: { + AMQPChannelCloseOk response; + SendMethod(session, response); + return MQERROR_SERVER_CLOSED; + } + case 41: break; + default: return MQERROR_UNEXPECTED_METHOD; + } + return MQERROR_NONE; + } + if (classId == AMQP_CLASS_EXCHANGE) { + if (methodId == 11) deferredExchangeDeclareOk.SetValue( + *static_cast(command.method)); + else if (methodId == 21) deferredExchangeDeleteOk.SetValue( + *static_cast(command.method)); + else return MQERROR_UNEXPECTED_METHOD; + return MQERROR_NONE; + } + if (classId == AMQP_CLASS_QUEUE) { + switch (methodId) { + case 11: deferredQueueDeclareOk.SetValue( + *static_cast(command.method)); break; + case 21: deferredQueueBindOk.SetValue( + *static_cast(command.method)); break; + case 31: deferredQueuePurgeOk.SetValue( + *static_cast(command.method)); break; + case 41: deferredQueueDeleteOk.SetValue( + *static_cast(command.method)); break; + default: return MQERROR_UNEXPECTED_METHOD; + } + return MQERROR_NONE; + } + if (classId == AMQP_CLASS_BASIC) { + switch (methodId) { + case 21: { + AMQPBasicConsumeOk& value = + *static_cast(command.method); + deferredBasicConsumeOk.SetValue(value); + idMQConsumer* consumer = LookupConsumer(value.consumerTag); + if (consumer != nullptr) consumer->ProcessBasicConsumeOk( + value.consumerTag); + break; + } + case 30: { + AMQPBasicCancel& value = + *static_cast(command.method); + idMQConsumer* consumer = LookupConsumer(value.consumerTag); + if (consumer != nullptr) consumer->ProcessBasicCancel( + value.consumerTag); + RemoveConsumer(value.consumerTag); + break; + } + case 31: deferredBasicCancelOk.SetValue( + *static_cast(command.method)); break; + case 60: { + AMQPBasicDeliver& value = + *static_cast(command.method); + idMQConsumer* consumer = LookupConsumer(value.consumerTag); + if (consumer != nullptr) consumer->ProcessBasicDeliver( + value.consumerTag, value.deliveryTag, value.redelivered, + value.exchange, value.routingKey, + static_cast(command.contentHeader), + command.contentBody); + break; + } + case 71: { + AMQPBasicGetOk& value = + *static_cast(command.method); + amqpBasicGetResultOk_t result; + result.deliveryTag = value.deliveryTag; + result.redelivered = value.redelivered; + result.exchange = value.exchange; + result.routingKey = value.routingKey; + result.messageCount = value.messageCount; + result.header = command.contentHeader == nullptr ? nullptr + : new AMQPBasicProperties( + *static_cast(command.contentHeader)); + result.body = command.contentBody; + deferredBasicGetResultOk.SetValue(result); + break; + } + case 111: deferredAMQPBasicRecoverOk.SetValue( + *static_cast(command.method)); break; + case 50: case 72: case 80: case 90: break; + default: return MQERROR_UNEXPECTED_METHOD; + } + return MQERROR_NONE; + } + return MQERROR_UNEXPECTED_CLASS; +} + +void idMQChannel::ConnectionStartOk(AMQPConnectionTune& tune, + const idStr& mechanism, const idMQList& response, + const idStr& locale) { + idScopedCriticalSection lock(protocolWait); + AMQPConnectionStartOk request; + request.mechanism = mechanism; + request.response = response; + request.locale = locale; + SendMethod(session, request); + deferredConnectionTune.GetValue(tune); +} + +void idMQChannel::ConnectionTuneOk(const unsigned short channelMax, + const unsigned int frameMax, const unsigned short heartbeat) { + AMQPConnectionTuneOk request; + request.channelMax = channelMax; + request.frameMax = frameMax; + request.heartbeat = heartbeat; + SendMethod(session, request); +} + +void idMQChannel::ConnectionOpen(idStr& knownHosts, const idStr& vhost, + const idStr& capabilities, const bool insist) { + idScopedCriticalSection lock(protocolWait); + AMQPConnectionOpen request; + request.virtualHost = vhost; + request.capabilities = capabilities; + request.insist = insist; + SendMethod(session, request); + AMQPConnectionOpenOk result; + if (deferredConnectionOpenOk.GetValue(result)) knownHosts = result.knownHosts; +} + +void idMQChannel::ConnectionClose(const unsigned short replyCode, + const idStr& replyText, const unsigned short classId, + const unsigned short methodId) { + AMQPConnectionClose request; + request.replyCode = replyCode; + request.replyText = replyText; + request.cId = classId; + request.mId = methodId; + SendMethod(session, request); +} + +void idMQChannel::ChannelOpen(const idStr& outOfBand) { + idScopedCriticalSection lock(protocolWait); + AMQPChannelOpen request; + request.oob = outOfBand; + SendMethod(session, request); + deferredChannelOpenOk.GetValue(); +} + +void idMQChannel::ExchangeDeclareInternal(const idStr& exchange, + const idStr& type, const bool passive, const bool durable, + const bool autoDelete, const bool internal, const bool noWait) { + idScopedCriticalSection lock(protocolWait); + AMQPExchangeDeclare request; + request.exchange = exchange; + request.type = type; + request.passive = passive; + request.durable = durable; + request.autoDelete = autoDelete; + request.isInternal = internal; + request.noWait = noWait; + SendMethod(session, request); + if (!noWait) deferredExchangeDeclareOk.GetValue(); +} + +void idMQChannel::ExchangeDeclare(const idStr& exchange, const idStr& type, + const bool durable) { + ExchangeDeclareInternal(exchange, type, false, durable, false, false, + false); +} + +void idMQChannel::ExchangeDeclare(const idStr& exchange, const idStr& type, + const bool durable, const bool autoDelete) { + ExchangeDeclareInternal(exchange, type, false, durable, autoDelete, + false, false); +} + +void idMQChannel::BasicPublishInternal(const idStr& exchange, + const idStr& routingKey, const bool mandatory, const bool immediate, + const idMQList& body) { + AMQPBasicPublish method; + method.exchange = exchange; + method.routingKey = routingKey; + method.mandatory = mandatory; + method.immediate = immediate; + AMQPBasicProperties properties; + properties.bodyLength = body.Num(); + idMQCommand command; + command.method = &method; + command.contentHeader = &properties; + command.contentBody = body; + session->SendCommand(command); +} + +void idMQChannel::BasicPublish(const idStr& exchange, + const idStr& routingKey, const idMQList& body) { + BasicPublishInternal(exchange, routingKey, false, false, body); +} + +void idMQChannel::BasicPublish(const idStr& exchange, + const idStr& routingKey, const bool mandatory, const bool immediate, + const void* data, const unsigned int size) { + idMQList body(16); + const unsigned char* bytes = static_cast(data); + for (unsigned int index = 0; index < size; ++index) body.Append(bytes[index]); + BasicPublishInternal(exchange, routingKey, mandatory, immediate, body); +} + +void idMQChannel::Close() { + if (session != nullptr) session->Close(); +} diff --git a/source/shared/idlib/networking/amqp/mqsession.cpp b/source/shared/idlib/networking/amqp/mqsession.cpp new file mode 100644 index 0000000..b288937 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqsession.cpp @@ -0,0 +1,127 @@ +#include "mqmessaging.h" + +#include + +idMQSession::idMQSession(const int number, idMQConnection* owner) + : connection(owner), cmdBuilder(), protocol(nullptr), currentCommand(), + channelNumber(number), isOpen(false) { +} + +idMQSession::~idMQSession() { + currentCommand.Reset(); + delete protocol; +} + +void idMQSession::Init(const bool connectionChannel) { + if (protocol == nullptr) protocol = new idMQChannel(this); + protocol->Init(); + isOpen = true; + if (!connectionChannel) protocol->ChannelOpen(); +} + +void idMQSession::Close() { + Close("client shutdown", MQERROR_NONE, true); +} + +void idMQSession::Close(const char*, const MQErrors_t, const bool notifyServer) { + if (!isOpen) return; + if (notifyServer && channelNumber != 0 && protocol != nullptr) { + AMQPChannelClose request; + request.replyCode = 200; + request.replyText = "Goodbye"; + idMQCommand command; + command.method = &request; + SendCommand(command); + } + isOpen = false; +} + +void idMQSession::HandleFrame(idMQFrame& frame) { + const MQErrors_t error = cmdBuilder.HandleFrame(currentCommand, frame); + if (error != MQERROR_NONE) { + Close("invalid frame", error, false); + currentCommand.Reset(); + return; + } + if (currentCommand.complete) { + const MQErrors_t result = protocol == nullptr + ? MQERROR_UNKNOWN : protocol->HandleCommand(currentCommand); + if (result != MQERROR_NONE && result != MQERROR_SERVER_CLOSED) { + Close("command handling failed", result, false); + } + currentCommand.Reset(); + } +} + +void idMQSession::SendCommand(idMQCommand& command) { + if (connection == nullptr || command.method == nullptr) return; + idMQFrame methodFrame(AMQP_FRAME_METHOD, + static_cast(channelNumber)); + methodFrame.WriteUInt16(command.method->GetClassId()); + methodFrame.WriteUInt16(command.method->GetMethodId()); + command.method->ToFrame(methodFrame); + connection->SendFrame(methodFrame); + + if (!command.method->HasContent() || command.contentHeader == nullptr) return; + command.contentHeader->bodyLength = command.contentBody.Num(); + idMQFrame headerFrame(AMQP_FRAME_HEADER, + static_cast(channelNumber)); + headerFrame.WriteUInt16(command.contentHeader->GetProtocolId()); + headerFrame.WriteUInt16(0); + headerFrame.WriteUInt64(command.contentHeader->bodyLength); + command.contentHeader->ToFrame(headerFrame); + connection->SendFrame(headerFrame); + + const unsigned int maximum = connection->frameMaxSize > 8 + ? connection->frameMaxSize - 8 : 131064; + int offset = 0; + while (offset < command.contentBody.Num()) { + const int amount = std::min(command.contentBody.Num() - offset, + static_cast(maximum)); + idMQFrame bodyFrame(AMQP_FRAME_BODY, + static_cast(channelNumber)); + bodyFrame.WriteData(command.contentBody.Ptr() + offset, amount); + connection->SendFrame(bodyFrame); + offset += amount; + } +} + +idMQSessionManager::idMQSessionManager(idMQConnection* owner) + : initialized(false), connection(owner), maxSessions(0), sessionList() { +} + +idMQSessionManager::~idMQSessionManager() { Shutdown(); } + +void idMQSessionManager::Init(const int maximumSessions) { + Shutdown(); + maxSessions = maximumSessions <= 0 ? 65535 : maximumSessions; + initialized = true; +} + +void idMQSessionManager::Shutdown() { + for (int index = 0; index < sessionList.Num(); ++index) { + delete sessionList[index]; + } + sessionList.Clear(); + initialized = false; +} + +idMQSession* idMQSessionManager::CreateSession() { + if (!initialized || sessionList.Num() >= maxSessions) return nullptr; + int number = 1; + while (Lookup(number) != nullptr) ++number; + idMQSession* session = new idMQSession(number, connection); + sessionList.Append(session); + session->Init(false); + return session; +} + +idMQSession* idMQSessionManager::Lookup(const int number) { + for (int index = 0; index < sessionList.Num(); ++index) { + if (sessionList[index] != nullptr + && sessionList[index]->channelNumber == number) { + return sessionList[index]; + } + } + return nullptr; +} diff --git a/source/shared/idlib/networking/amqp/mqthread.cpp b/source/shared/idlib/networking/amqp/mqthread.cpp new file mode 100644 index 0000000..571e0c4 --- /dev/null +++ b/source/shared/idlib/networking/amqp/mqthread.cpp @@ -0,0 +1,67 @@ +#include "mqcommon.h" + +#ifdef nullptr +#undef nullptr +#endif + +idMQClientThread::idMQClientThread() + : handle(0), signalQuit(false), terminated(true), connection(), + retryTime(0) { +} + +idMQClientThread::~idMQClientThread() { StopThread(true); } + +void idMQClientThread::StopThread(const bool waitForStop) { + signalQuit = true; + connection.Close(); + if (waitForStop && handle != 0) { + Sys_WaitForThread(static_cast(handle)); + Sys_DestroyThread(static_cast(handle)); + handle = 0; + } +} + +void idMQClientThread::Connect(idMQClientThread* thread) { + if (thread == nullptr) return; + amqpEndpoint_t endpoint; + thread->connection.Init(endpoint); + int delay = 1000; + for (int attempt = 0; attempt < 4 && !thread->signalQuit; ++attempt) { + if (thread->connection.Connect()) { + thread->retryTime = 0; + return; + } + thread->retryTime = static_cast(GetTickCount()) + delay; + const int end = thread->retryTime; + while (!thread->signalQuit + && static_cast(GetTickCount()) < end) Sleep(25); + delay += 1000; + } + thread->signalQuit = true; +} + +unsigned int idMQClientThread::Thread(void* value) { + idMQClientThread* thread = static_cast(value); + Connect(thread); + if (!thread->signalQuit) thread->PreRun(); + while (!thread->signalQuit) { + thread->ThreadSlice(); + if (thread->connection.connectionTerminated && !thread->signalQuit) { + Connect(thread); + if (!thread->signalQuit) thread->PreRun(); + } + } + thread->connection.Close(); + thread->OnThreadTerminate(); + thread->terminated = true; + return 0; +} + +void idMQClientThread::StartThread(const char* threadName) { + if (handle != 0) return; + signalQuit = false; + terminated = false; + handle = static_cast(Sys_CreateThread(Thread, this, + THREAD_LOWEST, threadName == nullptr ? "AMQP Client" : threadName, + CORE_ANY, 0x20000, false)); +} diff --git a/source/shared/idlib/networking/protocols/reports.pb.cpp b/source/shared/idlib/networking/protocols/reports.pb.cpp new file mode 100644 index 0000000..5221f27 --- /dev/null +++ b/source/shared/idlib/networking/protocols/reports.pb.cpp @@ -0,0 +1,584 @@ +#include "reports.pb.h" + +#include +#include +#include + +namespace { + +void WriteVarint(std::string& output, std::uint64_t value) { + while (value >= 0x80) { + output.push_back(static_cast((value & 0x7f) | 0x80)); + value >>= 7; + } + output.push_back(static_cast(value)); +} + +void WriteTag(std::string& output, const int number, const int wireType) { + WriteVarint(output, (static_cast(number) << 3) | wireType); +} + +void WriteLengthDelimited(std::string& output, const int number, + const std::string& value) { + WriteTag(output, number, 2); + WriteVarint(output, value.size()); + output.append(value); +} + +bool ReadVarint(const unsigned char*& cursor, const unsigned char* end, + std::uint64_t& value) { + value = 0; + for (int shift = 0; shift < 70 && cursor < end; shift += 7) { + const unsigned char byteValue = *cursor++; + if (shift < 64) value |= static_cast(byteValue & 0x7f) + << shift; + if ((byteValue & 0x80) == 0) return shift < 64 || byteValue <= 1; + } + return false; +} + +bool ReadLength(const unsigned char*& cursor, const unsigned char* end, + const unsigned char*& fieldEnd) { + std::uint64_t size = 0; + if (!ReadVarint(cursor, end, size) + || size > static_cast(end - cursor)) return false; + fieldEnd = cursor + static_cast(size); + return true; +} + +bool SkipField(const int wireType, const unsigned char*& cursor, + const unsigned char* end) { + std::uint64_t ignored = 0; + const unsigned char* fieldEnd = 0; + switch (wireType) { + case 0: return ReadVarint(cursor, end, ignored); + case 1: + if (end - cursor < 8) return false; + cursor += 8; + return true; + case 2: + if (!ReadLength(cursor, end, fieldEnd)) return false; + cursor = fieldEnd; + return true; + case 5: + if (end - cursor < 4) return false; + cursor += 4; + return true; + default: return false; + } +} + +std::shared_ptr CloneMessage( + const std::shared_ptr& source) { + if (!source) return std::shared_ptr(); + std::shared_ptr result(source->New()); + std::string data; + if (!source->SerializePartialToString(&data) + || !result->ParseFromString(data)) { + return std::shared_ptr(); + } + return result; +} + +} // namespace + +namespace google { +namespace protobuf { + +bool MessageLite::SerializeToArray(void* data, const int size) const { + return IsInitialized() && SerializePartialToArray(data, size); +} + +bool MessageLite::SerializePartialToString(std::string* output) const { + if (output == 0) return false; + const int size = ByteSize(); + output->resize(size); + return size == 0 || SerializePartialToArray(&(*output)[0], size); +} + +bool MessageLite::SerializeToString(std::string* output) const { + return IsInitialized() && SerializePartialToString(output); +} + +std::string MessageLite::SerializeAsString() const { + std::string output; + SerializeToString(&output); + return output; +} + +bool MessageLite::ParseFromString(const std::string& input) { + return ParseFromArray(input.data(), static_cast(input.size())); +} + +} // namespace protobuf +} // namespace google + +namespace idreports { + +bool LogEvent_Severity_IsValid(const int value) { + return value == 80 || value == 70 || value == 60 || value == 50 + || value == 40 || value == 30 || value == 20 || value == 10 + || value == 0; +} + +const char* LogEvent_Severity_Name(const LogEvent_Severity value) { + switch (value) { + case LogEvent_Severity_SEV_EMERGENCY: return "SEV_EMERGENCY"; + case LogEvent_Severity_SEV_ALERT: return "SEV_ALERT"; + case LogEvent_Severity_SEV_CRITICAL: return "SEV_CRITICAL"; + case LogEvent_Severity_SEV_ERROR: return "SEV_ERROR"; + case LogEvent_Severity_SEV_WARNING: return "SEV_WARNING"; + case LogEvent_Severity_SEV_NOTICE: return "SEV_NOTICE"; + case LogEvent_Severity_SEV_INFO: return "SEV_INFO"; + case LogEvent_Severity_SEV_DEBUG: return "SEV_DEBUG"; + case LogEvent_Severity_SEV_TRASH: return "SEV_TRASH"; + default: return "UNKNOWN"; + } +} + +bool TargetPlatform_IsValid(const int value) { + return value >= TARGET_WIN32 && value <= TARGET_LINUX; +} + +const char* TargetPlatform_Name(const TargetPlatform value) { + static const char* const names[] = { + "TARGET_WIN32", "TARGET_X64", "TARGET_XBOX360", "TARGET_PS3", + "TARGET_DURANGO", "TARGET_WIIU", "TARGET_IPHONE", "TARGET_OSX", + "TARGET_LINUX" + }; + return TargetPlatform_IsValid(value) ? names[value] : "UNKNOWN"; +} + +namespace internal { + +FieldValue::FieldValue() + : kind(FIELD_UNKNOWN), integer(0), real(0.0f), text(), message(), + strings(), integers(), messages() { +} + +ReportMessage::ReportMessage() : fields_() { +} + +ReportMessage::ReportMessage(const ReportMessage& other) : fields_() { + MergeFrom(other); +} + +ReportMessage& ReportMessage::operator=(const ReportMessage& other) { + if (this != &other) CopyFrom(other); + return *this; +} + +ReportMessage::~ReportMessage() { +} + +void ReportMessage::Clear() { + fields_.clear(); +} + +bool ReportMessage::HasField(const int number) const { + return fields_.find(number) != fields_.end(); +} + +void ReportMessage::ClearField(const int number) { + fields_.erase(number); +} + +std::uint64_t ReportMessage::GetInteger(const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0 : found->second.integer; +} + +void ReportMessage::SetInteger(const int number, const FieldKind kind, + const std::uint64_t value) { + FieldValue& field = fields_[number]; + field.kind = kind; + field.integer = value; +} + +float ReportMessage::GetFloat(const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0.0f : found->second.real; +} + +void ReportMessage::SetFloat(const int number, const float value) { + FieldValue& field = fields_[number]; + field.kind = FIELD_FLOAT; + field.real = value; +} + +const std::string& ReportMessage::GetString(const int number) const { + static const std::string empty; + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? empty : found->second.text; +} + +std::string* ReportMessage::MutableString(const int number) { + FieldValue& field = fields_[number]; + field.kind = FIELD_STRING; + return &field.text; +} + +std::string* ReportMessage::ReleaseString(const int number) { + const std::map::iterator found = fields_.find(number); + if (found == fields_.end()) return 0; + std::string* result = new std::string(found->second.text); + fields_.erase(found); + return result; +} + +void ReportMessage::SetString(const int number, const char* value, + const std::size_t size) { + FieldValue& field = fields_[number]; + field.kind = FIELD_STRING; + field.text.assign(value == 0 ? "" : value, value == 0 ? 0 : size); +} + +int ReportMessage::RepeatedIntegerSize(const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0 + : static_cast(found->second.integers.size()); +} + +std::uint64_t ReportMessage::GetRepeatedInteger(const int number, + const int index) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() || index < 0 + || index >= static_cast(found->second.integers.size()) + ? 0 : found->second.integers[index]; +} + +void ReportMessage::SetRepeatedInteger(const int number, const int index, + const FieldKind kind, const std::uint64_t value) { + FieldValue& field = fields_[number]; + field.kind = kind; + if (index >= 0 && index < static_cast(field.integers.size())) { + field.integers[index] = value; + } +} + +void ReportMessage::AddRepeatedInteger(const int number, const FieldKind kind, + const std::uint64_t value) { + FieldValue& field = fields_[number]; + field.kind = kind; + field.integers.push_back(value); +} + +int ReportMessage::RepeatedStringSize(const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0 + : static_cast(found->second.strings.size()); +} + +const std::string& ReportMessage::GetRepeatedString(const int number, + const int index) const { + static const std::string empty; + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() || index < 0 + || index >= static_cast(found->second.strings.size()) + ? empty : found->second.strings[index]; +} + +std::string* ReportMessage::MutableRepeatedString(const int number, + const int index) { + const std::map::iterator found = fields_.find(number); + return found == fields_.end() || index < 0 + || index >= static_cast(found->second.strings.size()) + ? 0 : &found->second.strings[index]; +} + +std::string* ReportMessage::AddRepeatedString(const int number) { + FieldValue& field = fields_[number]; + field.kind = FIELD_REPEATED_STRING; + field.strings.push_back(std::string()); + return &field.strings.back(); +} + +void ReportMessage::SetRepeatedString(const int number, const int index, + const char* value, const std::size_t size) { + std::string* item = MutableRepeatedString(number, index); + if (item != 0) item->assign(value == 0 ? "" : value, + value == 0 ? 0 : size); +} + +int ReportMessage::RepeatedMessageSize(const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0 + : static_cast(found->second.messages.size()); +} + +const google::protobuf::MessageLite* ReportMessage::GetRepeatedMessage( + const int number, const int index) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() || index < 0 + || index >= static_cast(found->second.messages.size()) + ? 0 : found->second.messages[index].get(); +} + +google::protobuf::MessageLite* ReportMessage::MutableRepeatedMessage( + const int number, const int index) { + const std::map::iterator found = fields_.find(number); + return found == fields_.end() || index < 0 + || index >= static_cast(found->second.messages.size()) + ? 0 : found->second.messages[index].get(); +} + +google::protobuf::MessageLite* ReportMessage::AddRepeatedMessage( + const int number) { + std::shared_ptr value( + NewMessageForField(number)); + if (!value) return 0; + FieldValue& field = fields_[number]; + field.kind = FIELD_REPEATED_MESSAGE; + field.messages.push_back(value); + return value.get(); +} + +const google::protobuf::MessageLite* ReportMessage::GetMessage( + const int number) const { + const std::map::const_iterator found = fields_.find(number); + return found == fields_.end() ? 0 : found->second.message.get(); +} + +google::protobuf::MessageLite* ReportMessage::MutableMessage(const int number) { + FieldValue& field = fields_[number]; + field.kind = FIELD_MESSAGE; + if (!field.message) field.message.reset(NewMessageForField(number)); + return field.message.get(); +} + +google::protobuf::MessageLite* ReportMessage::ReleaseMessage(const int number) { + const std::map::iterator found = fields_.find(number); + if (found == fields_.end() || !found->second.message) return 0; + google::protobuf::MessageLite* result = found->second.message->New(); + std::string data; + found->second.message->SerializePartialToString(&data); + result->ParseFromString(data); + fields_.erase(found); + return result; +} + +void ReportMessage::SetAllocatedMessage(const int number, + google::protobuf::MessageLite* value) { + if (value == 0) { + ClearField(number); + return; + } + FieldValue& field = fields_[number]; + field.kind = FIELD_MESSAGE; + field.message.reset(value); +} + +google::protobuf::MessageLite* ReportMessage::NewMessageForField(int) const { + return 0; +} + +bool ReportMessage::IsInitialized() const { + if (!HasRequiredFields()) return false; + for (std::map::const_iterator field = fields_.begin(); + field != fields_.end(); ++field) { + if (field->second.message && !field->second.message->IsInitialized()) { + return false; + } + for (std::size_t index = 0; index < field->second.messages.size(); + ++index) { + if (field->second.messages[index] + && !field->second.messages[index]->IsInitialized()) { + return false; + } + } + } + return true; +} + +bool ReportMessage::Serialize(std::string& output) const { + output.clear(); + for (std::map::const_iterator iterator = fields_.begin(); + iterator != fields_.end(); ++iterator) { + const int number = iterator->first; + const FieldValue& field = iterator->second; + switch (field.kind) { + case FIELD_STRING: + WriteLengthDelimited(output, number, field.text); + break; + case FIELD_UINT32: + case FIELD_UINT64: + case FIELD_INT32: + case FIELD_INT64: + case FIELD_ENUM: + case FIELD_BOOL: + WriteTag(output, number, 0); + WriteVarint(output, field.integer); + break; + case FIELD_FLOAT: { + WriteTag(output, number, 5); + std::uint32_t bits = 0; + std::memcpy(&bits, &field.real, sizeof(bits)); + output.push_back(static_cast(bits)); + output.push_back(static_cast(bits >> 8)); + output.push_back(static_cast(bits >> 16)); + output.push_back(static_cast(bits >> 24)); + break; + } + case FIELD_MESSAGE: + if (field.message) { + std::string nested; + field.message->SerializePartialToString(&nested); + WriteLengthDelimited(output, number, nested); + } + break; + case FIELD_REPEATED_STRING: + for (std::size_t index = 0; index < field.strings.size(); ++index) { + WriteLengthDelimited(output, number, field.strings[index]); + } + break; + case FIELD_REPEATED_UINT32: + case FIELD_REPEATED_UINT64: + for (std::size_t index = 0; index < field.integers.size(); ++index) { + WriteTag(output, number, 0); + WriteVarint(output, field.integers[index]); + } + break; + case FIELD_REPEATED_MESSAGE: + for (std::size_t index = 0; index < field.messages.size(); ++index) { + if (field.messages[index]) { + std::string nested; + field.messages[index]->SerializePartialToString(&nested); + WriteLengthDelimited(output, number, nested); + } + } + break; + default: break; + } + } + return true; +} + +int ReportMessage::ByteSize() const { + std::string output; + Serialize(output); + return static_cast(output.size()); +} + +bool ReportMessage::SerializePartialToArray(void* data, const int size) const { + if (size < 0 || (data == 0 && size != 0)) return false; + std::string output; + Serialize(output); + if (size < static_cast(output.size())) return false; + if (!output.empty()) std::memcpy(data, output.data(), output.size()); + return true; +} + +bool ReportMessage::ParseFromArray(const void* data, const int size) { + if (size < 0 || (data == 0 && size != 0)) return false; + Clear(); + const unsigned char* cursor = static_cast(data); + const unsigned char* const end = cursor + size; + while (cursor < end) { + std::uint64_t tag = 0; + if (!ReadVarint(cursor, end, tag) || tag == 0) return false; + const int number = static_cast(tag >> 3); + const int wireType = static_cast(tag & 7); + const FieldKind kind = FieldKindForNumber(number); + if (kind == FIELD_UNKNOWN) { + if (!SkipField(wireType, cursor, end)) return false; + continue; + } + + std::uint64_t integer = 0; + const unsigned char* fieldEnd = 0; + if (kind == FIELD_STRING || kind == FIELD_REPEATED_STRING + || kind == FIELD_MESSAGE || kind == FIELD_REPEATED_MESSAGE) { + if (wireType != 2 || !ReadLength(cursor, end, fieldEnd)) return false; + if (kind == FIELD_STRING) { + SetString(number, reinterpret_cast(cursor), + static_cast(fieldEnd - cursor)); + } else if (kind == FIELD_REPEATED_STRING) { + std::string* item = AddRepeatedString(number); + item->assign(reinterpret_cast(cursor), + static_cast(fieldEnd - cursor)); + } else { + google::protobuf::MessageLite* nested = kind == FIELD_MESSAGE + ? MutableMessage(number) : AddRepeatedMessage(number); + if (nested == 0 || !nested->ParseFromArray(cursor, + static_cast(fieldEnd - cursor))) return false; + } + cursor = fieldEnd; + } else if (kind == FIELD_FLOAT) { + if (wireType != 5 || end - cursor < 4) return false; + const std::uint32_t bits = static_cast(cursor[0]) + | (static_cast(cursor[1]) << 8) + | (static_cast(cursor[2]) << 16) + | (static_cast(cursor[3]) << 24); + float value = 0.0f; + std::memcpy(&value, &bits, sizeof(value)); + SetFloat(number, value); + cursor += 4; + } else if ((kind == FIELD_REPEATED_UINT32 + || kind == FIELD_REPEATED_UINT64) && wireType == 2) { + if (!ReadLength(cursor, end, fieldEnd)) return false; + while (cursor < fieldEnd) { + if (!ReadVarint(cursor, fieldEnd, integer)) return false; + AddRepeatedInteger(number, kind, integer); + } + } else { + if (wireType != 0 || !ReadVarint(cursor, end, integer)) return false; + if (kind == FIELD_REPEATED_UINT32 + || kind == FIELD_REPEATED_UINT64) { + AddRepeatedInteger(number, kind, integer); + } else { + SetInteger(number, kind, integer); + } + } + } + return true; +} + +void ReportMessage::CopyFrom(const google::protobuf::MessageLite& other) { + Clear(); + MergeFrom(other); +} + +void ReportMessage::MergeFrom(const google::protobuf::MessageLite& other) { + const ReportMessage* source = dynamic_cast(&other); + if (source == 0 || source == this || source->GetTypeName() != GetTypeName()) { + return; + } + for (std::map::const_iterator iterator = + source->fields_.begin(); iterator != source->fields_.end(); ++iterator) { + const FieldValue& incoming = iterator->second; + FieldValue& destination = fields_[iterator->first]; + if (incoming.kind == FIELD_REPEATED_STRING) { + destination.kind = incoming.kind; + destination.strings.insert(destination.strings.end(), + incoming.strings.begin(), incoming.strings.end()); + } else if (incoming.kind == FIELD_REPEATED_UINT32 + || incoming.kind == FIELD_REPEATED_UINT64) { + destination.kind = incoming.kind; + destination.integers.insert(destination.integers.end(), + incoming.integers.begin(), incoming.integers.end()); + } else if (incoming.kind == FIELD_REPEATED_MESSAGE) { + destination.kind = incoming.kind; + for (std::size_t index = 0; index < incoming.messages.size(); ++index) { + destination.messages.push_back(CloneMessage(incoming.messages[index])); + } + } else { + destination = incoming; + if (incoming.message) destination.message = CloneMessage(incoming.message); + } + } +} + +void ReportMessage::Swap(ReportMessage* other) { + if (other != 0) fields_.swap(other->fields_); +} + +} // namespace internal + +void protobuf_AddDesc_reports_2eproto() { +} + +void protobuf_ShutdownFile_reports_2eproto() { +} + +} // namespace idreports + diff --git a/source/shared/idlib/networking/protocols/reports.pb.h b/source/shared/idlib/networking/protocols/reports.pb.h new file mode 100644 index 0000000..963bab7 --- /dev/null +++ b/source/shared/idlib/networking/protocols/reports.pb.h @@ -0,0 +1,646 @@ +#pragma once + +// Reconstructed from the protobuf 2.4 generated code in the Tungsten Xbox 360 +// image. This keeps the original protobuf-lite surface and wire format while +// avoiding a dependency on the obsolete 2.4 runtime in the Windows port. + +#include +#include +#include +#include +#include +#include + +namespace google { +namespace protobuf { + +class MessageLite { +public: + virtual ~MessageLite() {} + virtual std::string GetTypeName() const = 0; + virtual MessageLite* New() const = 0; + virtual void Clear() = 0; + virtual bool IsInitialized() const = 0; + virtual int ByteSize() const = 0; + virtual int GetCachedSize() const { return ByteSize(); } + virtual bool SerializePartialToArray(void* data, int size) const = 0; + virtual bool ParseFromArray(const void* data, int size) = 0; + + bool SerializeToArray(void* data, int size) const; + bool SerializePartialToString(std::string* output) const; + bool SerializeToString(std::string* output) const; + std::string SerializeAsString() const; + bool ParseFromString(const std::string& input); +}; + +} // namespace protobuf +} // namespace google + +namespace idreports { + +enum LogEvent_Severity { + LogEvent_Severity_SEV_EMERGENCY = 80, + LogEvent_Severity_SEV_ALERT = 70, + LogEvent_Severity_SEV_CRITICAL = 60, + LogEvent_Severity_SEV_ERROR = 50, + LogEvent_Severity_SEV_WARNING = 40, + LogEvent_Severity_SEV_NOTICE = 30, + LogEvent_Severity_SEV_INFO = 20, + LogEvent_Severity_SEV_DEBUG = 10, + LogEvent_Severity_SEV_TRASH = 0 +}; + +bool LogEvent_Severity_IsValid(int value); +const char* LogEvent_Severity_Name(LogEvent_Severity value); + +enum TargetPlatform { + TARGET_WIN32 = 0, + TARGET_X64 = 1, + TARGET_XBOX360 = 2, + TARGET_PS3 = 3, + TARGET_DURANGO = 4, + TARGET_WIIU = 5, + TARGET_IPHONE = 6, + TARGET_OSX = 7, + TARGET_LINUX = 8 +}; + +bool TargetPlatform_IsValid(int value); +const char* TargetPlatform_Name(TargetPlatform value); + +enum HeapType { + UNKNOWN_HEAP = 0, + MAP_HEAP = 1, + SYSTEM_HEAP = 2 +}; + +enum Xbox360ConsoleType { + XBOX360_DEVELOPMENT_KIT = 0, + XBOX360_TEST_KIT = 1, + XBOX360_UNKNOWN_KIT = 2 +}; + +class BigUInt; +class Attachment; +class LogEvent; +class MemoryInfo; +class ExceptionInfo; +class CallstackLine; +class AssertReport; +class MapWarning; +class MapReport; +class ViewNoteReport; +class StringIDReferenceReport; +class CPUInfo; +class RegisterInfo; +class HeapCategoryUsage; +class HeapUsage; +class Xbox360VersionInfo; +class Xbox360KitInfo; +class XenonRegisterInfo; +class XenonMemoryInfo; +class PPURegisterInfo; +class SPURegisterInfo; +class PS3ExceptionInfo; +class PS3VMRegionStats; +class PS3MemoryInfo; +class CrashReport; +class Xbox360CrashReport; +class PS3CrashReport; +class CrashReportResult; + +namespace internal { + +enum FieldKind { + FIELD_UNKNOWN, + FIELD_STRING, + FIELD_UINT32, + FIELD_UINT64, + FIELD_INT32, + FIELD_INT64, + FIELD_ENUM, + FIELD_BOOL, + FIELD_FLOAT, + FIELD_MESSAGE, + FIELD_REPEATED_STRING, + FIELD_REPEATED_UINT32, + FIELD_REPEATED_UINT64, + FIELD_REPEATED_MESSAGE +}; + +struct FieldValue { + FieldValue(); + FieldKind kind; + std::uint64_t integer; + float real; + std::string text; + std::shared_ptr message; + std::vector strings; + std::vector integers; + std::vector > messages; +}; + +class ReportMessage : public google::protobuf::MessageLite { +public: + ReportMessage(); + ReportMessage(const ReportMessage& other); + ReportMessage& operator=(const ReportMessage& other); + virtual ~ReportMessage(); + + void Clear() override; + bool IsInitialized() const override; + int ByteSize() const override; + bool SerializePartialToArray(void* data, int size) const override; + bool ParseFromArray(const void* data, int size) override; + + void CopyFrom(const google::protobuf::MessageLite& other); + void MergeFrom(const google::protobuf::MessageLite& other); + void Swap(ReportMessage* other); + +protected: + bool HasField(int number) const; + void ClearField(int number); + std::uint64_t GetInteger(int number) const; + void SetInteger(int number, FieldKind kind, std::uint64_t value); + float GetFloat(int number) const; + void SetFloat(int number, float value); + const std::string& GetString(int number) const; + std::string* MutableString(int number); + std::string* ReleaseString(int number); + void SetString(int number, const char* value, std::size_t size); + int RepeatedIntegerSize(int number) const; + std::uint64_t GetRepeatedInteger(int number, int index) const; + void SetRepeatedInteger(int number, int index, FieldKind kind, + std::uint64_t value); + void AddRepeatedInteger(int number, FieldKind kind, std::uint64_t value); + int RepeatedStringSize(int number) const; + const std::string& GetRepeatedString(int number, int index) const; + std::string* MutableRepeatedString(int number, int index); + std::string* AddRepeatedString(int number); + void SetRepeatedString(int number, int index, const char* value, + std::size_t size); + int RepeatedMessageSize(int number) const; + const google::protobuf::MessageLite* GetRepeatedMessage(int number, + int index) const; + google::protobuf::MessageLite* MutableRepeatedMessage(int number, + int index); + google::protobuf::MessageLite* AddRepeatedMessage(int number); + const google::protobuf::MessageLite* GetMessage(int number) const; + google::protobuf::MessageLite* MutableMessage(int number); + google::protobuf::MessageLite* ReleaseMessage(int number); + void SetAllocatedMessage(int number, google::protobuf::MessageLite* value); + + virtual FieldKind FieldKindForNumber(int number) const = 0; + virtual google::protobuf::MessageLite* NewMessageForField(int number) const; + virtual bool HasRequiredFields() const = 0; + +private: + bool Serialize(std::string& output) const; + std::map fields_; +}; + +} // namespace internal + +#define IDR_DECL_string(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + const std::string& name() const { return GetString(number); } \ + void set_##name(const std::string& value) { SetString(number, value.data(), value.size()); } \ + void set_##name(const char* value) { SetString(number, value ? value : "", value ? std::char_traits::length(value) : 0); } \ + void set_##name(const char* value, std::size_t size) { SetString(number, value, size); } \ + std::string* mutable_##name() { return MutableString(number); } \ + std::string* release_##name() { return ReleaseString(number); } \ + void set_allocated_##name(std::string* value) { if (value) { set_##name(*value); delete value; } else { clear_##name(); } } + +#define IDR_DECL_uint32(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + std::uint32_t name() const { return static_cast(GetInteger(number)); } \ + void set_##name(std::uint32_t value) { SetInteger(number, internal::FIELD_UINT32, value); } + +#define IDR_DECL_uint64(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + std::uint64_t name() const { return GetInteger(number); } \ + void set_##name(std::uint64_t value) { SetInteger(number, internal::FIELD_UINT64, value); } + +#define IDR_DECL_int32(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + std::int32_t name() const { return static_cast(GetInteger(number)); } \ + void set_##name(std::int32_t value) { SetInteger(number, internal::FIELD_INT32, static_cast(static_cast(value))); } + +#define IDR_DECL_int64(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + std::int64_t name() const { return static_cast(GetInteger(number)); } \ + void set_##name(std::int64_t value) { SetInteger(number, internal::FIELD_INT64, static_cast(value)); } + +#define IDR_DECL_enum(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + type name() const { return static_cast(GetInteger(number)); } \ + void set_##name(type value) { SetInteger(number, internal::FIELD_ENUM, static_cast(value)); } + +#define IDR_DECL_bool(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + bool name() const { return GetInteger(number) != 0; } \ + void set_##name(bool value) { SetInteger(number, internal::FIELD_BOOL, value ? 1 : 0); } + +#define IDR_DECL_float(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + float name() const { return GetFloat(number); } \ + void set_##name(float value) { SetFloat(number, value); } + +#define IDR_DECL_message(name, number, type) \ + bool has_##name() const { return HasField(number); } \ + void clear_##name() { ClearField(number); } \ + const type& name() const { const google::protobuf::MessageLite* value = GetMessage(number); return value ? *static_cast(value) : type::default_instance(); } \ + type* mutable_##name() { return static_cast(MutableMessage(number)); } \ + type* release_##name() { return static_cast(ReleaseMessage(number)); } \ + void set_allocated_##name(type* value) { SetAllocatedMessage(number, value); } + +#define IDR_DECL_repeated_string(name, number, type) \ + int name##_size() const { return RepeatedStringSize(number); } \ + void clear_##name() { ClearField(number); } \ + const std::string& name(int index) const { return GetRepeatedString(number, index); } \ + std::string* mutable_##name(int index) { return MutableRepeatedString(number, index); } \ + std::string* add_##name() { return AddRepeatedString(number); } \ + void add_##name(const std::string& value) { std::string* item = AddRepeatedString(number); *item = value; } \ + void add_##name(const char* value) { std::string* item = AddRepeatedString(number); *item = value ? value : ""; } \ + void set_##name(int index, const std::string& value) { SetRepeatedString(number, index, value.data(), value.size()); } \ + void set_##name(int index, const char* value) { SetRepeatedString(number, index, value ? value : "", value ? std::char_traits::length(value) : 0); } + +#define IDR_DECL_repeated_uint32(name, number, type) \ + int name##_size() const { return RepeatedIntegerSize(number); } \ + void clear_##name() { ClearField(number); } \ + std::uint32_t name(int index) const { return static_cast(GetRepeatedInteger(number, index)); } \ + void set_##name(int index, std::uint32_t value) { SetRepeatedInteger(number, index, internal::FIELD_REPEATED_UINT32, value); } \ + void add_##name(std::uint32_t value) { AddRepeatedInteger(number, internal::FIELD_REPEATED_UINT32, value); } + +#define IDR_DECL_repeated_uint64(name, number, type) \ + int name##_size() const { return RepeatedIntegerSize(number); } \ + void clear_##name() { ClearField(number); } \ + std::uint64_t name(int index) const { return GetRepeatedInteger(number, index); } \ + void set_##name(int index, std::uint64_t value) { SetRepeatedInteger(number, index, internal::FIELD_REPEATED_UINT64, value); } \ + void add_##name(std::uint64_t value) { AddRepeatedInteger(number, internal::FIELD_REPEATED_UINT64, value); } + +#define IDR_DECL_repeated_message(name, number, type) \ + int name##_size() const { return RepeatedMessageSize(number); } \ + void clear_##name() { ClearField(number); } \ + const type& name(int index) const { return *static_cast(GetRepeatedMessage(number, index)); } \ + type* mutable_##name(int index) { return static_cast(MutableRepeatedMessage(number, index)); } \ + type* add_##name() { return static_cast(AddRepeatedMessage(number)); } + +#define IDR_KIND_string(name, number, type) case number: return internal::FIELD_STRING; +#define IDR_KIND_uint32(name, number, type) case number: return internal::FIELD_UINT32; +#define IDR_KIND_uint64(name, number, type) case number: return internal::FIELD_UINT64; +#define IDR_KIND_int32(name, number, type) case number: return internal::FIELD_INT32; +#define IDR_KIND_int64(name, number, type) case number: return internal::FIELD_INT64; +#define IDR_KIND_enum(name, number, type) case number: return internal::FIELD_ENUM; +#define IDR_KIND_bool(name, number, type) case number: return internal::FIELD_BOOL; +#define IDR_KIND_float(name, number, type) case number: return internal::FIELD_FLOAT; +#define IDR_KIND_message(name, number, type) case number: return internal::FIELD_MESSAGE; +#define IDR_KIND_repeated_string(name, number, type) case number: return internal::FIELD_REPEATED_STRING; +#define IDR_KIND_repeated_uint32(name, number, type) case number: return internal::FIELD_REPEATED_UINT32; +#define IDR_KIND_repeated_uint64(name, number, type) case number: return internal::FIELD_REPEATED_UINT64; +#define IDR_KIND_repeated_message(name, number, type) case number: return internal::FIELD_REPEATED_MESSAGE; + +#define IDR_FACTORY_string(name, number, type) +#define IDR_FACTORY_uint32(name, number, type) +#define IDR_FACTORY_uint64(name, number, type) +#define IDR_FACTORY_int32(name, number, type) +#define IDR_FACTORY_int64(name, number, type) +#define IDR_FACTORY_enum(name, number, type) +#define IDR_FACTORY_bool(name, number, type) +#define IDR_FACTORY_float(name, number, type) +#define IDR_FACTORY_repeated_string(name, number, type) +#define IDR_FACTORY_repeated_uint32(name, number, type) +#define IDR_FACTORY_repeated_uint64(name, number, type) +#define IDR_FACTORY_message(name, number, type) case number: return new type; +#define IDR_FACTORY_repeated_message(name, number, type) case number: return new type; + +#define IDR_REQUIRE(kind, name, number, type) if (!HasField(number)) return false; +#define IDR_DECLARE(kind, name, number, type) IDR_DECL_##kind(name, number, type) +#define IDR_KIND(kind, name, number, type) IDR_KIND_##kind(name, number, type) +#define IDR_FACTORY(kind, name, number, type) IDR_FACTORY_##kind(name, number, type) + +#define IDR_CLASS(name, fields, required, extras) \ +class name : public internal::ReportMessage { \ +public: \ + name() {} \ + name(const name& other) : internal::ReportMessage() { MergeFrom(other); } \ + name& operator=(const name& other) { if (this != &other) CopyFrom(other); return *this; } \ + std::string GetTypeName() const override { return "idreports." #name; } \ + name* New() const override { return new name; } \ + static const name& default_instance() { static const name value; return value; } \ + void CopyFrom(const name& other) { internal::ReportMessage::CopyFrom(other); } \ + void MergeFrom(const name& other) { internal::ReportMessage::MergeFrom(other); } \ + void Swap(name* other) { internal::ReportMessage::Swap(other); } \ + fields(IDR_DECLARE) \ + extras \ +protected: \ + internal::FieldKind FieldKindForNumber(int number) const override { switch (number) { fields(IDR_KIND) default: return internal::FIELD_UNKNOWN; } } \ + google::protobuf::MessageLite* NewMessageForField(int number) const override { switch (number) { case -1: return 0; fields(IDR_FACTORY) default: return 0; } } \ + bool HasRequiredFields() const override { required(IDR_REQUIRE) return true; } \ +}; + +#define IDR_NO_FIELDS(F) +#define IDR_NO_EXTRAS + +#define IDR_BIGUINT_FIELDS(F) \ + F(uint64, lowpart, 1, int) \ + F(uint64, highpart, 2, int) +#define IDR_BIGUINT_REQUIRED(F) IDR_BIGUINT_FIELDS(F) +IDR_CLASS(BigUInt, IDR_BIGUINT_FIELDS, IDR_BIGUINT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_ATTACHMENT_FIELDS(F) \ + F(string, attachment, 1, int) \ + F(string, filename, 2, int) +#define IDR_ATTACHMENT_REQUIRED(F) IDR_ATTACHMENT_FIELDS(F) +IDR_CLASS(Attachment, IDR_ATTACHMENT_FIELDS, IDR_ATTACHMENT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_MEMORYINFO_FIELDS(F) \ + F(uint32, inuse, 1, int) F(uint32, physicalmb, 2, int) \ + F(uint32, physicalfree, 3, int) F(uint32, pagingfile, 4, int) \ + F(uint32, pagingfree, 5, int) F(uint32, useraddress, 6, int) \ + F(uint32, userfree, 7, int) +#define IDR_MEMORYINFO_REQUIRED(F) IDR_MEMORYINFO_FIELDS(F) +IDR_CLASS(MemoryInfo, IDR_MEMORYINFO_FIELDS, IDR_MEMORYINFO_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_EXCEPTIONINFO_FIELDS(F) \ + F(string, expcode, 1, int) F(enum, expflags, 2, int) \ + F(string, expaddress, 3, int) +#define IDR_EXCEPTIONINFO_REQUIRED(F) IDR_EXCEPTIONINFO_FIELDS(F) +IDR_CLASS(ExceptionInfo, IDR_EXCEPTIONINFO_FIELDS, IDR_EXCEPTIONINFO_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_CALLSTACKLINE_FIELDS(F) \ + F(string, functioncall, 1, int) F(int32, line, 2, int) \ + F(int32, bytepos, 3, int) F(string, filename, 4, int) \ + F(string, address, 5, int) +#define IDR_CALLSTACKLINE_REQUIRED(F) IDR_CALLSTACKLINE_FIELDS(F) +IDR_CLASS(CallstackLine, IDR_CALLSTACKLINE_FIELDS, IDR_CALLSTACKLINE_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_MAPWARNING_FIELDS(F) \ + F(string, message, 1, int) F(repeated_uint32, marks, 2, int) \ + F(uint32, count, 3, int) +#define IDR_MAPWARNING_REQUIRED(F) F(string, message, 1, int) +IDR_CLASS(MapWarning, IDR_MAPWARNING_FIELDS, IDR_MAPWARNING_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_CPUINFO_FIELDS(F) \ + F(string, cpuid, 1, int) F(int32, packages, 2, int) \ + F(int32, cores, 3, int) F(int32, logical, 4, int) \ + F(float, frequency, 5, int) +#define IDR_CPUINFO_REQUIRED(F) IDR_CPUINFO_FIELDS(F) +IDR_CLASS(CPUInfo, IDR_CPUINFO_FIELDS, IDR_CPUINFO_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_REGISTERINFO_FIELDS(F) \ + F(string, edi, 1, int) F(string, esi, 2, int) F(string, eax, 3, int) \ + F(string, ebx, 4, int) F(string, ecx, 5, int) F(string, edx, 6, int) \ + F(string, eip, 7, int) F(string, ebp, 8, int) F(string, esp, 9, int) \ + F(string, eflags, 10, int) F(string, segcs, 11, int) \ + F(string, segss, 12, int) F(enum, platform, 13, TargetPlatform) +#define IDR_REGISTERINFO_REQUIRED(F) IDR_REGISTERINFO_FIELDS(F) +IDR_CLASS(RegisterInfo, IDR_REGISTERINFO_FIELDS, IDR_REGISTERINFO_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_HEAPCATEGORY_FIELDS(F) \ + F(string, categoryname, 1, int) F(uint32, totalever, 2, int) \ + F(uint32, totalcurrent, 3, int) F(uint32, totalsize, 4, int) \ + F(uint32, totalwaste, 5, int) +#define IDR_HEAPCATEGORY_REQUIRED(F) IDR_HEAPCATEGORY_FIELDS(F) +IDR_CLASS(HeapCategoryUsage, IDR_HEAPCATEGORY_FIELDS, IDR_HEAPCATEGORY_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_XBOXVERSION_FIELDS(F) \ + F(uint32, major, 1, int) F(uint32, minor, 2, int) \ + F(uint32, build, 3, int) F(uint32, qfe, 4, int) +#define IDR_XBOXVERSION_REQUIRED(F) IDR_XBOXVERSION_FIELDS(F) +IDR_CLASS(Xbox360VersionInfo, IDR_XBOXVERSION_FIELDS, IDR_XBOXVERSION_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_XENONMEMORY_FIELDS(F) \ + F(uint32, totalpages, 1, int) F(uint32, availablepages, 2, int) \ + F(uint32, stackpages, 3, int) F(uint32, virtualpagetablepages, 4, int) \ + F(uint32, systempagetablepages, 5, int) F(uint32, poolpages, 6, int) \ + F(uint32, virtualmappedpages, 7, int) F(uint32, imagepages, 8, int) \ + F(uint32, filecachepages, 9, int) F(uint32, contiguouspages, 10, int) \ + F(uint32, debuggerpages, 11, int) +#define IDR_XENONMEMORY_REQUIRED(F) IDR_XENONMEMORY_FIELDS(F) +IDR_CLASS(XenonMemoryInfo, IDR_XENONMEMORY_FIELDS, IDR_XENONMEMORY_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_PS3EXCEPTION_FIELDS(F) \ + F(string, exceptionname, 1, int) F(uint64, exceptioncode, 2, int) \ + F(uint64, dar, 3, int) +#define IDR_PS3EXCEPTION_REQUIRED(F) \ + F(string, exceptionname, 1, int) F(uint64, exceptioncode, 2, int) +IDR_CLASS(PS3ExceptionInfo, IDR_PS3EXCEPTION_FIELDS, IDR_PS3EXCEPTION_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_PS3VMREGION_FIELDS(F) \ + F(string, regionname, 1, int) F(int32, virtualsizeinbytes, 3, int) \ + F(int32, uncommittedpages, 4, int) F(int32, physicalpages, 5, int) \ + F(int32, diskpages, 6, int) F(int32, maxpages, 7, int) \ + F(int32, committedpages, 8, int) +#define IDR_PS3VMREGION_REQUIRED(F) IDR_PS3VMREGION_FIELDS(F) +IDR_CLASS(PS3VMRegionStats, IDR_PS3VMREGION_FIELDS, IDR_PS3VMREGION_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_LOGEVENT_FIELDS(F) \ + F(string, timestamp, 1, int) F(enum, severity, 2, LogEvent_Severity) \ + F(string, message, 3, int) F(repeated_string, tags, 4, int) \ + F(string, data, 5, int) F(string, datatype, 6, int) \ + F(repeated_message, externs, 7, Attachment) \ + F(enum, platform, 8, TargetPlatform) +#define IDR_LOGEVENT_REQUIRED(F) \ + F(string, timestamp, 1, int) F(string, message, 3, int) +#define IDR_LOGEVENT_EXTRAS \ + typedef LogEvent_Severity Severity; \ + static const Severity SEV_EMERGENCY = LogEvent_Severity_SEV_EMERGENCY; \ + static const Severity SEV_ALERT = LogEvent_Severity_SEV_ALERT; \ + static const Severity SEV_CRITICAL = LogEvent_Severity_SEV_CRITICAL; \ + static const Severity SEV_ERROR = LogEvent_Severity_SEV_ERROR; \ + static const Severity SEV_WARNING = LogEvent_Severity_SEV_WARNING; \ + static const Severity SEV_NOTICE = LogEvent_Severity_SEV_NOTICE; \ + static const Severity SEV_INFO = LogEvent_Severity_SEV_INFO; \ + static const Severity SEV_DEBUG = LogEvent_Severity_SEV_DEBUG; \ + static const Severity SEV_TRASH = LogEvent_Severity_SEV_TRASH; +IDR_CLASS(LogEvent, IDR_LOGEVENT_FIELDS, IDR_LOGEVENT_REQUIRED, IDR_LOGEVENT_EXTRAS) + +#define IDR_ASSERTREPORT_FIELDS(F) \ + F(string, mapname, 1, int) F(string, filename, 2, int) \ + F(uint32, line, 3, int) F(string, expression, 4, int) \ + F(string, username, 5, int) +#define IDR_ASSERTREPORT_REQUIRED(F) \ + F(string, mapname, 1, int) F(string, filename, 2, int) \ + F(uint32, line, 3, int) F(string, expression, 4, int) +IDR_CLASS(AssertReport, IDR_ASSERTREPORT_FIELDS, IDR_ASSERTREPORT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_MAPREPORT_FIELDS(F) \ + F(string, mapname, 1, int) F(uint32, loadtime, 2, int) \ + F(string, username, 3, int) F(string, machinename, 4, int) \ + F(string, platform, 5, int) F(string, game, 6, int) \ + F(string, buildversion, 7, int) F(string, buildtimestamp, 8, int) \ + F(repeated_message, warnings, 9, MapWarning) +#define IDR_MAPREPORT_REQUIRED(F) \ + F(string, mapname, 1, int) F(uint32, loadtime, 2, int) \ + F(string, platform, 5, int) F(string, game, 6, int) \ + F(string, buildversion, 7, int) F(string, buildtimestamp, 8, int) +IDR_CLASS(MapReport, IDR_MAPREPORT_FIELDS, IDR_MAPREPORT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_VIEWNOTE_FIELDS(F) \ + F(string, game, 1, int) F(string, username, 2, int) \ + F(string, reportedby, 3, int) F(bool, issingleplayer, 4, int) \ + F(string, platform, 5, int) F(int32, buildnumbermajor, 6, int) \ + F(int32, buildnumberminor, 7, int) F(string, timestamp, 9, int) \ + F(string, launchcommand, 10, int) F(string, vtfilepath, 11, int) \ + F(string, vtfilepathvmtroverride, 12, int) F(string, bugtitle, 13, int) \ + F(string, tasktype, 14, int) F(string, reprosteps, 15, int) \ + F(string, details, 16, int) F(string, severity, 17, int) \ + F(string, mappath, 19, int) F(string, priority, 20, int) \ + F(message, attachment, 22, Attachment) +#define IDR_VIEWNOTE_REQUIRED(F) \ + F(string, game, 1, int) F(string, username, 2, int) \ + F(string, reportedby, 3, int) F(bool, issingleplayer, 4, int) \ + F(string, platform, 5, int) F(int32, buildnumbermajor, 6, int) \ + F(int32, buildnumberminor, 7, int) F(string, timestamp, 9, int) \ + F(string, launchcommand, 10, int) F(string, vtfilepath, 11, int) \ + F(string, vtfilepathvmtroverride, 12, int) F(string, bugtitle, 13, int) \ + F(string, tasktype, 14, int) F(string, reprosteps, 15, int) \ + F(string, details, 16, int) F(string, severity, 17, int) +IDR_CLASS(ViewNoteReport, IDR_VIEWNOTE_FIELDS, IDR_VIEWNOTE_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_STRINGID_FIELDS(F) F(repeated_string, stringids, 1, int) +IDR_CLASS(StringIDReferenceReport, IDR_STRINGID_FIELDS, IDR_NO_FIELDS, IDR_NO_EXTRAS) + +#define IDR_HEAPUSAGE_FIELDS(F) \ + F(enum, heaptype, 1, HeapType) F(uint32, totalever, 2, int) \ + F(uint32, totalcurrent, 3, int) F(uint32, totalsize, 4, int) \ + F(uint32, totalwaste, 5, int) \ + F(repeated_message, categories, 6, HeapCategoryUsage) +#define IDR_HEAPUSAGE_REQUIRED(F) \ + F(enum, heaptype, 1, HeapType) F(uint32, totalever, 2, int) \ + F(uint32, totalcurrent, 3, int) F(uint32, totalsize, 4, int) \ + F(uint32, totalwaste, 5, int) +IDR_CLASS(HeapUsage, IDR_HEAPUSAGE_FIELDS, IDR_HEAPUSAGE_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_XBOXKIT_FIELDS(F) \ + F(enum, kittype, 1, Xbox360ConsoleType) \ + F(message, basekernelversion, 2, Xbox360VersionInfo) \ + F(message, kernelversion, 3, Xbox360VersionInfo) \ + F(message, xdkversion, 4, Xbox360VersionInfo) \ + F(uint32, systeminfoflags, 5, int) F(string, consolename, 6, int) +#define IDR_XBOXKIT_REQUIRED(F) IDR_XBOXKIT_FIELDS(F) +IDR_CLASS(Xbox360KitInfo, IDR_XBOXKIT_FIELDS, IDR_XBOXKIT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_XENONREGISTER_FIELDS(F) \ + F(string, msr, 1, int) F(string, iar, 2, int) F(string, lr, 3, int) \ + F(string, ctr, 4, int) F(string, fpscr, 5, int) \ + F(repeated_string, fpr, 6, int) F(string, cr, 7, int) \ + F(string, xer, 8, int) F(repeated_string, gpr, 9, int) \ + F(string, vscr, 10, int) F(repeated_string, vr, 11, int) +#define IDR_XENONREGISTER_REQUIRED(F) \ + F(string, msr, 1, int) F(string, iar, 2, int) F(string, lr, 3, int) \ + F(string, ctr, 4, int) F(string, fpscr, 5, int) \ + F(string, cr, 7, int) F(string, xer, 8, int) F(string, vscr, 10, int) +IDR_CLASS(XenonRegisterInfo, IDR_XENONREGISTER_FIELDS, IDR_XENONREGISTER_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_PPUREGISTER_FIELDS(F) \ + F(repeated_uint64, gpr, 1, int) F(uint32, cr, 2, int) \ + F(uint64, xer, 3, int) F(uint64, lr, 4, int) F(uint64, ctr, 5, int) \ + F(uint64, pc, 6, int) F(repeated_uint64, fpr, 7, int) \ + F(uint32, fpscr, 8, int) F(repeated_message, vr, 9, BigUInt) \ + F(message, vscr, 10, BigUInt) +#define IDR_PPUREGISTER_REQUIRED(F) \ + F(uint32, cr, 2, int) F(uint64, xer, 3, int) F(uint64, lr, 4, int) \ + F(uint64, ctr, 5, int) F(uint64, pc, 6, int) \ + F(uint32, fpscr, 8, int) F(message, vscr, 10, BigUInt) +IDR_CLASS(PPURegisterInfo, IDR_PPUREGISTER_FIELDS, IDR_PPUREGISTER_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_SPUREGISTER_FIELDS(F) \ + F(repeated_message, gpr, 1, BigUInt) F(uint32, npc, 2, int) \ + F(repeated_message, fpscr, 3, BigUInt) F(uint32, srr0, 4, int) \ + F(uint32, spu_status, 5, int) F(uint64, spu_cfg, 6, int) \ + F(uint32, mb_stat, 7, int) F(uint32, ppu_mb, 8, int) \ + F(repeated_uint32, spu_mb, 9, int) F(uint32, decrementer, 10, int) \ + F(repeated_uint64, mfc_cq_sr, 11, int) +#define IDR_SPUREGISTER_REQUIRED(F) \ + F(uint32, npc, 2, int) F(uint32, srr0, 4, int) \ + F(uint32, spu_status, 5, int) F(uint64, spu_cfg, 6, int) \ + F(uint32, mb_stat, 7, int) F(uint32, ppu_mb, 8, int) \ + F(uint32, decrementer, 10, int) +IDR_CLASS(SPURegisterInfo, IDR_SPUREGISTER_FIELDS, IDR_SPUREGISTER_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_PS3MEMORY_FIELDS(F) \ + F(uint32, totalsystemmemory, 1, int) F(uint32, usedsystemmemory, 2, int) \ + F(uint32, availablesystemmemory, 3, int) F(uint32, unaccountedfor, 4, int) \ + F(uint32, pmem_total, 5, int) F(uint32, pmem_used, 6, int) \ + F(uint64, page_in, 7, int) F(uint64, page_out, 8, int) \ + F(uint64, page_fault_ppu, 9, int) F(uint64, page_fault_spu, 10, int) \ + F(repeated_message, regions, 11, PS3VMRegionStats) +#define IDR_PS3MEMORY_REQUIRED(F) \ + F(uint32, totalsystemmemory, 1, int) F(uint32, usedsystemmemory, 2, int) \ + F(uint32, availablesystemmemory, 3, int) F(uint32, unaccountedfor, 4, int) \ + F(uint32, pmem_total, 5, int) F(uint32, pmem_used, 6, int) \ + F(uint64, page_in, 7, int) F(uint64, page_out, 8, int) \ + F(uint64, page_fault_ppu, 9, int) F(uint64, page_fault_spu, 10, int) +IDR_CLASS(PS3MemoryInfo, IDR_PS3MEMORY_FIELDS, IDR_PS3MEMORY_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_COMMON_CRASH_FIELDS(F) \ + F(string, game, 1, int) F(string, username, 2, int) \ + F(string, platform, 3, int) F(int32, buildnumbermajor, 4, int) \ + F(int32, buildnumberminor, 5, int) F(string, buildstring, 6, int) \ + F(string, timestamp, 7, int) F(string, launchcommand, 8, int) \ + F(string, vtfilepath, 9, int) F(string, vtfilepathvmtroverride, 10, int) + +#define IDR_CRASHREPORT_FIELDS(F) \ + IDR_COMMON_CRASH_FIELDS(F) F(string, bugtitle, 11, int) \ + F(string, reprosteps, 12, int) F(string, details, 13, int) \ + F(string, severity, 14, int) F(string, component, 15, int) \ + F(string, mappath, 16, int) F(string, priority, 17, int) \ + F(string, localfilename, 18, int) F(string, dmppath, 19, int) \ + F(repeated_message, callstack, 20, CallstackLine) \ + F(message, exception, 21, ExceptionInfo) F(message, registers, 22, RegisterInfo) \ + F(message, attachment, 23, Attachment) F(message, cpuinfo, 24, CPUInfo) \ + F(message, meminfo, 25, MemoryInfo) \ + F(repeated_string, consolehistory, 26, int) +#define IDR_CRASHREPORT_REQUIRED(F) \ + IDR_COMMON_CRASH_FIELDS(F) F(string, bugtitle, 11, int) \ + F(string, reprosteps, 12, int) F(string, details, 13, int) \ + F(string, severity, 14, int) F(string, component, 15, int) \ + F(string, localfilename, 18, int) +IDR_CLASS(CrashReport, IDR_CRASHREPORT_FIELDS, IDR_CRASHREPORT_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_XBOXCRASH_FIELDS(F) \ + IDR_COMMON_CRASH_FIELDS(F) F(string, mappath, 11, int) \ + F(message, kitinfo, 12, Xbox360KitInfo) \ + F(repeated_string, callstack, 13, int) \ + F(message, exception, 14, ExceptionInfo) \ + F(message, registers, 15, XenonRegisterInfo) \ + F(message, consolememory, 16, XenonMemoryInfo) \ + F(message, titlememory, 17, XenonMemoryInfo) +#define IDR_XBOXCRASH_REQUIRED(F) \ + IDR_COMMON_CRASH_FIELDS(F) F(message, kitinfo, 12, Xbox360KitInfo) +IDR_CLASS(Xbox360CrashReport, IDR_XBOXCRASH_FIELDS, IDR_XBOXCRASH_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_PS3CRASH_FIELDS(F) \ + IDR_COMMON_CRASH_FIELDS(F) F(string, mappath, 11, int) \ + F(repeated_uint64, callstack, 12, int) \ + F(message, exception, 13, PS3ExceptionInfo) \ + F(message, ppuregisters, 14, PPURegisterInfo) \ + F(repeated_message, spuregisters, 15, SPURegisterInfo) \ + F(message, memoryinfo, 16, PS3MemoryInfo) +#define IDR_PS3CRASH_REQUIRED(F) IDR_COMMON_CRASH_FIELDS(F) +IDR_CLASS(PS3CrashReport, IDR_PS3CRASH_FIELDS, IDR_PS3CRASH_REQUIRED, IDR_NO_EXTRAS) + +#define IDR_CRASHRESULT_FIELDS(F) F(int64, bugid, 1, int) +#define IDR_CRASHRESULT_REQUIRED(F) IDR_CRASHRESULT_FIELDS(F) +IDR_CLASS(CrashReportResult, IDR_CRASHRESULT_FIELDS, IDR_CRASHRESULT_REQUIRED, IDR_NO_EXTRAS) + +#undef IDR_CLASS +#undef IDR_DECLARE +#undef IDR_KIND +#undef IDR_FACTORY +#undef IDR_REQUIRE + +void protobuf_AddDesc_reports_2eproto(); +void protobuf_ShutdownFile_reports_2eproto(); + +} // namespace idreports diff --git a/source/shared/idlib/packing/bitblockallocator.cpp b/source/shared/idlib/packing/bitblockallocator.cpp new file mode 100644 index 0000000..b65c00e --- /dev/null +++ b/source/shared/idlib/packing/bitblockallocator.cpp @@ -0,0 +1,180 @@ +#include "bitblockallocator.h" + +#include +#include +#include + +idBitBlockAllocator::idBitBlockAllocator(const int blocksWide, + const int blocksHigh) + : width(std::max(0, blocksWide)), height(std::max(0, blocksHigh)), + bits(nullptr) { + const std::size_t count = static_cast(width) + * static_cast(height); + if (count != 0) { + bits = static_cast(std::calloc((count + 7u) / 8u, 1)); + } +} + +idBitBlockAllocator::~idBitBlockAllocator() { + std::free(bits); +} + +void idBitBlockAllocator::Clear() { + if (bits != nullptr) { + const std::size_t count = static_cast(width) + * static_cast(height); + std::memset(bits, 0, (count + 7u) / 8u); + } +} + +bool idBitBlockAllocator::Get(const int x, const int y) const { + if (bits == nullptr || x < 0 || y < 0 || x >= width || y >= height) { + return false; + } + const int bit = y * width + x; + return (bits[bit >> 3] & (1u << (bit & 7))) != 0; +} + +void idBitBlockAllocator::Set(const int x, const int y) { + if (bits == nullptr || x < 0 || y < 0 || x >= width || y >= height) { + return; + } + const int bit = y * width + x; + bits[bit >> 3] |= static_cast(1u << (bit & 7)); +} + +void idBitBlockAllocator::FillBlock(const int x, const int y, + const int w, const int h) { + for (int row = 0; row < h; ++row) { + for (int column = 0; column < w; ++column) { + Set(x + column, y + row); + } + } +} + +void idBitBlockAllocator::FillBitBlock(const int x, const int y, + const idBitBlockAllocator& block) { + for (int row = 0; row < block.height; ++row) { + for (int column = 0; column < block.width; ++column) { + if (block.Get(column, row)) { + Set(x + column, y + row); + } + } + } +} + +bool idBitBlockAllocator::TestBlock(const int x, const int y, const int w, + const int h, int* const fail) const { + if (w < 0 || h < 0 || x < 0 || y < 0 || x + w > width || y + h > height) { + return false; + } + if (fail != nullptr && fail[0] >= x && fail[0] < x + w + && fail[1] >= y && fail[1] < y + h) { + return false; + } + for (int row = 0; row < h; ++row) { + for (int column = w - 1; column >= 0; --column) { + if (Get(x + column, y + row)) { + if (fail != nullptr) { + fail[0] = x + column; + fail[1] = y + row; + } + return false; + } + } + } + return true; +} + +bool idBitBlockAllocator::TestBitBlock(const int x, const int y, + const idBitBlockAllocator& block) const { + if (x < 0 || y < 0 || x + block.width > width || y + block.height > height) { + return false; + } + for (int row = 0; row < block.height; ++row) { + for (int column = 0; column < block.width; ++column) { + if (block.Get(column, row) && Get(x + column, y + row)) { + return false; + } + } + } + return true; +} + +bool idBitBlockAllocator::FindBlockLinear(int& x, int& y, const int w, + const int h) { + int fail[2] = { -1, -1 }; + for (y = 0; y <= height - h; ++y) { + for (x = 0; x <= width - w; ++x) { + if (TestBlock(x, y, w, h, fail)) { + FillBlock(x, y, w, h); + return true; + } + } + } + return false; +} + +bool idBitBlockAllocator::FindBitBlockLinear(int& x, int& y, + const idBitBlockAllocator& block) { + for (y = 0; y <= height - block.height; ++y) { + for (x = 0; x <= width - block.width; ++x) { + if (TestBitBlock(x, y, block)) { + FillBitBlock(x, y, block); + return true; + } + } + } + return false; +} + +void SeparateBits(const int value, int& x, int& y) { + x = 0; + y = 0; + const unsigned int bits = static_cast(value); + for (unsigned int index = 0; index < 16; ++index) { + x |= static_cast(((bits >> (index * 2)) & 1u) << index); + y |= static_cast(((bits >> (index * 2 + 1)) & 1u) << index); + } +} + +void SeparateBits2(const int bits, int& x, int& y) { + SeparateBits(bits, x, y); +} + +bool idBitBlockAllocator::FindBlockInterleaved(int& x, int& y, const int w, + const int h, int& searchPoint, const bool naturallyAlign) { + if (w > width || h > height || w < 0 || h < 0) { + return false; + } + if (searchPoint < 0) { + searchPoint = 0; + } + const int count = width * height; + int fail[2] = { -1, -1 }; + const auto nextPowerOfTwo = [](const int value) { + int power = 1; + while (power < std::max(1, value)) { + power <<= 1; + } + return power; + }; + const int alignX = nextPowerOfTwo(w); + const int alignY = nextPowerOfTwo(h); + for (; searchPoint < count; ++searchPoint) { + const int interleaved = naturallyAlign + ? searchPoint + : ((searchPoint >> 1) ^ searchPoint); + SeparateBits2(interleaved, x, y); + if (naturallyAlign + && (x / alignX != (x + std::max(0, w - 1)) / alignX + || y / alignY != (y + std::max(0, h - 1)) / alignY)) { + continue; + } + if (TestBlock(x, y, w, h, fail)) { + FillBlock(x, y, w, h); + return true; + } + } + return false; +} diff --git a/source/shared/idlib/packing/bitblockallocator.h b/source/shared/idlib/packing/bitblockallocator.h new file mode 100644 index 0000000..ae91bfd --- /dev/null +++ b/source/shared/idlib/packing/bitblockallocator.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +class idBitBlockAllocator { +public: + idBitBlockAllocator(int blocksWide, int blocksHigh); + ~idBitBlockAllocator(); + + idBitBlockAllocator(const idBitBlockAllocator&) = delete; + idBitBlockAllocator& operator=(const idBitBlockAllocator&) = delete; + + void Clear(); + void FillBlock(int x, int y, int w, int h); + void FillBitBlock(int x, int y, const idBitBlockAllocator& block); + bool TestBlock(int x, int y, int w, int h, int* fail = nullptr) const; + bool TestBitBlock(int x, int y, const idBitBlockAllocator& block) const; + bool FindBlockLinear(int& x, int& y, int w, int h); + bool FindBitBlockLinear(int& x, int& y, const idBitBlockAllocator& block); + bool FindBlockInterleaved(int& x, int& y, int w, int h, + int& searchPoint, bool naturallyAlign); + + int Width() const { return width; } + int Height() const { return height; } + bool Get(int x, int y) const; + +private: + int width; + int height; + unsigned char* bits; + + void Set(int x, int y); +}; + +void SeparateBits(int bits, int& x, int& y); +void SeparateBits2(int bits, int& x, int& y); + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idBitBlockAllocator) == 12, + "Recovered idBitBlockAllocator ABI changed"); +#endif diff --git a/source/shared/idlib/parallelism/locklessqueue.cpp b/source/shared/idlib/parallelism/locklessqueue.cpp new file mode 100644 index 0000000..86cf7f8 --- /dev/null +++ b/source/shared/idlib/parallelism/locklessqueue.cpp @@ -0,0 +1,5 @@ +#include "locklessqueue.h" + +// The recovered translation unit contains the engine's producer/consumer +// stress command. The queue algorithms are template code in the header; their +// portable regression coverage lives in idlib_containers_test. diff --git a/source/shared/idlib/parallelism/locklessqueue.h b/source/shared/idlib/parallelism/locklessqueue.h new file mode 100644 index 0000000..06f1b27 --- /dev/null +++ b/source/shared/idlib/parallelism/locklessqueue.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +template +class idLocklessQueueSingleProdCons { +public: + idLocklessQueueSingleProdCons() + : queue{}, queueStart(0), queueEnd(0) { + static_assert(queueSize > 1 && (queueSize & (queueSize - 1)) == 0, + "Lockless queue size must be a power of two"); + } + + void Add(type* element) { + int end = queueEnd.load(std::memory_order_relaxed); + while (((end + 1) & Mask()) + == queueStart.load(std::memory_order_acquire)) { + std::this_thread::yield(); + end = queueEnd.load(std::memory_order_relaxed); + } + queue[end] = element; + queueEnd.store((end + 1) & Mask(), std::memory_order_release); + } + + type* Next() { + const int start = queueStart.load(std::memory_order_relaxed); + if (start == queueEnd.load(std::memory_order_acquire)) { + return nullptr; + } + type* const result = queue[start]; + queueStart.store((start + 1) & Mask(), std::memory_order_release); + return result; + } + + bool IsEmpty() const { + return queueStart.load(std::memory_order_acquire) + == queueEnd.load(std::memory_order_acquire); + } + +private: + type* queue[queueSize]; + std::atomic queueStart; + alignas(128) std::atomic queueEnd; + + static constexpr int Mask() { return queueSize - 1; } +}; + +template +class idLocklessQueueMultiProdCons { +public: + idLocklessQueueMultiProdCons() + : queue{}, queueStart(0), queueFetched(0), queueEnd(0), queueAlloced(0) { + static_assert(queueSize > 1 && (queueSize & (queueSize - 1)) == 0, + "Lockless queue size must be a power of two"); + static_assert(sizeof(std::atomic) == sizeof(type*), + "PC atomics changed the recovered queue slot layout"); + for (int index = 0; index < queueSize; ++index) { + queue[index].store(nullptr, std::memory_order_relaxed); + } + } + + void Add(type* element) { + int allocated; + for (;;) { + allocated = queueAlloced.load(std::memory_order_relaxed); + while ((((allocated + 1) + ^ queueFetched.load(std::memory_order_acquire)) & Mask()) == 0) { + std::this_thread::yield(); + allocated = queueAlloced.load(std::memory_order_relaxed); + } + if (queueAlloced.compare_exchange_weak(allocated, allocated + 1, + std::memory_order_acq_rel, std::memory_order_relaxed)) { + break; + } + } + + queue[allocated & Mask()].store(element, std::memory_order_release); + AdvancePublishedEnd(); + } + + type* Next() { + int start = queueStart.load(std::memory_order_relaxed); + for (;;) { + if (start == queueEnd.load(std::memory_order_acquire)) { + return nullptr; + } + if (queueStart.compare_exchange_weak(start, start + 1, + std::memory_order_acq_rel, std::memory_order_relaxed)) { + break; + } + } + + type* const result = queue[start & Mask()].exchange( + nullptr, std::memory_order_acq_rel + ); + AdvanceFetched(); + return result; + } + + bool IsEmpty() const { + return queueStart.load(std::memory_order_acquire) + == queueEnd.load(std::memory_order_acquire); + } + +private: + std::atomic queue[queueSize]; + std::atomic queueStart; + alignas(128) std::atomic queueFetched; + alignas(128) std::atomic queueEnd; + alignas(128) std::atomic queueAlloced; + + static constexpr int Mask() { return queueSize - 1; } + + void AdvancePublishedEnd() { + int end = queueEnd.load(std::memory_order_relaxed); + while (end != queueAlloced.load(std::memory_order_acquire) + && queue[end & Mask()].load(std::memory_order_acquire) != nullptr) { + if (!queueEnd.compare_exchange_weak(end, end + 1, + std::memory_order_release, std::memory_order_relaxed)) { + continue; + } + end = queueEnd.load(std::memory_order_relaxed); + } + } + + void AdvanceFetched() { + int fetched = queueFetched.load(std::memory_order_relaxed); + while (fetched != queueStart.load(std::memory_order_acquire) + && queue[fetched & Mask()].load(std::memory_order_acquire) == nullptr) { + if (!queueFetched.compare_exchange_weak(fetched, fetched + 1, + std::memory_order_release, std::memory_order_relaxed)) { + continue; + } + fetched = queueFetched.load(std::memory_order_relaxed); + } + } +}; diff --git a/source/shared/idlib/runningaverage.cpp b/source/shared/idlib/runningaverage.cpp new file mode 100644 index 0000000..66b82d3 --- /dev/null +++ b/source/shared/idlib/runningaverage.cpp @@ -0,0 +1,95 @@ +#include "runningaverage.h" + +#include +#include + +idRunningAverage::idRunningAverage() + : maxNum(0) + , current(0) + , vals{nullptr, 0, 0, 16, 5, 0} { +} + +idRunningAverage::~idRunningAverage() { + std::free(vals.list); +} + +float idRunningAverage::GetAverage() { + if (vals.num == 0) { + return 0.0f; + } + + float total = 0.0f; + for (int index = 0; index < vals.num; ++index) { + total += vals.list[index]; + } + return total / static_cast(vals.num); +} + +float idRunningAverage::GetMin() { + if (vals.num == 0) { + return 0.0f; + } + + float minimum = vals.list[0]; + for (int index = 1; index < vals.num; ++index) { + minimum = std::min(minimum, vals.list[index]); + } + return minimum; +} + +float idRunningAverage::GetMax() { + if (vals.num == 0) { + return 0.0f; + } + + float maximum = vals.list[0]; + for (int index = 1; index < vals.num; ++index) { + maximum = std::max(maximum, vals.list[index]); + } + return maximum; +} + +void idRunningAverage::Init(const int num) { + current = 0; + + if (num <= 0) { + vals.num = 0; + maxNum = 0; + return; + } + + if (vals.size < num) { + float* const replacement = static_cast( + std::malloc(sizeof(float) * static_cast(num)) + ); + if (replacement == nullptr) { + vals.num = 0; + maxNum = 0; + return; + } + std::free(vals.list); + vals.list = replacement; + vals.size = num; + } + + vals.num = 0; + maxNum = num; +} + +void idRunningAverage::Add(const float val) { + if (maxNum <= 0) { + return; + } + + if (vals.num != maxNum) { + ++vals.num; + current = vals.num - 1; + } else { + ++current; + if (current >= maxNum) { + current = 0; + } + } + + vals.list[current] = val; +} diff --git a/source/shared/idlib/runningaverage.h b/source/shared/idlib/runningaverage.h new file mode 100644 index 0000000..638655b --- /dev/null +++ b/source/shared/idlib/runningaverage.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +// Recovered layout of idList from tungsten.exe.h type 12501. This +// private storage facade avoids importing the newer BFG container definition +// while retaining the original Win32 field widths and offsets. +struct idRunningAverageFloatList { + float* list; + int num; + int size; + std::int16_t granularity; + std::uint8_t memTag; + std::uint8_t listStatic; +}; + +static_assert( + sizeof(idRunningAverageFloatList) == 16, + "Recovered idList layout changed" +); + +// tungsten.exe.h type 12505. +class idRunningAverage { +public: + idRunningAverage(); + ~idRunningAverage(); + + idRunningAverage(const idRunningAverage&) = delete; + idRunningAverage& operator=(const idRunningAverage&) = delete; + + float GetAverage(); + float GetMin(); + float GetMax(); + void Init(int num); + void Add(float val); + +private: + int maxNum; + int current; + idRunningAverageFloatList vals; +}; + +static_assert(sizeof(idRunningAverage) == 24, "idRunningAverage ABI changed"); diff --git a/source/shared/idlib/runtimeexpression.cpp b/source/shared/idlib/runtimeexpression.cpp new file mode 100644 index 0000000..a030ce0 --- /dev/null +++ b/source/shared/idlib/runtimeexpression.cpp @@ -0,0 +1,5 @@ +#include "runtimeexpression.h" + +// The recovered RuntimeExpression.cpp is the console regression harness. The +// reusable parser/evaluator is template code in RuntimeExpression.h and is +// covered by idlib_runtimeexpression_test on the PC port. diff --git a/source/shared/idlib/runtimeexpression.h b/source/shared/idlib/runtimeexpression.h new file mode 100644 index 0000000..abfd150 --- /dev/null +++ b/source/shared/idlib/runtimeexpression.h @@ -0,0 +1,380 @@ +#pragma once + +#include "text/str.h" + +#include +#include +#include +#include +#include +#include +#include + +template +class idRuntimeExpression { +public: + enum { MAX_NODES = 253, INVALID_INDEX = 255 }; + + class ExpNode { + public: + enum op_t { + OP_NONE = 0, + OP_VAL, + OP_VAR, + OP_MUL, + OP_DIV, + OP_MAX, + OP_MIN, + OP_LERP, + OP_CLAMP, + OP_SUB, + OP_ADD + }; + + ExpNode() + : coef(1.0f), op(OP_NONE), parent(INVALID_INDEX), + var0(INVALID_INDEX), var1(INVALID_INDEX), var2(INVALID_INDEX), + varId() { + } + + float coef; + op_t op; + std::uint8_t parent; + std::uint8_t var0; + std::uint8_t var1; + std::uint8_t var2; + varType varId; + }; + + idRuntimeExpression() + : root(INVALID_INDEX), nodes(nullptr), numNodes(0), capacity(0), + granularity(5), memTag(5), listStatic(0) { + } + + ~idRuntimeExpression() { + Clear(); + } + + idRuntimeExpression(const idRuntimeExpression&) = delete; + idRuntimeExpression& operator=(const idRuntimeExpression&) = delete; + + bool Parse(const char* expression, varContextType& context) { + Clear(); + const char* cursor = expression == nullptr ? "" : expression; + const int parsedRoot = ParseAddSub(cursor, context); + SkipWhitespace(cursor); + if (parsedRoot == INVALID_INDEX || *cursor != '\0') { + Clear(); + return false; + } + root = parsedRoot; + return true; + } + + float Eval(varContextType& context) const { + return root == INVALID_INDEX ? 0.0f : EvalNode(root, context); + } + + idStr PrintExp() const { + return root == INVALID_INDEX ? idStr() : PrintNode(root); + } + + int NumNodes() const { return numNodes; } + + void Clear() { + if (nodes != nullptr) { + for (int index = 0; index < capacity; ++index) { + nodes[index].~ExpNode(); + } + std::free(nodes); + } + root = INVALID_INDEX; + nodes = nullptr; + numNodes = 0; + capacity = 0; + } + +private: + int root; + ExpNode* nodes; + int numNodes; + int capacity; + std::int16_t granularity; + std::uint8_t memTag; + std::uint8_t listStatic; + + static void SkipWhitespace(const char*& cursor) { + while (std::isspace(static_cast(*cursor)) != 0) { + ++cursor; + } + } + + bool Grow() { + if (capacity >= MAX_NODES) { + return false; + } + const int newCapacity = std::min(static_cast(MAX_NODES), + capacity + (granularity > 0 ? granularity : 5)); + ExpNode* const replacement = static_cast( + std::malloc(sizeof(ExpNode) * static_cast(newCapacity)) + ); + if (replacement == nullptr) { + return false; + } + for (int index = 0; index < newCapacity; ++index) { + new (&replacement[index]) ExpNode(); + } + for (int index = 0; index < numNodes; ++index) { + replacement[index] = nodes[index]; + } + if (nodes != nullptr) { + for (int index = 0; index < capacity; ++index) { + nodes[index].~ExpNode(); + } + std::free(nodes); + } + nodes = replacement; + capacity = newCapacity; + return true; + } + + int AddNode(const typename ExpNode::op_t op, const int child0 = INVALID_INDEX, + const int child1 = INVALID_INDEX, const int child2 = INVALID_INDEX, + const float coefficient = 1.0f, const varType* variable = nullptr) { + if (numNodes == capacity && !Grow()) { + return INVALID_INDEX; + } + const int index = numNodes++; + ExpNode& node = nodes[index]; + node.op = op; + node.coef = coefficient; + node.var0 = static_cast(child0); + node.var1 = static_cast(child1); + node.var2 = static_cast(child2); + if (variable != nullptr) { + node.varId = *variable; + } + if (child0 != INVALID_INDEX) nodes[child0].parent = static_cast(index); + if (child1 != INVALID_INDEX) nodes[child1].parent = static_cast(index); + if (child2 != INVALID_INDEX) nodes[child2].parent = static_cast(index); + return index; + } + + int ParseAddSub(const char*& cursor, varContextType& context) { + int left = ParseMulDiv(cursor, context); + while (left != INVALID_INDEX) { + SkipWhitespace(cursor); + const char operation = *cursor; + if (operation != '+' && operation != '-') break; + ++cursor; + const int right = ParseMulDiv(cursor, context); + if (right == INVALID_INDEX) return INVALID_INDEX; + left = AddNode(operation == '+' ? ExpNode::OP_ADD : ExpNode::OP_SUB, + left, right); + } + return left; + } + + int ParseMulDiv(const char*& cursor, varContextType& context) { + int left = ParseUnary(cursor, context); + while (left != INVALID_INDEX) { + SkipWhitespace(cursor); + const char operation = *cursor; + if (operation != '*' && operation != '/') break; + ++cursor; + const int right = ParseUnary(cursor, context); + if (right == INVALID_INDEX) return INVALID_INDEX; + left = AddNode(operation == '*' ? ExpNode::OP_MUL : ExpNode::OP_DIV, + left, right); + } + return left; + } + + int ParseUnary(const char*& cursor, varContextType& context) { + SkipWhitespace(cursor); + if (*cursor == '+') { + ++cursor; + return ParseUnary(cursor, context); + } + if (*cursor == '-') { + ++cursor; + const int child = ParseUnary(cursor, context); + return child == INVALID_INDEX ? INVALID_INDEX + : AddNode(ExpNode::OP_MUL, child, INVALID_INDEX, + INVALID_INDEX, -1.0f); + } + return ParsePrimary(cursor, context); + } + + int ParsePrimary(const char*& cursor, varContextType& context) { + SkipWhitespace(cursor); + if (*cursor == '(') { + ++cursor; + const int result = ParseAddSub(cursor, context); + SkipWhitespace(cursor); + if (*cursor != ')') return INVALID_INDEX; + ++cursor; + return result; + } + if (std::isdigit(static_cast(*cursor)) != 0 + || *cursor == '.') { + char* numberEnd = nullptr; + const float value = std::strtof(cursor, &numberEnd); + if (numberEnd == cursor) return INVALID_INDEX; + cursor = numberEnd; + return AddNode(ExpNode::OP_VAL, INVALID_INDEX, INVALID_INDEX, + INVALID_INDEX, value); + } + + char name[128]; + int nameLength = 0; + while ((std::isalnum(static_cast(*cursor)) != 0 + || *cursor == '_' || *cursor == '.') && nameLength < 127) { + name[nameLength++] = *cursor++; + } + name[nameLength] = '\0'; + if (nameLength == 0) return INVALID_INDEX; + SkipWhitespace(cursor); + if (*cursor != '(') { + varType variable; + if (!context.LookUpVar(name, variable)) return INVALID_INDEX; + return AddNode(ExpNode::OP_VAR, INVALID_INDEX, INVALID_INDEX, + INVALID_INDEX, 1.0f, &variable); + } + + ++cursor; + const int first = ParseAddSub(cursor, context); + SkipWhitespace(cursor); + if (first == INVALID_INDEX || *cursor != ',') return INVALID_INDEX; + ++cursor; + const int second = ParseAddSub(cursor, context); + if (second == INVALID_INDEX) return INVALID_INDEX; + int third = INVALID_INDEX; + SkipWhitespace(cursor); + if (*cursor == ',') { + ++cursor; + third = ParseAddSub(cursor, context); + if (third == INVALID_INDEX) return INVALID_INDEX; + } + SkipWhitespace(cursor); + if (*cursor != ')') return INVALID_INDEX; + ++cursor; + + typename ExpNode::op_t operation = ExpNode::OP_NONE; + if (std::strcmp(name, "max") == 0 && third == INVALID_INDEX) operation = ExpNode::OP_MAX; + else if (std::strcmp(name, "min") == 0 && third == INVALID_INDEX) operation = ExpNode::OP_MIN; + else if (std::strcmp(name, "lerp") == 0 && third != INVALID_INDEX) operation = ExpNode::OP_LERP; + else if (std::strcmp(name, "clamp") == 0 && third != INVALID_INDEX) operation = ExpNode::OP_CLAMP; + if (operation == ExpNode::OP_NONE) return INVALID_INDEX; + return AddNode(operation, first, second, third); + } + + float EvalNode(const int index, varContextType& context) const { + const ExpNode& node = nodes[index]; + const float first = node.var0 == INVALID_INDEX ? 0.0f + : EvalNode(node.var0, context); + const float second = node.var1 == INVALID_INDEX ? 0.0f + : EvalNode(node.var1, context); + const float third = node.var2 == INVALID_INDEX ? 0.0f + : EvalNode(node.var2, context); + float value = 0.0f; + switch (node.op) { + case ExpNode::OP_VAL: value = 1.0f; break; + case ExpNode::OP_VAR: value = context.GetVar(node.varId); break; + case ExpNode::OP_MUL: + value = node.var1 == INVALID_INDEX ? first : first * second; + break; + case ExpNode::OP_DIV: value = first / second; break; + case ExpNode::OP_MAX: value = std::max(first, second); break; + case ExpNode::OP_MIN: value = std::min(first, second); break; + case ExpNode::OP_LERP: value = first + (second - first) * third; break; + case ExpNode::OP_CLAMP: value = std::max(first, std::min(second, third)); break; + case ExpNode::OP_SUB: value = first - second; break; + case ExpNode::OP_ADD: value = first + second; break; + default: value = 0.0f; break; + } + return node.coef * value; + } + + idStr PrintNode(const int index) const { + const ExpNode& node = nodes[index]; + if (node.op == ExpNode::OP_VAL) { + char number[64]; + std::snprintf(number, sizeof(number), "%g", node.coef); + return idStr(number); + } + if (node.op == ExpNode::OP_VAR) { + idStr result = node.varId.GetStr(); + if (node.coef != 1.0f) { + char prefix[64]; + std::snprintf(prefix, sizeof(prefix), "%g*", node.coef); + idStr scaled(prefix); + scaled.Append(result); + return scaled; + } + return result; + } + const char* opName = nullptr; + switch (node.op) { + case ExpNode::OP_MUL: opName = "*"; break; + case ExpNode::OP_DIV: opName = "/"; break; + case ExpNode::OP_SUB: opName = "-"; break; + case ExpNode::OP_ADD: opName = "+"; break; + case ExpNode::OP_MAX: opName = "max"; break; + case ExpNode::OP_MIN: opName = "min"; break; + case ExpNode::OP_LERP: opName = "lerp"; break; + case ExpNode::OP_CLAMP: opName = "clamp"; break; + default: return idStr(); + } + idStr result; + if (node.op == ExpNode::OP_MAX || node.op == ExpNode::OP_MIN + || node.op == ExpNode::OP_LERP || node.op == ExpNode::OP_CLAMP) { + result.Append(opName); + result.Append('('); + result.Append(PrintNode(node.var0)); + result.Append(", "); + result.Append(PrintNode(node.var1)); + if (node.var2 != INVALID_INDEX) { + result.Append(", "); + result.Append(PrintNode(node.var2)); + } + result.Append(')'); + } else if (node.var1 == INVALID_INDEX) { + char coefficient[64]; + std::snprintf(coefficient, sizeof(coefficient), "%g*", node.coef); + result.Append(coefficient); + result.Append(PrintNode(node.var0)); + return result; + } else { + result.Append('('); + result.Append(PrintNode(node.var0)); + result.Append(' '); + result.Append(opName); + result.Append(' '); + result.Append(PrintNode(node.var1)); + result.Append(')'); + } + if (node.coef != 1.0f) { + char coefficient[64]; + std::snprintf(coefficient, sizeof(coefficient), "%g*", node.coef); + idStr scaled(coefficient); + scaled.Append(result); + return scaled; + } + return result; + } +}; + +#if INTPTR_MAX == INT32_MAX +struct idRuntimeExpressionLayoutVar { + int index; + idStr GetStr() const { return idStr(); } +}; +struct idRuntimeExpressionLayoutContext { + bool LookUpVar(const char*, idRuntimeExpressionLayoutVar&) { return false; } + float GetVar(const idRuntimeExpressionLayoutVar&) const { return 0.0f; } +}; +static_assert(sizeof(idRuntimeExpression) == 20, + "Recovered idRuntimeExpression ABI changed"); +#endif diff --git a/source/shared/idlib/sourcecontrol.h b/source/shared/idlib/sourcecontrol.h new file mode 100644 index 0000000..7ddb6a3 --- /dev/null +++ b/source/shared/idlib/sourcecontrol.h @@ -0,0 +1,171 @@ +#pragma once + +#include "idlib/text/str.h" + +#include + +enum scFileType_t { + SCT_TEXT = 0, + SCT_BINARY, + SCT_SYMLINK, + SCT_APPLE, + SCT_RESOURCE, + SCT_UNICODE, + SCT_UTF16, + SCT_NONE +}; + +enum scFileStatus_t { + SCF_NOTMANAGED = 0, + SCF_NOTIMPORTED, + SCF_CHECKEDOUT, + SCF_CHECKEDOUT_PLUS, + SCF_CHECKEDOUT_BYOTHER, + SCF_CHECKEDOUT_BYOTHER_EXCLUSIVE, + SCF_CHECKEDIN +}; + +template +class idSourceControlList { +public: + explicit idSourceControlList(int listGranularity = 16) + : list(nullptr), num(0), size(0), + granularity(static_cast(listGranularity > 0 + ? listGranularity : 16)), memTag(tag), listStatic(0) {} + + idSourceControlList(const idSourceControlList& other) + : idSourceControlList(other.granularity) { + EnsureCapacity(other.num); + for (int index = 0; index < other.num; ++index) Append(other[index]); + } + + ~idSourceControlList() { delete[] list; } + + idSourceControlList& operator=(const idSourceControlList& other) { + if (this == &other) return *this; + Clear(); + granularity = other.granularity; + EnsureCapacity(other.num); + for (int index = 0; index < other.num; ++index) Append(other[index]); + return *this; + } + + int Append(const type& value) { + if (!EnsureCapacity(num + 1)) return -1; + list[num] = value; + return num++; + } + + void Clear() { num = 0; } + int Num() const { return num; } + type& operator[](int index) { return list[index]; } + const type& operator[](int index) const { return list[index]; } + +private: + bool EnsureCapacity(int amount) { + if (amount <= size) return true; + int newSize = size == 0 ? granularity : size; + while (newSize < amount) newSize += granularity; + type* const replacement = new (std::nothrow) type[newSize]; + if (replacement == nullptr) return false; + for (int index = 0; index < num; ++index) replacement[index] = list[index]; + delete[] list; + list = replacement; + size = newSize; + return true; + } + + type* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; +}; + +using idSourceControlFileList = idSourceControlList; + +class idSourceControl { +public: + struct idSourceControlWorkspace { + idStr name; + idStr lastAccessTime; + idStr owner; + idStr host; + }; + using workspaceList_t = idSourceControlList; + + virtual ~idSourceControl() = default; + virtual bool Init() { return false; } + virtual void Shutdown() {} + virtual void SetSilentCheckOut(bool) {} + virtual bool GetSilentCheckOut() const { return false; } + virtual void SetSilentCheckIn(bool) {} + virtual bool GetSilentCheckIn() const { return false; } + virtual bool IsConnected() const { return false; } + virtual bool IsInitialized() const { return false; } + virtual int CheckOut(const idSourceControlFileList&) { return 0; } + virtual int UndoCheckOut(const idSourceControlFileList&) { return 0; } + virtual int Import(const idSourceControlFileList&, bool, bool, + scFileType_t) { return 0; } + virtual int GetLatest(const idSourceControlFileList&, bool) { return 0; } + virtual int CheckIn(const idSourceControlFileList&, const idStr&, + bool) { return 0; } + virtual int Delete(const idSourceControlFileList&) { return 0; } + virtual int UpdateFolder(const char*) { return 0; } + virtual scFileStatus_t GetFileStatus(const idStr&) { + return SCF_NOTMANAGED; + } + virtual bool GetFileVersion(const idStr&, int&, int&) { return false; } + virtual int GetNumCheckOutUsers(const idStr&) { return 0; } + virtual void GetCheckOutUser(const idStr&, int, char* output, + int outputSize) { + if (output != nullptr && outputSize > 0) output[0] = '\0'; + } + virtual void GetUsernameForFileVersion(const idStr&, int, + idStr& output) { output.Clear(); } + virtual void GetCurrentWorkspace(idSourceControlWorkspace& output) { + output = idSourceControlWorkspace(); + } + virtual void GetAvailableWorkspaces(workspaceList_t& output) { + output.Clear(); + } + virtual void SetWorkspace(const char*) {} + virtual void SetWorkspace(idSourceControlWorkspace&) {} + virtual bool IsWorkspaceValid() { return false; } + virtual idStr GetUsername() { return idStr(); } + + int CheckOut(const idStr& file) { + idSourceControlFileList files(1); + files.Append(file); + return CheckOut(files); + } + + int Import(const idStr& file, bool keepCheckedOut, bool submit, + scFileType_t fileType) { + idSourceControlFileList files(1); + files.Append(file); + return Import(files, keepCheckedOut, submit, fileType); + } + + int GetLatest(const idStr& file, bool force) { + idSourceControlFileList files(1); + files.Append(file); + return GetLatest(files, force); + } + + int Delete(const idStr& file) { + idSourceControlFileList files(1); + files.Append(file); + return Delete(files); + } +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idSourceControlFileList) == 16, + "Recovered source-control list ABI changed"); +static_assert(sizeof(idSourceControl::idSourceControlWorkspace) == 128, + "Recovered source-control workspace ABI changed"); +static_assert(sizeof(idSourceControl) == 4, + "Recovered idSourceControl ABI changed"); +#endif diff --git a/source/shared/idlib/sys/sys_alloc.cpp b/source/shared/idlib/sys/sys_alloc.cpp new file mode 100644 index 0000000..b637773 --- /dev/null +++ b/source/shared/idlib/sys/sys_alloc.cpp @@ -0,0 +1,208 @@ +#include "sys_alloc.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { +struct allocationRecord_t { + allocationRecord_t* next; + void* pointer; + unsigned int bytes; + memTag_t tag; + heapType_t heap; + const char* location; +}; + +SRWLOCK allocationLock = SRWLOCK_INIT; +allocationRecord_t* allocations = nullptr; +std::atomic allocatedBytes(0); +std::atomic outOfMemoryCallback(nullptr); +thread_local heapType_t currentHeap = HEAP_SYSTEMHEAP; +thread_local heapType_t pushedHeap = HEAP_SYSTEMHEAP; +thread_local int heapStackDepth = 0; + +const char* const tagNames[TAG_NUM_TAGS] = { + "UNSET", "STATIC_EXE", "RESOURCE_GRAPH", "DEBUG", "NEW", "IDLIST", + "TEMP", "STRING", "ATOMIC_STRING", "BITARRAY", "MATH", "LEXER", + "COMPILER", "COLLISION", "COLLISION_QUERY", "CLIPMODEL", "MORPH", + "MD6_MISC", "MD6_NODES", "MD6", "MD6_ANIMS", "MD6_LIPSYNC", + "MD6_JOINTCACHE", "MD6_MESHES", "MD6_JOINTBUFFERS", "MD6_BLENDSTACK", + "MD6_JOINTMODS", "MD6_COLLISION", "MD6_ANIMEVENTS", "MD6_PHASE_TRACK", + "ANIMATION", "ANIMATION_DEBUG", "DECL_ANIMWEB", "ANIMWEB", "IMAGE", + "DXIMAGE", "VIRTUALTEXTURE", "AAS", "SOUND", "SOUND_BSP", "SOUND_DATA", + "SOUND_STREAM", "SOUND_MULTISTREAM", "SOUND_SAMPLETABLES", "IDLIB", + "TRIANGLES", "DECL", "DECLTEXT", "FILE", "CVAR", "PAGEFILECACHE", + "IDCLASS", "PRESENTABLE", "FOLIAGE", "WATER", "MEMORY_MAPPED_FILE", + "RENDERPARM", "NETWORKING", "SCRIPT", "FXPHYSICS", "LWO", "RENDERWORLD", + "RENDERER", "AI_GAMESTATE", "EVENTS", "VOICEOVER", "VOICETRACK_EVENTS", + "VOICETRACK_FRAMEREFS", "VOICETRACK_PHONEMES", "VISEMESET_VISEMES", + "VISEMESET_PHONEMES", "AF", "SWF", "GUI", "GUI_MODEL", "FUNC_CALLBACK", + "MENU", "GAME", "HASHINDEX", "PARTICLE", "EFFECT_PARTICLE", "CLOTH", + "ANIMTAGS", "IK", "STATICMODEL", "RENDERMODEL", "DXBUFFER", "TOOLS", + "CLOUD", "AMQP", "RENDERPROG", "HASHTABLE", "AI_FSM", "AI_VISCACHE", + "AI_SEARCH", "AI_OBSTACLE", "JOBLIST", "TRANSPARENCY", "DETAIL", + "RESOURCE", "FILE_RESOURCE", "RESOURCE_BGL", "RESOURCE_BGL_RING", + "RESOURCE_BGL_OVERSIZE", "RESOURCE_MGR", "PVS", "DEFERRED_VIS", "FIBER", + "SUPERSCRIPT", "FX", "SAVEGAMES", "AI_TRANSITIONS", "AI_STATEDATA", + "AI_COMBATANIM", "TYPEINFO", "DAMAGEDECAL", "TABLE", "VIDEO", "NAVPOWER", + "LANGDICT", "SPLINE", "BINK", "FONTS", "EVENT_LISTENER", "PHYSICAL_BLOCK", + "DXOBJECT" +}; + +void AddRecord(void* pointer, const unsigned int bytes, const memTag_t tag, + const heapType_t heap, const char* location) { + allocationRecord_t* const record = static_cast( + std::malloc(sizeof(allocationRecord_t)) + ); + if (record == nullptr) { + return; + } + record->pointer = pointer; + record->bytes = bytes; + record->tag = tag; + record->heap = heap; + record->location = location; + AcquireSRWLockExclusive(&allocationLock); + record->next = allocations; + allocations = record; + ReleaseSRWLockExclusive(&allocationLock); + allocatedBytes.fetch_add(bytes, std::memory_order_relaxed); +} + +unsigned int RemoveRecord(void* pointer) { + unsigned int bytes = 0; + AcquireSRWLockExclusive(&allocationLock); + allocationRecord_t** link = &allocations; + while (*link != nullptr) { + if ((*link)->pointer == pointer) { + allocationRecord_t* const record = *link; + *link = record->next; + bytes = record->bytes; + std::free(record); + break; + } + link = &(*link)->next; + } + ReleaseSRWLockExclusive(&allocationLock); + if (bytes != 0) { + allocatedBytes.fetch_sub(bytes, std::memory_order_relaxed); + } + return bytes; +} +} + +idMem mem; +idMemLocal memLocal; + +const char* GetMemTagName(const int tag) { + return tag >= 0 && tag < TAG_NUM_TAGS ? tagNames[tag] : ""; +} + +void* idMem::AllocWithLocation(const char* const location, + const unsigned int size, const memTag_t tag, const bool zeroBuffer, + const align_t alignment, const heapType_t requestedHeap) { + const std::size_t allocationSize = size == 0 ? 1u : size; + const std::size_t requestedAlignment = static_cast(alignment); + const std::size_t safeAlignment = requestedAlignment < sizeof(void*) + ? sizeof(void*) : requestedAlignment; + + void* pointer = _aligned_malloc(allocationSize, safeAlignment); + if (pointer == nullptr) { + idOutOfMemoryCallback callback = outOfMemoryCallback.load( + std::memory_order_acquire + ); + if (callback != nullptr && callback()) { + pointer = _aligned_malloc(allocationSize, safeAlignment); + } + } + if (pointer == nullptr) { + return nullptr; + } + if (zeroBuffer) { + std::memset(pointer, 0, allocationSize); + } + const heapType_t usedHeap = requestedHeap == HEAP_DEFAULTHEAP + ? currentHeap : requestedHeap; + AddRecord(pointer, size, tag, usedHeap, location); + return pointer; +} + +void idMem::Free(void* const pointer, const align_t) { + if (pointer == nullptr) { + return; + } + RemoveRecord(pointer); + _aligned_free(pointer); +} + +int idMem::BytesCurrentlyAllocated() const { + const unsigned long long bytes = allocatedBytes.load(std::memory_order_relaxed); + return bytes > static_cast((std::numeric_limits::max)()) + ? (std::numeric_limits::max)() : static_cast(bytes); +} + +void idMem::InitMapHeap() { +} + +void idMem::ResetMapHeap() { + // The PC port uses the process CRT heap for both logical heaps. Allocations + // remain individually tracked so stale map allocations can still be found. +} + +void idMem::PushHeap(const heapType_t heapType) { + if (heapStackDepth++ == 0) { + pushedHeap = currentHeap; + currentHeap = heapType == HEAP_DEFAULTHEAP ? HEAP_MAPHEAP : heapType; + } +} + +void idMem::PopHeap() { + if (heapStackDepth <= 0) { + heapStackDepth = 0; + return; + } + if (--heapStackDepth == 0) { + currentHeap = pushedHeap; + } +} + +bool idMem::IsGlobalHeap() const { + return currentHeap == HEAP_SYSTEMHEAP; +} + +void idMem::SetOutOfMemoryCallback(const idOutOfMemoryCallback callback) { + outOfMemoryCallback.store(callback, std::memory_order_release); +} + +idOutOfMemoryCallback idMem::GetOutOfMemoryCallback() const { + return outOfMemoryCallback.load(std::memory_order_acquire); +} + +void idMem::WriteMemoryReport(const char* const directory, + const char* const fileName) const { + char path[MAX_PATH]; + const char* const safeDirectory = directory == nullptr ? "." : directory; + const char* const safeFile = fileName == nullptr ? "memory_report.txt" : fileName; + std::snprintf(path, sizeof(path), "%s\\%s", safeDirectory, safeFile); + FILE* report = nullptr; + if (fopen_s(&report, path, "w") != 0 || report == nullptr) { + return; + } + std::fprintf(report, "bytes currently allocated: %d\n", BytesCurrentlyAllocated()); + AcquireSRWLockShared(&allocationLock); + for (allocationRecord_t* record = allocations; record != nullptr; + record = record->next) { + std::fprintf(report, "%p %10u %-24s heap=%d %s\n", record->pointer, + record->bytes, GetMemTagName(record->tag), record->heap, + record->location == nullptr ? "" : record->location); + } + ReleaseSRWLockShared(&allocationLock); + std::fclose(report); +} diff --git a/source/shared/idlib/sys/sys_alloc.h b/source/shared/idlib/sys/sys_alloc.h new file mode 100644 index 0000000..8469082 --- /dev/null +++ b/source/shared/idlib/sys/sys_alloc.h @@ -0,0 +1,133 @@ +#pragma once + +#include + +enum memTag_t : int { + TAG_UNSET = 0x00, TAG_STATIC_EXE, TAG_RESOURCE_GRAPH, TAG_DEBUG, + TAG_NEW, TAG_IDLIST, TAG_TEMP, TAG_STRING, TAG_ATOMIC_STRING, + TAG_BITARRAY, TAG_MATH, TAG_LEXER, TAG_COMPILER, TAG_COLLISION, + TAG_COLLISION_QUERY, TAG_CLIPMODEL, TAG_MORPH, TAG_MD6_MISC, + TAG_MD6_NODES, TAG_MD6, TAG_MD6_ANIMS, TAG_MD6_LIPSYNC, + TAG_MD6_JOINTCACHE, TAG_MD6_MESHES, TAG_MD6_JOINTBUFFERS, + TAG_MD6_BLENDSTACK, TAG_MD6_JOINTMODS, TAG_MD6_COLLISION, + TAG_MD6_ANIMEVENTS, TAG_MD6_PHASE_TRACK, TAG_ANIMATION, + TAG_ANIMATION_DEBUG, TAG_DECL_ANIMWEB, TAG_ANIMWEB, TAG_IMAGE, + TAG_DXIMAGE, TAG_VIRTUALTEXTURE, TAG_AAS, TAG_SOUND, TAG_SOUND_BSP, + TAG_SOUND_DATA, TAG_SOUND_STREAM, TAG_SOUND_MULTISTREAM, + TAG_SOUND_SAMPLETABLES, TAG_IDLIB, TAG_TRIANGLES, TAG_DECL, + TAG_DECLTEXT, TAG_FILE, TAG_CVAR, TAG_PAGEFILECACHE, TAG_IDCLASS, + TAG_PRESENTABLE, TAG_FOLIAGE, TAG_WATER, TAG_MEMORY_MAPPED_FILE, + TAG_RENDERPARM, TAG_NETWORKING, TAG_SCRIPT, TAG_FXPHYSICS, TAG_LWO, + TAG_RENDERWORLD, TAG_RENDERER, TAG_AI_GAMESTATE, TAG_EVENTS, + TAG_VOICEOVER, TAG_VOICETRACK_EVENTS, TAG_VOICETRACK_FRAMEREFS, + TAG_VOICETRACK_PHONEMES, TAG_VISEMESET_VISEMES, + TAG_VISEMESET_PHONEMES, TAG_AF, TAG_SWF, TAG_GUI, TAG_GUI_MODEL, + TAG_FUNC_CALLBACK, TAG_MENU, TAG_GAME, TAG_HASHINDEX, TAG_PARTICLE, + TAG_EFFECT_PARTICLE, TAG_CLOTH, TAG_ANIMTAGS, TAG_IK, TAG_STATICMODEL, + TAG_RENDERMODEL, TAG_DXBUFFER, TAG_TOOLS, TAG_CLOUD, TAG_AMQP, + TAG_RENDERPROG, TAG_HASHTABLE, TAG_AI_FSM, TAG_AI_VISCACHE, + TAG_AI_SEARCH, TAG_AI_OBSTACLE, TAG_JOBLIST, TAG_TRANSPARENCY, + TAG_DETAIL, TAG_RESOURCE, TAG_FILE_RESOURCE, TAG_RESOURCE_BGL, + TAG_RESOURCE_BGL_RING, TAG_RESOURCE_BGL_OVERSIZE, TAG_RESOURCE_MGR, + TAG_PVS, TAG_DEFERRED_VIS, TAG_FIBER, TAG_SUPERSCRIPT, TAG_FX, + TAG_SAVEGAMES, TAG_AI_TRANSITIONS, TAG_AI_STATEDATA, TAG_AI_COMBATANIM, + TAG_TYPEINFO, TAG_DAMAGEDECAL, TAG_TABLE, TAG_VIDEO, TAG_NAVPOWER, + TAG_LANGDICT, TAG_SPLINE, TAG_BINK, TAG_FONTS, TAG_EVENT_LISTENER, + TAG_PHYSICAL_BLOCK, TAG_DXOBJECT, TAG_NUM_TAGS +}; + +enum align_t : int { + ALIGN_16 = 0x10, + ALIGN_128 = 0x80, + ALIGN_1M = 0x100000 +}; + +enum heapType_t : int { + HEAP_DEFAULTHEAP = -1, + HEAP_SYSTEMHEAP = 0, + HEAP_MAPHEAP = 1 +}; + +using idOutOfMemoryCallback = bool (*)(); + +class idMem { +public: + void* AllocWithLocation(const char* location, unsigned int size, + memTag_t tag, bool zeroBuffer = false, align_t alignment = ALIGN_16, + heapType_t heap = HEAP_DEFAULTHEAP); + void Free(void* pointer, align_t alignment = ALIGN_16); + + int BytesCurrentlyAllocated() const; + void InitMapHeap(); + void ResetMapHeap(); + void PushHeap(heapType_t heapType); + void PopHeap(); + bool IsGlobalHeap() const; + + void SetOutOfMemoryCallback(idOutOfMemoryCallback callback); + idOutOfMemoryCallback GetOutOfMemoryCallback() const; + void WriteMemoryReport(const char* directory, const char* fileName) const; +}; + +class idMemLocal : public idMem { +}; + +extern idMem mem; +extern idMemLocal memLocal; + +const char* GetMemTagName(int tag); +bool Sys_AllocWillUseMapHeap(); +void Sys_ReportHeaps(); +void ReportGlobalMemoryStatus(); + +void* Sys_Alloc(unsigned int size, memTag_t tag, + align_t alignment = ALIGN_16, + heapType_t heap = HEAP_DEFAULTHEAP); +void Sys_Free(void* pointer); +unsigned int Sys_GetStreamFileCacheUsage(); +unsigned int Sys_GetMemoryUsage(); +unsigned int Sys_GetFreeMemory(); +void Sys_WriteMemoryReport(const char* mapName, const char* version); +void Sys_DumpMemory(); + +class idPhysicalMemoryBlock { +public: + idPhysicalMemoryBlock(); + + void Init(int bytesToAllocate); + void RevertToDiscreteAllocations(); + void BeginResourceLoads(); + void EndResourceLoads(bool neverFreeAllocatedData); + void* PhysicalAlloc(unsigned int bytes, int alignment, memTag_t tag); + void* OverlayAlloc(unsigned int bytes, const char* name); + void OverlayFree(void* pointer); + bool AddressIsInReservedPhysicalMemoryBlock(const void* pointer) const; + bool AddressIsInOverlayPhysicalMemoryBlock(const void* pointer) const; + void ReportPhysicalMemoryBlock() const; + void ReportUntouchedPhysicalMemory() const; + +private: + unsigned char* reservedPhysicalMemoryBlock; + int totalBlockSize; + int commonBytes; + int overlayBytes; + int cacheBytes; + bool insideResourceBlockLoad; + int physicalBytesAllocated; + int imageBytesAllocated; + int bufferBytesAllocated; + int otherBytesAllocated; + int alignmentWaste; + int bytesForcedOutsideBlock; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idPhysicalMemoryBlock) == 48, + "Recovered idPhysicalMemoryBlock ABI changed"); +#endif + +class idScopedGlobalHeap { +public: + idScopedGlobalHeap() { mem.PushHeap(HEAP_SYSTEMHEAP); } + ~idScopedGlobalHeap() { mem.PopHeap(); } +}; diff --git a/source/shared/idlib/sys/sys_mgrd.cpp b/source/shared/idlib/sys/sys_mgrd.cpp new file mode 100644 index 0000000..60b2c07 --- /dev/null +++ b/source/shared/idlib/sys/sys_mgrd.cpp @@ -0,0 +1,15 @@ +#include "sys_mgrd.h" + +// MGRD was an optional external memory-profiler transport selected with the +// Xbox-era -mgrd switch. The PC recovery keeps the instrumentation API intact +// while making it a no-op until a desktop profiler backend is selected. +void RD_Init() {} +void RD_CreateGPUHeaps(void*, unsigned int, void*, unsigned int) {} +void RD_DestroyGPUHeaps() {} +void RD_CreateMapHeap() {} +void RD_DestroyMapHeap() {} +void RD_MemAlloc(void*, unsigned int, unsigned int, int) {} +void RD_MemFree(void*, int) {} +void RD_EventBegin(const char*) {} +void RD_EventEnd() {} +void RD_Syncpoint(const char*) {} diff --git a/source/shared/idlib/sys/sys_mgrd.h b/source/shared/idlib/sys/sys_mgrd.h new file mode 100644 index 0000000..15b5a86 --- /dev/null +++ b/source/shared/idlib/sys/sys_mgrd.h @@ -0,0 +1,19 @@ +#pragma once + +void RD_Init(); +void RD_CreateGPUHeaps(void* gpuMemory, unsigned int gpuBytes, + void* systemMemory, unsigned int systemBytes); +void RD_DestroyGPUHeaps(); +void RD_CreateMapHeap(); +void RD_DestroyMapHeap(); +void RD_MemAlloc(void* pointer, unsigned int size, unsigned int waste, int heap); +void RD_MemFree(void* pointer, int heap); +void RD_EventBegin(const char* name); +void RD_EventEnd(); +void RD_Syncpoint(const char* name); + +class idRDScopedEvent { +public: + explicit idRDScopedEvent(const char* name) { RD_EventBegin(name); } + ~idRDScopedEvent() { RD_EventEnd(); } +}; diff --git a/source/shared/idlib/sys/sys_networking.h b/source/shared/idlib/sys/sys_networking.h new file mode 100644 index 0000000..a6e8ddd --- /dev/null +++ b/source/shared/idlib/sys/sys_networking.h @@ -0,0 +1,123 @@ +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +#include + +#ifndef __SYS_PUBLIC__ +enum netadrtype_t { + NA_BAD = 0, + NA_LOOPBACK, + NA_BROADCAST, + NA_IP +}; + +struct netadr_t { + netadrtype_t type; + unsigned char ip[4]; + unsigned short port; +}; +#endif + +class idSimpleSerializer { +public: + idSimpleSerializer(unsigned char* buffer, int bufferSize, bool write) + : data(buffer), size(bufferSize), pos(0), writing(write) { + } + + bool Serialize(unsigned char& value); + bool Serialize(unsigned int& value); + bool SerializeBytes(char* bytes, unsigned int& numBytes); + bool SerializeString(char* text, int maxSize); + + int GetPos() const { return pos; } + int GetSize() const { return size; } + int GetSerializedSize() const { return writing ? pos : size; } + bool IsWriting() const { return writing; } + +private: + unsigned char* data; + int size; + int pos; + bool writing; +}; + +#ifndef __SYS_PUBLIC__ +class idUDP { +public: + idUDP(); + virtual ~idUDP(); + + bool InitForPort(int portNumber, bool useBackend = false); + void Close(); + bool GetPacket(netadr_t& from, void* data, int& size, int maxSize); + bool GetPacketBlocking(netadr_t& from, void* data, int& size, int maxSize, + int timeoutMS); + void SendPacket(netadr_t to, const void* data, int size); + + int GetPort() const { return bound_to.port; } + netadr_t GetAdr() const { return bound_to; } + bool IsOpen() const { return netSocket != 0; } + void SetSilent(bool value) { silent = value; } + bool GetSilent() const { return silent; } + + int packetsRead; + int bytesRead; + int packetsWritten; + int bytesWritten; + +private: + netadr_t bound_to; + int netSocket; + bool silent; +}; +#endif + +class idTCP { +public: + idTCP(); + virtual ~idTCP(); + + bool Connect(const char* host, unsigned short port, bool nonBlocking = false, + bool silent = false, bool nagle = true); + bool Select(int timeoutMS); + bool IsOpen() const; + void Close(); + int Read(void* data, int size); + int ReadBlocking(void* data, int size, int timeoutMS); + int Write(const void* data, int size); + int WriteBlocking(const void* data, int size, int timeoutMS); + bool WriteDataBlock(const char* buffer, int size, int timeoutMS); + int ReadDataBlock(char* buffer, int bufferSize, int timeoutMS); + + netadr_t GetAddress() const { return address; } + +private: + netadr_t address; + int fd; +}; + +#ifndef __SYS_PUBLIC__ +void Sys_InitNetworking(); +void Sys_ShutdownNetworking(); +bool Sys_StringToNetAdr(const char* text, netadr_t* address, bool doDNSResolve); +const char* Sys_NetAdrToString(const netadr_t& address); +bool Sys_IsLANAddress(const netadr_t& address); +bool Sys_CompareNetAdrBase(const netadr_t& left, const netadr_t& right); +int Sys_GetLocalIPCount(); +const char* Sys_GetLocalIP(int index); +#endif + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(netadr_t) == 12, "Recovered netadr_t ABI changed"); +static_assert(sizeof(idSimpleSerializer) == 16, + "Recovered idSimpleSerializer ABI changed"); +#ifndef __SYS_PUBLIC__ +static_assert(sizeof(idUDP) == 40, "Recovered idUDP ABI changed"); +#endif +static_assert(sizeof(idTCP) == 20, "Recovered idTCP ABI changed"); +#endif diff --git a/source/shared/idlib/sys/sys_time.cpp b/source/shared/idlib/sys/sys_time.cpp new file mode 100644 index 0000000..14055d1 --- /dev/null +++ b/source/shared/idlib/sys/sys_time.cpp @@ -0,0 +1,115 @@ +#include "sys_time.h" + +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace { +thread_local char timeString[128]; + +bool LocalTime(const std::time_t value, std::tm& output) { +#if defined(_WIN32) + return localtime_s(&output, &value) == 0; +#else + return localtime_r(&value, &output) != nullptr; +#endif +} +} + +std::uint64_t Sys_CurrentSystemTime() { +#if defined(_WIN32) + FILETIME fileTime; + GetSystemTimeAsFileTime(&fileTime); + return (static_cast(fileTime.dwHighDateTime) << 32) + | static_cast(fileTime.dwLowDateTime); +#else + constexpr std::uint64_t windowsEpochOffset = 116444736000000000ULL; + const auto ticks = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count() / 100; + return windowsEpochOffset + static_cast(ticks); +#endif +} + +char* Sys_TimeStampToLogFormat(const int timeStamp) { + timeString[0] = '\0'; + if (timeStamp == -1) { + return timeString; + } + const std::time_t source = static_cast(timeStamp); + std::tm local = {}; + if (LocalTime(source, local)) { + std::snprintf(timeString, sizeof(timeString), + "%d-%02d-%02dT%02d:%02d:%02dZ", + local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, + local.tm_hour, local.tm_min, local.tm_sec); + } + return timeString; +} + +char* Sys_TimeStampToStr(const int timeStamp, const bool padded) { + timeString[0] = '\0'; + if (timeStamp == -1) { + return timeString; + } + const std::time_t source = static_cast(timeStamp); + std::tm local = {}; + if (!LocalTime(source, local)) { + return timeString; + } + + int hour = local.tm_hour % 12; + if (hour == 0) { + hour = 12; + } + if (padded) { + std::snprintf(timeString, sizeof(timeString), + "%02d/%02d/%d %02d:%02d:%02d%s", + local.tm_mon + 1, local.tm_mday, local.tm_year + 1900, + hour, local.tm_min, local.tm_sec, + local.tm_hour < 12 ? "am" : "pm"); + } else { + std::snprintf(timeString, sizeof(timeString), + "%d/%d/%d %d:%02d:%02d%s", + local.tm_mon + 1, local.tm_mday, local.tm_year + 1900, + hour, local.tm_min, local.tm_sec, + local.tm_hour < 12 ? "am" : "pm"); + } + return timeString; +} + +char* Sys_DateStr(const bool padded) { + return Sys_TimeStampToStr(static_cast(std::time(nullptr)), padded); +} + +idStr Sys_SecToStr(int seconds) { + idStr result; + if (seconds < 0) { + seconds = 0; + } + char buffer[32]; + const int weeks = seconds / 604800; + if (weeks > 0) { + std::snprintf(buffer, sizeof(buffer), "%dw, ", weeks); + result.Append(buffer); + seconds %= 604800; + } + const int days = seconds / 86400; + if (weeks > 0 || days > 0) { + std::snprintf(buffer, sizeof(buffer), "%dd, ", days); + result.Append(buffer); + seconds %= 86400; + } + const int hours = seconds / 3600; + const int minutes = (seconds % 3600) / 60; + const int remainingSeconds = seconds % 60; + std::snprintf(buffer, sizeof(buffer), "%d:%02d:%02d", + hours, minutes, remainingSeconds); + result.Append(buffer); + return result; +} diff --git a/source/shared/idlib/sys/sys_time.h b/source/shared/idlib/sys/sys_time.h new file mode 100644 index 0000000..dee2ded --- /dev/null +++ b/source/shared/idlib/sys/sys_time.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "../text/str.h" + +std::uint64_t Sys_CurrentSystemTime(); +char* Sys_TimeStampToLogFormat(int timeStamp); +char* Sys_TimeStampToStr(int timeStamp, bool padded); +char* Sys_DateStr(bool padded); +idStr Sys_SecToStr(int seconds); diff --git a/source/shared/idlib/sys/sys_utils.h b/source/shared/idlib/sys/sys_utils.h new file mode 100644 index 0000000..30e59c8 --- /dev/null +++ b/source/shared/idlib/sys/sys_utils.h @@ -0,0 +1,5 @@ +#pragma once + +const char* Sys_GetOSUserName(); +const char* Sys_GetMachineName(); + diff --git a/source/shared/idlib/sys/win32/win_fibers.cpp b/source/shared/idlib/sys/win32/win_fibers.cpp new file mode 100644 index 0000000..98366e9 --- /dev/null +++ b/source/shared/idlib/sys/win32/win_fibers.cpp @@ -0,0 +1,63 @@ +#include "win_fibers.h" + +#include +#include + +namespace { + +char* CopyFiberName(const char* source) { + const char* const text = source == nullptr ? "" : source; + const std::size_t length = std::strlen(text); + char* const copy = static_cast(std::malloc(length + 1)); + if (copy != nullptr) { + std::memcpy(copy, text, length + 1); + } + return copy; +} + +} // namespace + +idSysFiber::idSysFiber(const char* fiberName) + : name(CopyFiberName(fiberName)) + , alive(true) + , fiber(nullptr) + , parent(nullptr) { + fiber = CreateFiber(0x20000u, &idSysFiber::FiberRoutine, this); + if (fiber == nullptr) { + alive = false; + } +} + +idSysFiber::~idSysFiber() { + std::free(name); + if (fiber != nullptr) { + DeleteFiber(fiber); + } +} + +bool idSysFiber::Execute() { + if (alive && fiber != nullptr) { + parent = GetCurrentFiber(); + SwitchToFiber(fiber); + } + return alive; +} + +void idSysFiber::YieldFiber() { + if (parent != nullptr) { + SwitchToFiber(parent); + } +} + +void WINAPI idSysFiber::FiberRoutine(void* data) { + idSysFiber* const self = static_cast(data); + self->Run(); + self->alive = false; + self->YieldFiber(); + + // A finished Windows fiber must never return from its entry routine when + // its parent is expected to retain control. Match the recovered guard. + for (;;) { + self->YieldFiber(); + } +} diff --git a/source/shared/idlib/sys/win32/win_fibers.h b/source/shared/idlib/sys/win32/win_fibers.h new file mode 100644 index 0000000..39a5704 --- /dev/null +++ b/source/shared/idlib/sys/win32/win_fibers.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +class idSysFiber { +public: + explicit idSysFiber(const char* name); + virtual ~idSysFiber(); + + idSysFiber(const idSysFiber&) = delete; + idSysFiber& operator=(const idSysFiber&) = delete; + + bool Execute(); + virtual void Run() = 0; + +protected: + void YieldFiber(); + +private: + static void WINAPI FiberRoutine(void* data); + + char* name; + bool alive; + void* fiber; + void* parent; +}; + +static_assert(sizeof(idSysFiber) == 20, "Recovered idSysFiber ABI changed"); diff --git a/source/shared/idlib/sys/win32/win_memshared.cpp b/source/shared/idlib/sys/win32/win_memshared.cpp new file mode 100644 index 0000000..5b2a0b1 --- /dev/null +++ b/source/shared/idlib/sys/win32/win_memshared.cpp @@ -0,0 +1,23 @@ +#include "../sys_alloc.h" + +#include +#include + +bool Sys_AllocWillUseMapHeap() { + return !mem.IsGlobalHeap(); +} + +void Sys_ReportHeaps() { + std::printf("PC logical heap: %s, tracked bytes: %d\n", + mem.IsGlobalHeap() ? "global" : "map", mem.BytesCurrentlyAllocated()); +} + +void ReportGlobalMemoryStatus() { + MEMORYSTATUSEX status = {}; + status.dwLength = sizeof(status); + if (GlobalMemoryStatusEx(&status)) { + std::printf("physical memory: %llu / %llu bytes available\n", + static_cast(status.ullAvailPhys), + static_cast(status.ullTotalPhys)); + } +} diff --git a/source/shared/idlib/sys/xenon/xen_mem.cpp b/source/shared/idlib/sys/xenon/xen_mem.cpp new file mode 100644 index 0000000..1d9938e --- /dev/null +++ b/source/shared/idlib/sys/xenon/xen_mem.cpp @@ -0,0 +1,203 @@ +#include "idlib/sys/sys_alloc.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#include +#include +#include +#include + +namespace { + +unsigned int ClampToUInt64(const unsigned long long value) { + return value > (std::numeric_limits::max)() + ? (std::numeric_limits::max)() + : static_cast(value); +} + +int AlignUp(const int value, const int alignment) { + const int safeAlignment = alignment <= 0 ? 16 : alignment; + return (value + safeAlignment - 1) & ~(safeAlignment - 1); +} + +} // namespace + +void MapVirtualAddressSpace() { + // Win32 already supplies a flat virtual address space. Logical map/system + // heap selection remains handled by idMem. +} + +unsigned int Sys_GetStreamFileCacheUsage() { + // The PC filesystem owns its cache allocations through idMem rather than + // a carved physical-memory range. + return 0; +} + +unsigned int Sys_GetMemoryUsage() { + PROCESS_MEMORY_COUNTERS_EX counters = {}; + counters.cb = sizeof(counters); + if (!GetProcessMemoryInfo(GetCurrentProcess(), + reinterpret_cast(&counters), + sizeof(counters))) { + return static_cast(mem.BytesCurrentlyAllocated()); + } + return ClampToUInt64(counters.PrivateUsage); +} + +unsigned int Sys_GetFreeMemory() { + MEMORYSTATUSEX status = {}; + status.dwLength = sizeof(status); + return GlobalMemoryStatusEx(&status) ? ClampToUInt64(status.ullAvailPhys) : 0; +} + +void Sys_WriteMemoryReport(const char* mapName, const char* version) { + char fileName[192] = {}; + std::snprintf(fileName, sizeof(fileName), "memory_%s_%s.txt", + mapName == nullptr || mapName[0] == '\0' ? "nomap" : mapName, + version == nullptr || version[0] == '\0' ? "unknown" : version); + for (char* cursor = fileName; *cursor != '\0'; ++cursor) { + if (*cursor == '\\' || *cursor == '/' || *cursor == ':' + || *cursor == '*' || *cursor == '?' || *cursor == '"' + || *cursor == '<' || *cursor == '>' || *cursor == '|') { + *cursor = '_'; + } + } + mem.WriteMemoryReport(".", fileName); +} + +void Sys_DumpMemory() { + mem.WriteMemoryReport(".", "memory_dump.txt"); +} + +void AddTagStats(int, int, int, int) { + // idMem records tags per allocation, so a second accounting table would + // double count on PC. +} + +void SubtractTagStats(int, int, int, int) { +} + +void* XMemAlloc(const unsigned int size, const int) { + return mem.AllocWithLocation("XMemAlloc PC replacement", size, TAG_IDLIB, + false, ALIGN_16, HEAP_DEFAULTHEAP); +} + +void XMemFree(unsigned char* pointer, const unsigned int) { + mem.Free(pointer); +} + +void* Sys_Alloc(const unsigned int size, const memTag_t tag, + const align_t alignment, const heapType_t heap) { + return mem.AllocWithLocation("Sys_Alloc PC replacement", size, tag, + false, alignment, heap); +} + +void Sys_Free(void* pointer) { + mem.Free(pointer); +} + +idPhysicalMemoryBlock::idPhysicalMemoryBlock() + : reservedPhysicalMemoryBlock(nullptr), totalBlockSize(0), commonBytes(0), + overlayBytes(0), cacheBytes(0), insideResourceBlockLoad(false), + physicalBytesAllocated(0), imageBytesAllocated(0), bufferBytesAllocated(0), + otherBytesAllocated(0), alignmentWaste(0), bytesForcedOutsideBlock(0) { +} + +void idPhysicalMemoryBlock::Init(const int bytesToAllocate) { + if (reservedPhysicalMemoryBlock != nullptr || bytesToAllocate <= 0) return; + totalBlockSize = AlignUp(bytesToAllocate, 65536); + reservedPhysicalMemoryBlock = static_cast( + mem.AllocWithLocation("idPhysicalMemoryBlock PC reserve", + totalBlockSize, TAG_PHYSICAL_BLOCK, true, ALIGN_16, + HEAP_SYSTEMHEAP)); + if (reservedPhysicalMemoryBlock == nullptr) totalBlockSize = 0; +} + +void idPhysicalMemoryBlock::RevertToDiscreteAllocations() { + if (physicalBytesAllocated != 0) return; + mem.Free(reservedPhysicalMemoryBlock); + reservedPhysicalMemoryBlock = nullptr; + totalBlockSize = 0; + commonBytes = overlayBytes = cacheBytes = 0; +} + +void idPhysicalMemoryBlock::BeginResourceLoads() { + insideResourceBlockLoad = true; +} + +void idPhysicalMemoryBlock::EndResourceLoads(const bool neverFreeAllocatedData) { + insideResourceBlockLoad = false; + physicalBytesAllocated = std::min(AlignUp(physicalBytesAllocated, 65536), + totalBlockSize); + if (neverFreeAllocatedData) commonBytes = physicalBytesAllocated; + cacheBytes = std::max(0, totalBlockSize - physicalBytesAllocated); + overlayBytes = 0; +} + +void* idPhysicalMemoryBlock::PhysicalAlloc(const unsigned int bytes, + const int alignment, const memTag_t tag) { + const int alignedOffset = AlignUp(physicalBytesAllocated, alignment); + alignmentWaste += alignedOffset - physicalBytesAllocated; + if (reservedPhysicalMemoryBlock != nullptr + && alignedOffset >= 0 + && bytes <= static_cast(totalBlockSize - alignedOffset)) { + physicalBytesAllocated = alignedOffset + static_cast(bytes); + if (tag == TAG_DXIMAGE) imageBytesAllocated += bytes; + else if (tag == TAG_DXBUFFER) bufferBytesAllocated += bytes; + else otherBytesAllocated += bytes; + return reservedPhysicalMemoryBlock + alignedOffset; + } + bytesForcedOutsideBlock += bytes; + return mem.AllocWithLocation("physical allocation fallback", bytes, tag, + false, alignment >= ALIGN_128 ? ALIGN_128 : ALIGN_16, + HEAP_SYSTEMHEAP); +} + +void* idPhysicalMemoryBlock::OverlayAlloc(const unsigned int bytes, + const char*) { + // PC resources are individually reclaimable; keeping overlays discrete + // avoids the Xenon-only 64 KiB overlay fragmentation rules. + return mem.AllocWithLocation("overlay allocation PC replacement", bytes, + TAG_PHYSICAL_BLOCK, false, ALIGN_16, HEAP_SYSTEMHEAP); +} + +void idPhysicalMemoryBlock::OverlayFree(void* pointer) { + if (!AddressIsInReservedPhysicalMemoryBlock(pointer)) mem.Free(pointer); +} + +bool idPhysicalMemoryBlock::AddressIsInReservedPhysicalMemoryBlock( + const void* pointer) const { + const unsigned char* const address = static_cast(pointer); + return reservedPhysicalMemoryBlock != nullptr + && address >= reservedPhysicalMemoryBlock + && address < reservedPhysicalMemoryBlock + totalBlockSize; +} + +bool idPhysicalMemoryBlock::AddressIsInOverlayPhysicalMemoryBlock( + const void* pointer) const { + if (!AddressIsInReservedPhysicalMemoryBlock(pointer) || overlayBytes <= 0) { + return false; + } + const unsigned char* const overlayStart = + reservedPhysicalMemoryBlock + totalBlockSize - overlayBytes; + return static_cast(pointer) >= overlayStart; +} + +void idPhysicalMemoryBlock::ReportPhysicalMemoryBlock() const { + std::printf("physical block: %d/%d bytes, images=%d buffers=%d other=%d " + "alignment=%d fallback=%d\n", physicalBytesAllocated, totalBlockSize, + imageBytesAllocated, bufferBytesAllocated, otherBytesAllocated, + alignmentWaste, bytesForcedOutsideBlock); +} + +void idPhysicalMemoryBlock::ReportUntouchedPhysicalMemory() const { + std::printf("physical block untouched/available: %d bytes\n", + std::max(0, totalBlockSize - physicalBytesAllocated)); +} diff --git a/source/shared/idlib/sys/xenon/xen_net.cpp b/source/shared/idlib/sys/xenon/xen_net.cpp new file mode 100644 index 0000000..abcd0a1 --- /dev/null +++ b/source/shared/idlib/sys/xenon/xen_net.cpp @@ -0,0 +1,431 @@ +#include "idlib/sys/sys_networking.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::mutex networkMutex; +bool networkInitialized = false; +std::vector localAddresses; + +SOCKET ToSocket(const int value) { + return static_cast(static_cast(value)); +} + +int FromSocket(const SOCKET value) { + return static_cast(value); +} + +void ClearAddress(netadr_t& address) { + std::memset(&address, 0, sizeof(address)); + address.type = NA_BAD; +} + +void NetAdrToSockAdr(const netadr_t& source, sockaddr_in& destination) { + std::memset(&destination, 0, sizeof(destination)); + destination.sin_family = AF_INET; + if (source.type == NA_BROADCAST) { + destination.sin_addr.s_addr = INADDR_BROADCAST; + } else { + std::memcpy(&destination.sin_addr.s_addr, source.ip, sizeof(source.ip)); + } + destination.sin_port = htons(source.port); +} + +void SockAdrToNetAdr(const sockaddr_in& source, netadr_t& destination) { + std::memset(&destination, 0, sizeof(destination)); + std::memcpy(destination.ip, &source.sin_addr.s_addr, sizeof(destination.ip)); + destination.port = ntohs(source.sin_port); + destination.type = ntohl(source.sin_addr.s_addr) == INADDR_LOOPBACK + ? NA_LOOPBACK : NA_IP; +} + +bool WaitForSocket(const SOCKET socketValue, const int timeoutMS, + const bool write) { + if (socketValue == INVALID_SOCKET) return false; + fd_set set; + FD_ZERO(&set); + FD_SET(socketValue, &set); + timeval timeout = {}; + timeout.tv_sec = timeoutMS < 0 ? 0 : timeoutMS / 1000; + timeout.tv_usec = timeoutMS < 0 ? 0 : (timeoutMS % 1000) * 1000; + const int result = select(0, write ? nullptr : &set, + write ? &set : nullptr, nullptr, timeoutMS < 0 ? nullptr : &timeout); + return result > 0 && FD_ISSET(socketValue, &set) != 0; +} + +void EnsureNetworking() { + std::lock_guard guard(networkMutex); + if (networkInitialized) return; + WSADATA data = {}; + if (WSAStartup(MAKEWORD(2, 2), &data) != 0) { + return; + } + networkInitialized = true; + + localAddresses.clear(); + char hostName[256] = {}; + if (gethostname(hostName, sizeof(hostName)) == 0) { + addrinfo hints = {}; + hints.ai_family = AF_INET; + addrinfo* result = nullptr; + if (getaddrinfo(hostName, nullptr, &hints, &result) == 0) { + for (addrinfo* item = result; item != nullptr; item = item->ai_next) { + const sockaddr_in* address = + reinterpret_cast(item->ai_addr); + char text[INET_ADDRSTRLEN] = {}; + if (inet_ntop(AF_INET, &address->sin_addr, text, sizeof(text)) + != nullptr + && std::find(localAddresses.begin(), localAddresses.end(), text) + == localAddresses.end()) { + localAddresses.emplace_back(text); + } + } + freeaddrinfo(result); + } + } + if (localAddresses.empty()) localAddresses.emplace_back("127.0.0.1"); +} + +} // namespace + +bool idSimpleSerializer::Serialize(unsigned char& value) { + if (data == nullptr || pos < 0 || pos + 1 > size) return false; + if (writing) data[pos] = value; + else value = data[pos]; + ++pos; + return true; +} + +bool idSimpleSerializer::Serialize(unsigned int& value) { + if (data == nullptr || pos < 0 || pos + 4 > size) return false; + if (writing) { + data[pos + 0] = static_cast(value >> 0); + data[pos + 1] = static_cast(value >> 8); + data[pos + 2] = static_cast(value >> 16); + data[pos + 3] = static_cast(value >> 24); + } else { + value = static_cast(data[pos + 0]) + | (static_cast(data[pos + 1]) << 8) + | (static_cast(data[pos + 2]) << 16) + | (static_cast(data[pos + 3]) << 24); + } + pos += 4; + return true; +} + +bool idSimpleSerializer::SerializeBytes(char* bytes, unsigned int& numBytes) { + const unsigned int capacity = numBytes; + if (!Serialize(numBytes)) return false; + if (!writing && numBytes > capacity) { + numBytes = 0; + return false; + } + if (numBytes > static_cast(size - pos) + || (numBytes > 0 && bytes == nullptr)) { + return false; + } + if (writing) std::memcpy(data + pos, bytes, numBytes); + else std::memcpy(bytes, data + pos, numBytes); + pos += static_cast(numBytes); + return true; +} + +bool idSimpleSerializer::SerializeString(char* text, const int maxSize) { + if (text == nullptr || maxSize <= 0) return false; + unsigned int bytes = writing + ? static_cast(std::strlen(text)) + : static_cast(maxSize - 1); + if (!SerializeBytes(text, bytes)) return false; + if (!writing) text[bytes] = '\0'; + return true; +} + +void Sys_InitNetworking() { + EnsureNetworking(); +} + +void Sys_ShutdownNetworking() { + std::lock_guard guard(networkMutex); + if (!networkInitialized) return; + localAddresses.clear(); + WSACleanup(); + networkInitialized = false; +} + +bool Sys_StringToNetAdr(const char* text, netadr_t* address, + const bool doDNSResolve) { + if (text == nullptr || address == nullptr) return false; + EnsureNetworking(); + + std::string host(text); + unsigned short port = 0; + const std::size_t separator = host.rfind(':'); + if (separator != std::string::npos) { + const long parsed = std::strtol(host.c_str() + separator + 1, nullptr, 10); + if (parsed < 0 || parsed > 65535) return false; + port = static_cast(parsed); + host.resize(separator); + } + if (host == "localhost") host = "127.0.0.1"; + + sockaddr_in socketAddress = {}; + socketAddress.sin_family = AF_INET; + socketAddress.sin_port = htons(port); + if (inet_pton(AF_INET, host.c_str(), &socketAddress.sin_addr) != 1) { + if (!doDNSResolve) return false; + addrinfo hints = {}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + addrinfo* result = nullptr; + if (getaddrinfo(host.c_str(), nullptr, &hints, &result) != 0 + || result == nullptr) { + if (result != nullptr) freeaddrinfo(result); + return false; + } + socketAddress.sin_addr = + reinterpret_cast(result->ai_addr)->sin_addr; + freeaddrinfo(result); + } + SockAdrToNetAdr(socketAddress, *address); + return true; +} + +const char* Sys_NetAdrToString(const netadr_t& address) { + thread_local char result[64]; + const char* prefix = address.type == NA_LOOPBACK ? "127.0.0.1" : nullptr; + char ipText[INET_ADDRSTRLEN] = {}; + in_addr ipAddress = {}; + std::memcpy(&ipAddress.s_addr, address.ip, sizeof(address.ip)); + if (prefix == nullptr) { + prefix = inet_ntop(AF_INET, &ipAddress, ipText, sizeof(ipText)); + } + if (prefix == nullptr) prefix = "0.0.0.0"; + if (address.port != 0) { + std::snprintf(result, sizeof(result), "%s:%u", prefix, address.port); + } else { + std::snprintf(result, sizeof(result), "%s", prefix); + } + return result; +} + +bool Sys_IsLANAddress(const netadr_t& address) { + if (address.type == NA_LOOPBACK) return true; + if (address.type != NA_IP) return false; + return address.ip[0] == 10 + || (address.ip[0] == 172 && address.ip[1] >= 16 && address.ip[1] <= 31) + || (address.ip[0] == 192 && address.ip[1] == 168) + || address.ip[0] == 127; +} + +bool Sys_CompareNetAdrBase(const netadr_t& left, const netadr_t& right) { + if (left.type != right.type) return false; + if (left.type == NA_LOOPBACK) return true; + if (left.type != NA_IP && left.type != NA_BROADCAST) return false; + return std::memcmp(left.ip, right.ip, sizeof(left.ip)) == 0; +} + +int Sys_GetLocalIPCount() { + EnsureNetworking(); + return static_cast(localAddresses.size()); +} + +const char* Sys_GetLocalIP(const int index) { + EnsureNetworking(); + return index >= 0 && index < static_cast(localAddresses.size()) + ? localAddresses[index].c_str() : nullptr; +} + +idUDP::idUDP() + : packetsRead(0), bytesRead(0), packetsWritten(0), bytesWritten(0), + netSocket(0), silent(false) { + ClearAddress(bound_to); +} + +idUDP::~idUDP() { Close(); } + +bool idUDP::InitForPort(const int portNumber, const bool) { + Close(); + EnsureNetworking(); + const SOCKET socketValue = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (socketValue == INVALID_SOCKET) return false; + u_long nonBlocking = 1; + ioctlsocket(socketValue, FIONBIO, &nonBlocking); + BOOL broadcast = TRUE; + setsockopt(socketValue, SOL_SOCKET, SO_BROADCAST, + reinterpret_cast(&broadcast), sizeof(broadcast)); + + sockaddr_in bindAddress = {}; + bindAddress.sin_family = AF_INET; + bindAddress.sin_addr.s_addr = htonl(INADDR_ANY); + bindAddress.sin_port = htons(static_cast(portNumber)); + if (bind(socketValue, reinterpret_cast(&bindAddress), + sizeof(bindAddress)) == SOCKET_ERROR) { + closesocket(socketValue); + return false; + } + int addressLength = sizeof(bindAddress); + getsockname(socketValue, reinterpret_cast(&bindAddress), &addressLength); + SockAdrToNetAdr(bindAddress, bound_to); + netSocket = FromSocket(socketValue); + return true; +} + +void idUDP::Close() { + if (netSocket != 0) closesocket(ToSocket(netSocket)); + netSocket = 0; + ClearAddress(bound_to); +} + +bool idUDP::GetPacket(netadr_t& from, void* data, int& dataSize, + const int maxSize) { + if (!IsOpen() || data == nullptr || maxSize <= 0) return false; + sockaddr_in source = {}; + int sourceLength = sizeof(source); + const int received = recvfrom(ToSocket(netSocket), static_cast(data), + maxSize, 0, reinterpret_cast(&source), &sourceLength); + if (received == SOCKET_ERROR) return false; + SockAdrToNetAdr(source, from); + dataSize = received; + ++packetsRead; + bytesRead += received; + return true; +} + +bool idUDP::GetPacketBlocking(netadr_t& from, void* data, int& dataSize, + const int maxSize, const int timeoutMS) { + return WaitForSocket(ToSocket(netSocket), timeoutMS, false) + && GetPacket(from, data, dataSize, maxSize); +} + +void idUDP::SendPacket(const netadr_t to, const void* data, const int dataSize) { + if (!IsOpen() || data == nullptr || dataSize < 0 || to.type == NA_BAD) return; + sockaddr_in destination = {}; + NetAdrToSockAdr(to, destination); + const int sent = sendto(ToSocket(netSocket), static_cast(data), + dataSize, 0, reinterpret_cast(&destination), + sizeof(destination)); + if (sent >= 0) { + ++packetsWritten; + bytesWritten += sent; + } +} + +idTCP::idTCP() : fd(0) { ClearAddress(address); } +idTCP::~idTCP() { Close(); } + +bool idTCP::Connect(const char* host, const unsigned short port, + const bool nonBlocking, const bool, const bool nagle) { + Close(); + EnsureNetworking(); + if (!Sys_StringToNetAdr(host, &address, true)) return false; + if (address.port == 0) address.port = port; + address.type = address.type == NA_LOOPBACK ? NA_LOOPBACK : NA_IP; + sockaddr_in destination = {}; + NetAdrToSockAdr(address, destination); + const SOCKET socketValue = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (socketValue == INVALID_SOCKET) return false; + BOOL noDelay = nagle ? FALSE : TRUE; + setsockopt(socketValue, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&noDelay), sizeof(noDelay)); + if (nonBlocking) { + u_long mode = 1; + ioctlsocket(socketValue, FIONBIO, &mode); + } + const int result = connect(socketValue, + reinterpret_cast(&destination), sizeof(destination)); + if (result == SOCKET_ERROR) { + const int error = WSAGetLastError(); + if (!nonBlocking || (error != WSAEWOULDBLOCK + && error != WSAEINPROGRESS && error != WSAEALREADY)) { + closesocket(socketValue); + return false; + } + } + fd = FromSocket(socketValue); + return true; +} + +bool idTCP::Select(const int timeoutMS) { + return WaitForSocket(ToSocket(fd), timeoutMS, false); +} + +bool idTCP::IsOpen() const { return fd != 0; } + +void idTCP::Close() { + if (fd != 0) closesocket(ToSocket(fd)); + fd = 0; +} + +int idTCP::Read(void* data, const int dataSize) { + if (!IsOpen() || data == nullptr || dataSize < 0) return -1; + const int result = recv(ToSocket(fd), static_cast(data), dataSize, 0); + if (result > 0) return result; + if (result == 0) { + Close(); + return -2; + } + if (WSAGetLastError() == WSAEWOULDBLOCK) return 0; + Close(); + return -1; +} + +int idTCP::ReadBlocking(void* data, const int dataSize, const int timeoutMS) { + int total = 0; + while (total < dataSize && WaitForSocket(ToSocket(fd), timeoutMS, false)) { + const int received = Read(static_cast(data) + total, dataSize - total); + if (received < 0) return received; + total += received; + } + return total; +} + +int idTCP::Write(const void* data, const int dataSize) { + if (!IsOpen() || data == nullptr || dataSize < 0) return -1; + const int result = send(ToSocket(fd), static_cast(data), dataSize, 0); + if (result >= 0) return result; + if (WSAGetLastError() == WSAEWOULDBLOCK) return 0; + Close(); + return -1; +} + +int idTCP::WriteBlocking(const void* data, const int dataSize, const int timeoutMS) { + int total = 0; + while (total < dataSize && WaitForSocket(ToSocket(fd), timeoutMS, true)) { + const int sent = Write(static_cast(data) + total, dataSize - total); + if (sent < 0) return sent; + total += sent; + } + return total; +} + +bool idTCP::WriteDataBlock(const char* buffer, const int dataSize, + const int timeoutMS) { + unsigned char lengthData[4] = {}; + unsigned int length = static_cast(dataSize); + idSimpleSerializer serializer(lengthData, sizeof(lengthData), true); + return serializer.Serialize(length) + && WriteBlocking(lengthData, sizeof(lengthData), timeoutMS) + == sizeof(lengthData) + && WriteBlocking(buffer, dataSize, timeoutMS) == dataSize; +} + +int idTCP::ReadDataBlock(char* buffer, const int bufferSize, + const int timeoutMS) { + unsigned char lengthData[4] = {}; + if (ReadBlocking(lengthData, sizeof(lengthData), timeoutMS) + != sizeof(lengthData)) return -1; + unsigned int length = 0; + idSimpleSerializer serializer(lengthData, sizeof(lengthData), false); + if (!serializer.Serialize(length) || length > static_cast(bufferSize)) { + return -1; + } + return ReadBlocking(buffer, static_cast(length), timeoutMS) + == static_cast(length) ? static_cast(length) : -1; +} diff --git a/source/shared/idlib/sys/xenon/xen_utils.cpp b/source/shared/idlib/sys/xenon/xen_utils.cpp new file mode 100644 index 0000000..ac2ee0f --- /dev/null +++ b/source/shared/idlib/sys/xenon/xen_utils.cpp @@ -0,0 +1,25 @@ +#include "idlib/sys/sys_utils.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +const char* Sys_GetOSUserName() { + static char userName[64] = {}; + DWORD length = static_cast(sizeof(userName)); + if (!GetUserNameA(userName, &length)) { + userName[0] = '\0'; + } + return userName; +} + +const char* Sys_GetMachineName() { + static char machineName[64] = {}; + DWORD length = static_cast(sizeof(machineName)); + if (!GetComputerNameA(machineName, &length)) { + machineName[0] = '\0'; + } + return machineName; +} + diff --git a/source/shared/idlib/text/atomicstring.cpp b/source/shared/idlib/text/atomicstring.cpp new file mode 100644 index 0000000..416583c --- /dev/null +++ b/source/shared/idlib/text/atomicstring.cpp @@ -0,0 +1,106 @@ +#include "atomicstring.h" + +#include +#include +#include + +struct idAtomicStringManager::Entry { + Entry* next; + char text[1]; +}; + +idAtomicStringManager atomicStringManager; + +idAtomicStringManager::idAtomicStringManager() + : hashTable{}, hashTableAtMarkStatic{}, blocks(nullptr), + blocksAtMarkStatic(nullptr), usedBytesAtMarkStatic(0) { +} + +const char* idAtomicStringManager::MakeAtomic(const char* source) { + if (source == nullptr || source[0] == '\0') { + return ""; + } + + unsigned int hash = 0; + for (const unsigned char* cursor = + reinterpret_cast(source); + *cursor != 0; ++cursor) { + hash = 31u * hash + *cursor; + } + Entry*& bucket = hashTable[hash & 0xFFFFu]; + for (Entry* entry = bucket; entry != nullptr; entry = entry->next) { + if (std::strcmp(entry->text, source) == 0) { + return entry->text; + } + } + + const std::size_t textBytes = std::strlen(source) + 1; + const std::size_t entryBytes = (sizeof(Entry*) + textBytes + 3u) & ~3u; + if (blocks == nullptr + || entryBytes > static_cast(blocks->bufferBytes - blocks->usedBytes)) { + const std::size_t totalBytes = std::max( + 65536u, sizeof(atomicStringBlock_t) + entryBytes + ); + atomicStringBlock_t* const block = static_cast( + std::malloc(totalBytes) + ); + if (block == nullptr) { + return ""; + } + block->nextBlock = blocks; + block->bufferBytes = static_cast(totalBytes - sizeof(*block)); + block->usedBytes = 0; + blocks = block; + } + + unsigned char* const storage = reinterpret_cast(blocks + 1) + + blocks->usedBytes; + blocks->usedBytes += static_cast(entryBytes); + Entry* const entry = reinterpret_cast(storage); + entry->next = bucket; + std::memcpy(entry->text, source, textBytes); + bucket = entry; + return entry->text; +} + +void idAtomicStringManager::MarkStatic() { + std::memcpy(hashTableAtMarkStatic, hashTable, sizeof(hashTable)); + blocksAtMarkStatic = blocks; + usedBytesAtMarkStatic = blocks == nullptr ? 0 : blocks->usedBytes; +} + +void idAtomicStringManager::FreeDynamic() { + std::memcpy(hashTable, hashTableAtMarkStatic, sizeof(hashTable)); + while (blocks != nullptr && blocks != blocksAtMarkStatic) { + atomicStringBlock_t* const next = blocks->nextBlock; + std::free(blocks); + blocks = next; + } + if (blocks != nullptr) { + blocks->usedBytes = usedBytesAtMarkStatic; + } +} + +idAtomicString::idAtomicString() + : str("") { +} + +idAtomicString::idAtomicString(const char* text) + : str(atomicStringManager.MakeAtomic(text)) { +} + +void idAtomicString::Set(const char* text) { + str = atomicStringManager.MakeAtomic(text); +} + +bool idAtomicString::operator==(const char* text) const { + return std::strcmp(str, text == nullptr ? "" : text) == 0; +} + +void idAtomicString::MarkStatic() { + atomicStringManager.MarkStatic(); +} + +void idAtomicString::FreeDynamic() { + atomicStringManager.FreeDynamic(); +} diff --git a/source/shared/idlib/text/atomicstring.h b/source/shared/idlib/text/atomicstring.h new file mode 100644 index 0000000..30a0b4b --- /dev/null +++ b/source/shared/idlib/text/atomicstring.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +struct atomicStringBlock_t { + atomicStringBlock_t* nextBlock; + int bufferBytes; + int usedBytes; +}; + +class idAtomicStringManager { +public: + idAtomicStringManager(); + + const char* MakeAtomic(const char* text); + void MarkStatic(); + void FreeDynamic(); + +private: + struct Entry; + Entry* hashTable[65536]; + Entry* hashTableAtMarkStatic[65536]; + atomicStringBlock_t* blocks; + atomicStringBlock_t* blocksAtMarkStatic; + int usedBytesAtMarkStatic; + + friend class idAtomicString; +}; + +class idAtomicString { +public: + idAtomicString(); + explicit idAtomicString(const char* text); + + void Set(const char* text); + const char* c_str() const { return str; } + bool IsEmpty() const { return str[0] == '\0'; } + + bool operator==(const idAtomicString& other) const { return str == other.str; } + bool operator!=(const idAtomicString& other) const { return str != other.str; } + bool operator==(const char* text) const; + + static void MarkStatic(); + static void FreeDynamic(); + +private: + const char* str; +}; + +extern idAtomicStringManager atomicStringManager; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(atomicStringBlock_t) == 12, + "Recovered atomicStringBlock_t ABI changed"); +static_assert(sizeof(idAtomicString) == 4, + "Recovered idAtomicString ABI changed"); +#endif diff --git a/source/shared/idlib/text/cmdargs.h b/source/shared/idlib/text/cmdargs.h new file mode 100644 index 0000000..1e8eef6 --- /dev/null +++ b/source/shared/idlib/text/cmdargs.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include + +class idCmdArgs { +public: + idCmdArgs() + : argc(0) { + tokenized[0] = '\0'; + } + + idCmdArgs(const char* text, const bool keepAsStrings) + : idCmdArgs() { + TokenizeString(text, keepAsStrings); + } + + idCmdArgs& operator=(const idCmdArgs& other) { + if (this == &other) { + return *this; + } + Clear(); + for (int index = 0; index < other.argc; ++index) { + AppendArg(other.argv[index]); + } + return *this; + } + + int Argc() const { + return argc; + } + + const char* Argv(const int arg) const { + return arg >= 0 && arg < argc ? argv[arg] : ""; + } + + void Clear() { + argc = 0; + tokenized[0] = '\0'; + } + + void AppendArg(const char* text) { + if (argc >= 64) { + return; + } + const char* const source = text == nullptr ? "" : text; + int used = 0; + for (int index = 0; index < argc; ++index) { + used += static_cast(std::strlen(argv[index])) + 1; + } + const int length = static_cast(std::strlen(source)); + if (used + length + 1 > 2048) { + return; + } + argv[argc] = tokenized + used; + std::memcpy(argv[argc], source, static_cast(length + 1)); + ++argc; + } + + void TokenizeString(const char* text, const bool keepAsStrings) { + Clear(); + const char* cursor = text == nullptr ? "" : text; + while (*cursor != '\0' && argc < 64) { + while (std::isspace(static_cast(*cursor)) != 0) { + ++cursor; + } + if (*cursor == '\0') { + break; + } + + char token[2048]; + int tokenLength = 0; + if (*cursor == '"') { + ++cursor; + while (*cursor != '\0' && *cursor != '"' + && tokenLength < 2047) { + token[tokenLength++] = *cursor++; + } + if (*cursor == '"') { + ++cursor; + } + } else { + while (*cursor != '\0' + && std::isspace(static_cast(*cursor)) == 0 + && tokenLength < 2047) { + if (!keepAsStrings && IsPunctuation(*cursor) + && tokenLength == 0) { + token[tokenLength++] = *cursor++; + break; + } + if (!keepAsStrings && IsPunctuation(*cursor)) { + break; + } + token[tokenLength++] = *cursor++; + } + } + token[tokenLength] = '\0'; + AppendArg(token); + } + } + +private: + int argc; + char* argv[64]; + char tokenized[2048]; + + static bool IsPunctuation(const char value) { + switch (value) { + case '{': case '}': case '(': case ')': + case '[': case ']': case ',': case ';': + return true; + default: + return false; + } + } +}; + +static_assert(sizeof(idCmdArgs) == 2308, "Recovered idCmdArgs ABI changed"); diff --git a/source/shared/idlib/text/str.h b/source/shared/idlib/text/str.h new file mode 100644 index 0000000..1f3e08b --- /dev/null +++ b/source/shared/idlib/text/str.h @@ -0,0 +1,211 @@ +#pragma once + +#include +#include +#include +#include + +// Exact tungsten idStr storage layout (tungsten.exe.h type 12142). This is a +// deliberately small ABI facade; more recovered text methods will be added as +// text/str.cpp replaces the BFG baseline. +class idStr { +public: + idStr() + : len(0) + , data(baseBuffer) + , allocedAndFlag(20) { + baseBuffer[0] = '\0'; + } + + idStr(const char* text) + : idStr() { + Assign(text); + } + + idStr(const idStr& other) + : idStr() { + Assign(other.c_str()); + } + + ~idStr() { + if (data != baseBuffer && !IsStaticBuffer()) { + std::free(data); + } + } + + idStr& operator=(const idStr& other) { + if (this != &other) { + Assign(other.c_str()); + } + return *this; + } + + idStr& operator=(const char* text) { + Assign(text); + return *this; + } + + const char* c_str() const { + return data; + } + + operator const char*() const { + return data; + } + + int Length() const { + return len; + } + + void Clear() { + len = 0; + data[0] = '\0'; + } + + void TrimWhitespaceRecovered() { + while (len > 0 + && std::isspace(static_cast(data[len - 1])) != 0) { + data[--len] = '\0'; + } + int amount = 0; + while (amount < len + && std::isspace(static_cast(data[amount])) != 0) { + ++amount; + } + if (amount > 0) { + std::memmove(data, data + amount, + static_cast(len - amount + 1)); + len -= amount; + } + } + + void Append(const char* text) { + if (text == nullptr || *text == '\0') { + return; + } + const int appendLength = static_cast(std::strlen(text)); + if (!EnsureAlloced(len + appendLength + 1)) { + return; + } + std::memcpy(data + len, text, static_cast(appendLength + 1)); + len += appendLength; + } + + void Append(const char value) { + if (!EnsureAlloced(len + 2)) { + return; + } + data[len++] = value; + data[len] = '\0'; + } + + void Append(const idStr& text) { + Append(text.c_str()); + } + + void ReplaceRecovered(const char* oldText, const char* newText) { + if (oldText == nullptr || oldText[0] == '\0') { + return; + } + const char* const replacement = newText == nullptr ? "" : newText; + const int oldLength = static_cast(std::strlen(oldText)); + idStr result; + const char* cursor = data; + for (;;) { + const char* const match = std::strstr(cursor, oldText); + if (match == nullptr) { + result.Append(cursor); + break; + } + const int prefixLength = static_cast(match - cursor); + for (int index = 0; index < prefixLength; ++index) { + result.Append(cursor[index]); + } + result.Append(replacement); + cursor = match + oldLength; + } + Assign(result.c_str()); + } + + static int Cmp(const char* left, const char* right) { + const char* const safeLeft = left == nullptr ? "" : left; + const char* const safeRight = right == nullptr ? "" : right; + return std::strcmp(safeLeft, safeRight); + } + + bool operator==(const idStr& other) const { + return Cmp(c_str(), other.c_str()) == 0; + } + + bool operator!=(const idStr& other) const { + return !(*this == other); + } + + bool operator<(const idStr& other) const { + return Cmp(c_str(), other.c_str()) < 0; + } + +protected: + int len; + char* data; + int allocedAndFlag; + char baseBuffer[20]; + + void UseStaticBufferRecovered(char* buffer, const int capacity) { + if (data != baseBuffer && !IsStaticBuffer()) { + std::free(data); + } + data = buffer; + len = 0; + allocedAndFlag = capacity | static_cast(0x80000000u); + if (data != nullptr && capacity > 0) { + data[0] = '\0'; + } + } + +private: + + int GetAlloced() const { + return allocedAndFlag & 0x7FFFFFFF; + } + + bool IsStaticBuffer() const { + return (static_cast(allocedAndFlag) & 0x80000000u) != 0; + } + + bool EnsureAlloced(const int amount) { + if (amount <= GetAlloced()) { + return true; + } + if (IsStaticBuffer()) { + return false; + } + + const int newAmount = std::max(amount, amount + amount / 2); + char* const replacement = static_cast( + std::malloc(static_cast(newAmount)) + ); + if (replacement == nullptr) { + return false; + } + std::memcpy(replacement, data, static_cast(len + 1)); + if (data != baseBuffer) { + std::free(data); + } + data = replacement; + allocedAndFlag = newAmount; + return true; + } + + void Assign(const char* text) { + const char* const source = text == nullptr ? "" : text; + const int sourceLength = static_cast(std::strlen(source)); + if (!EnsureAlloced(sourceLength + 1)) { + return; + } + std::memmove(data, source, static_cast(sourceLength + 1)); + len = sourceLength; + } +}; + +static_assert(sizeof(idStr) == 32, "Recovered idStr ABI changed"); diff --git a/source/shared/idlib/text/tokenstatic.h b/source/shared/idlib/text/tokenstatic.h new file mode 100644 index 0000000..df0ee65 --- /dev/null +++ b/source/shared/idlib/text/tokenstatic.h @@ -0,0 +1,55 @@ +#pragma once + +#include "str.h" + +#include +#include +#include + +class idToken : public idStr { +public: + idToken() + : type(0), subtype(0), line(0), linesCrossed(0), flags(0), + intvalue(0), floatvalue(-FLT_MAX), whiteSpaceStart_p(nullptr), + whiteSpaceEnd_p(nullptr), next(nullptr) { + } + + int type; + int subtype; + int line; + int linesCrossed; + int flags; + unsigned int intvalue; + float floatvalue; + const char* whiteSpaceStart_p; + const char* whiteSpaceEnd_p; + idToken* next; +}; + +template +class idTokenStatic : public idToken { +public: + idTokenStatic() { + UseStaticBufferRecovered(buffer, BUFFER_SIZE); + } + + explicit idTokenStatic(const idStr& text) { + UseStaticBufferRecovered(buffer, BUFFER_SIZE); + const int copyLength = std::min(text.Length(), BUFFER_SIZE - 1); + if (copyLength > 0) { + std::memcpy(data, text.c_str(), static_cast(copyLength)); + } + data[copyLength] = '\0'; + len = copyLength; + } + +private: + char buffer[BUFFER_SIZE]; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idToken) == 72, "Recovered idToken ABI changed"); +static_assert(sizeof(idTokenStatic<256>) == 328, + "Recovered idTokenStatic<256> ABI changed"); +#endif + diff --git a/source/shared/idlib/typeinfo/typeinfofile.cpp b/source/shared/idlib/typeinfo/typeinfofile.cpp new file mode 100644 index 0000000..52b5674 --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfofile.cpp @@ -0,0 +1,994 @@ +#ifdef __IDLIB__ +#include "idlib/precompiled.h" +#ifdef nullptr +#undef nullptr +#endif +#ifdef strcmp +#undef strcmp +#endif +#ifdef snprintf +#undef snprintf +#endif +#ifdef vsnprintf +#undef vsnprintf +#endif +#else +#include "../math/vector.h" +#include "../text/str.h" +#endif + +#include "typeinfofile.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Token { + std::string text; + bool quoted = false; + bool whitespace = false; +}; + +struct TypeInfoState { + std::string output; + std::string input; + std::string name; + std::string lastWhitespace; + std::string lastComment; + std::size_t cursor = 0; + int line = 1; + bool hadError = false; + bool hadWarning = false; +}; + +TypeInfoState* State(idTypeInfoFile* file, const bool create = true) { + if (file->fp == nullptr && create) file->fp = new TypeInfoState; + return static_cast(file->fp); +} + +const TypeInfoState* State(const idTypeInfoFile* file) { + return static_cast(file->fp); +} + +bool IsPunctuation(const char value) { + switch (value) { + case '{': case '}': case '[': case ']': case '=': case ';': + case '!': case '<': case '>': + return true; + default: + return false; + } +} + +void AppendEscape(std::string& result, const char value) { + switch (value) { + case 'n': result.push_back('\n'); break; + case 'r': result.push_back('\r'); break; + case 't': result.push_back('\t'); break; + case 'v': result.push_back('\v'); break; + case 'b': result.push_back('\b'); break; + case 'f': result.push_back('\f'); break; + case 'a': result.push_back('\a'); break; + default: result.push_back(value); break; + } +} + +std::string Quote(const char* value) { + std::string result(1, '"'); + const unsigned char* cursor = reinterpret_cast( + value == nullptr ? "" : value); + for (; *cursor != 0; ++cursor) { + switch (*cursor) { + case '\\': result += "\\\\"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '\v': result += "\\v"; break; + case '\b': result += "\\b"; break; + case '\f': result += "\\f"; break; + case '\a': result += "\\a"; break; + case '\'': result += "\\'"; break; + case '"': result += "\\\""; break; + case '?': result += "\\?"; break; + default: result.push_back(static_cast(*cursor)); break; + } + } + result.push_back('"'); + return result; +} + +std::string Trim(std::string value) { + while (!value.empty() + && std::isspace(static_cast(value.back())) != 0) { + value.pop_back(); + } + std::size_t first = 0; + while (first < value.size() + && std::isspace(static_cast(value[first])) != 0) { + ++first; + } + return value.substr(first); +} + +void SkipWhitespace(TypeInfoState& state, bool& skipped) { + const std::size_t begin = state.cursor; + state.lastComment.clear(); + while (state.cursor < state.input.size()) { + const char current = state.input[state.cursor]; + if (std::isspace(static_cast(current)) != 0) { + if (current == '\n') ++state.line; + ++state.cursor; + continue; + } + if (current == '/' && state.cursor + 1 < state.input.size() + && state.input[state.cursor + 1] == '/') { + state.cursor += 2; + const std::size_t commentStart = state.cursor; + while (state.cursor < state.input.size() + && state.input[state.cursor] != '\n') { + ++state.cursor; + } + state.lastComment = Trim(state.input.substr( + commentStart, state.cursor - commentStart)); + continue; + } + if (current == '/' && state.cursor + 1 < state.input.size() + && state.input[state.cursor + 1] == '*') { + state.cursor += 2; + const std::size_t commentStart = state.cursor; + while (state.cursor + 1 < state.input.size() + && !(state.input[state.cursor] == '*' + && state.input[state.cursor + 1] == '/')) { + if (state.input[state.cursor] == '\n') ++state.line; + ++state.cursor; + } + state.lastComment = Trim(state.input.substr( + commentStart, state.cursor - commentStart)); + if (state.cursor + 1 < state.input.size()) state.cursor += 2; + continue; + } + break; + } + state.lastWhitespace = state.input.substr(begin, state.cursor - begin); + skipped = state.cursor != begin; +} + +bool NextToken(TypeInfoState& state, Token& token) { + token = Token{}; + SkipWhitespace(state, token.whitespace); + if (state.cursor >= state.input.size()) return false; + + const char first = state.input[state.cursor]; + if (first == '"') { + token.quoted = true; + ++state.cursor; + while (state.cursor < state.input.size()) { + const char current = state.input[state.cursor++]; + if (current == '"') return true; + if (current == '\\' && state.cursor < state.input.size()) { + AppendEscape(token.text, state.input[state.cursor++]); + } else { + if (current == '\n') ++state.line; + token.text.push_back(current); + } + } + state.hadError = true; + return true; + } + if (IsPunctuation(first)) { + token.text.assign(1, first); + ++state.cursor; + return true; + } + + const std::size_t begin = state.cursor; + while (state.cursor < state.input.size()) { + const char current = state.input[state.cursor]; + if (std::isspace(static_cast(current)) != 0 + || IsPunctuation(current) + || (current == '/' && state.cursor + 1 < state.input.size() + && (state.input[state.cursor + 1] == '/' + || state.input[state.cursor + 1] == '*'))) { + break; + } + ++state.cursor; + } + if (state.cursor == begin) ++state.cursor; + token.text = state.input.substr(begin, state.cursor - begin); + return true; +} + +struct ParserMark { + std::size_t cursor; + int line; + std::string whitespace; + std::string comment; +}; + +ParserMark Mark(const TypeInfoState& state) { + return {state.cursor, state.line, state.lastWhitespace, state.lastComment}; +} + +void Restore(TypeInfoState& state, const ParserMark& mark) { + state.cursor = mark.cursor; + state.line = mark.line; + state.lastWhitespace = mark.whitespace; + state.lastComment = mark.comment; +} + +bool CheckToken(TypeInfoState& state, const char* expected) { + const ParserMark mark = Mark(state); + Token token; + if (NextToken(state, token) && token.text == expected) return true; + Restore(state, mark); + return false; +} + +bool PeekToken(TypeInfoState& state, const char* expected) { + const ParserMark mark = Mark(state); + const bool result = CheckToken(state, expected); + Restore(state, mark); + return result; +} + +bool ExpectToken(TypeInfoState& state, const char* expected) { + if (CheckToken(state, expected)) return true; + state.hadError = true; + return false; +} + +void Write(idTypeInfoFile* file, const std::string& text) { + TypeInfoState* const state = State(file, false); + if (state != nullptr) state->output += text; +} + +std::string Indentation(bool& newline, const int indent) { + std::string result; + if (newline) result.push_back('\n'); + else newline = true; + result.append(static_cast(std::max(indent, 0)), '\t'); + return result; +} + +std::string FormatFixed(const double value, const int precision) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << std::fixed << std::setprecision(precision) << value; + return stream.str(); +} + +std::string FormatCompact(const float value, const int precision = 8) { + std::string text = FormatFixed(value, precision); + while (!text.empty() && text.back() == '0') text.pop_back(); + if (!text.empty() && text.back() == '.') text.pop_back(); + if (text == "-0") text = "0"; + return text; +} + +bool ReadLong(TypeInfoState& state, long& value) { + Token token; + if (!NextToken(state, token)) { + state.hadError = true; + return false; + } + char* end = nullptr; + value = std::strtol(token.text.c_str(), &end, 0); + if (end == token.text.c_str() || *end != '\0') { + state.hadError = true; + return false; + } + return true; +} + +bool ReadUnsignedLongValue(TypeInfoState& state, unsigned long& value) { + Token token; + if (!NextToken(state, token)) { + state.hadError = true; + return false; + } + char* end = nullptr; + value = std::strtoul(token.text.c_str(), &end, 0); + if (end == token.text.c_str() || *end != '\0') { + state.hadError = true; + return false; + } + return true; +} + +bool ReadReal(TypeInfoState& state, double& value) { + Token token; + if (!NextToken(state, token)) { + state.hadError = true; + return false; + } + char* end = nullptr; + value = std::strtod(token.text.c_str(), &end); + if (end == token.text.c_str() || *end != '\0') { + state.hadError = true; + return false; + } + return true; +} + +void Assign(idStr& target, const std::string& value) { + target = value.c_str(); +} + +bool ParseAssignment(TypeInfoState& state, std::vector& parts) { + parts.clear(); + for (int count = 0; count < 3; ++count) { + Token token; + if (!NextToken(state, token)) return false; + if (token.text == "=" || token.text == "[") return false; + parts.push_back(token); + if (CheckToken(state, "=")) return true; + } + return false; +} + +void SetTypeParts(const std::vector& parts, idStr& type, idStr& ops, + idStr& name) { + type.Clear(); + ops.Clear(); + name.Clear(); + if (parts.size() == 1) { + Assign(name, parts[0].text); + } else if (parts.size() == 2) { + Assign(type, parts[0].text); + Assign(name, parts[1].text); + } else if (parts.size() == 3) { + Assign(type, parts[0].text); + Assign(ops, parts[1].text); + Assign(name, parts[2].text); + } +} + +bool StringsEqual(const char* left, const char* right) { + return std::strcmp(left == nullptr ? "" : left, + right == nullptr ? "" : right) == 0; +} + +bool TypeMatches(const std::vector& parts, const char* type, + const char* ops, const char* name) { + if (parts.size() == 1) return StringsEqual(parts[0].text.c_str(), name); + if (parts.size() == 2) { + return (type == nullptr || *type == 0 + || StringsEqual(parts[0].text.c_str(), type)) + && (ops == nullptr || *ops == 0) + && StringsEqual(parts[1].text.c_str(), name); + } + return parts.size() == 3 + && (type == nullptr || *type == 0 + || StringsEqual(parts[0].text.c_str(), type)) + && (ops == nullptr || *ops == 0 + || StringsEqual(parts[1].text.c_str(), ops)) + && StringsEqual(parts[2].text.c_str(), name); +} + +bool ParseArrayAssignment(TypeInfoState& state, std::vector& parts, + int& index, const bool allowPlain) { + parts.clear(); + index = -1; + for (int count = 0; count < 3; ++count) { + Token token; + if (!NextToken(state, token)) return false; + if (token.text == "=" || token.text == "[") return false; + parts.push_back(token); + if (CheckToken(state, "=")) return allowPlain; + if (CheckToken(state, "[")) { + unsigned long parsed = 0; + if (!ReadUnsignedLongValue(state, parsed)) return false; + index = static_cast(parsed); + return ExpectToken(state, "]") && ExpectToken(state, "="); + } + } + return false; +} + +void NormalizeExpected(idStr& actualType, idStr& actualOps, + const char* expectedType, const char* expectedOps) { + if (expectedType != nullptr && *expectedType != 0) { + if (actualType.Length() == 0) { + actualType = expectedType; + actualOps = expectedOps == nullptr ? "" : expectedOps; + } + } else { + actualType.Clear(); + actualOps.Clear(); + } + if (expectedOps != nullptr && *expectedOps != 0) { + if (actualOps.Length() == 0) actualOps = expectedOps; + } else { + actualOps.Clear(); + } +} + +std::string FormatMessage(const char* format, va_list args) { + char buffer[4096]; + const int amount = std::vsnprintf(buffer, sizeof(buffer), + format == nullptr ? "" : format, args); + if (amount < 0) return format == nullptr ? "" : format; + return std::string(buffer, + static_cast(std::min(amount, + static_cast(sizeof(buffer) - 1)))); +} + +struct StringTableState { std::deque values; }; +std::mutex stringTablesMutex; +std::unordered_map stringTables; + +} // namespace + +idTypeInfoSettings::idTypeInfoSettings() + : writeModifier(WRITE_ALL_PROPERTIES), resolveEntityPointers(true), + resolveModelPointers(true), skipMarkedObjects(false), + skipScriptObjects(false), writeComments(true), writeType(true) {} + +idTypeInfoSettings::idTypeInfoSettings(const bool entities, const bool models) + : idTypeInfoSettings() { + resolveEntityPointers = entities; + resolveModelPointers = models; +} + +idTypeInfoSettings::idTypeInfoSettings(const bool entities, const bool models, + const bool skipMarked) + : idTypeInfoSettings(entities, models) { + skipMarkedObjects = skipMarked; +} + +idTypeInfoFile::idTypeInfoFile(const int initialIndent) + : settings(), fp(nullptr), src{}, indent(initialIndent), newline(false) { + settings.writeComments = false; +} + +idTypeInfoFile::~idTypeInfoFile() { + delete State(this, false); + fp = nullptr; +} + +bool idTypeInfoFile::WriteMemoryFile() { + delete State(this, false); + fp = new TypeInfoState; + return fp != nullptr; +} + +const char* idTypeInfoFile::GetTypeInfoString() const { + const TypeInfoState* const state = State(this); + return state == nullptr ? "" : state->output.c_str(); +} + +bool idTypeInfoFile::ReadMemory(const char* data, const int length, + const char* sourceName, const int startLine) { + TypeInfoState* const state = State(this); + state->input.assign(data == nullptr ? "" : data, + data == nullptr || length <= 0 ? 0 : static_cast(length)); + state->name = sourceName == nullptr ? "" : sourceName; + state->cursor = 0; + state->line = std::max(startLine, 1); + state->hadError = false; + state->hadWarning = false; + state->lastWhitespace.clear(); + state->lastComment.clear(); + return data != nullptr || length == 0; +} + +bool idTypeInfoFile::ReadMemoryFile() { + TypeInfoState* const state = State(this, false); + if (state == nullptr) return false; + state->input = state->output; + state->cursor = 0; + state->line = 1; + state->hadError = false; + state->hadWarning = false; + return true; +} + +void idTypeInfoFile::WriteOpeningBrace() { + if (State(this, false) != nullptr) { + Write(this, "{"); + ++indent; + } +} + +void idTypeInfoFile::ReadOpeningBrace() { + TypeInfoState* const state = State(this); + ExpectToken(*state, "{"); + ++indent; +} + +bool idTypeInfoFile::CheckOpeningBrace() { + TypeInfoState* const state = State(this); + if (!CheckToken(*state, "{")) return false; + ++indent; + return true; +} + +void idTypeInfoFile::WriteClosingBrace() { + if (State(this, false) == nullptr) return; + --indent; + Write(this, Indentation(newline, indent) + "}"); +} + +void idTypeInfoFile::ReadClosingBrace() { + --indent; + ExpectToken(*State(this), "}"); +} + +bool idTypeInfoFile::CheckClosingBrace() { + TypeInfoState* const state = State(this); + if (!CheckToken(*state, "}")) return false; + --indent; + return true; +} + +bool idTypeInfoFile::PeekClosingBrace() { + return PeekToken(*State(this), "}"); +} + +void idTypeInfoFile::WriteSkipObject(const bool skip) { + if (skip) Write(this, "! "); +} + +bool idTypeInfoFile::CheckSkipObject() { + return CheckToken(*State(this), "!"); +} + +bool idTypeInfoFile::CheckType(const char* type, const char* ops, + const char* name) { + TypeInfoState* const state = State(this); + const ParserMark mark = Mark(*state); + std::vector parts; + if (ParseAssignment(*state, parts) && TypeMatches(parts, type, ops, name)) { + return true; + } + Restore(*state, mark); + return false; +} + +void idTypeInfoFile::WriteType(const char* type, const char* ops, + const char* name) { + if (State(this, false) == nullptr) return; + const char* const safeName = name == nullptr ? "" : name; + const unsigned char first = static_cast(*safeName); + const bool plainName = std::isalpha(first) != 0 || first == '_'; + const std::string outputName = plainName ? safeName : Quote(safeName); + std::string output = Indentation(newline, indent); + if (settings.writeType && type != nullptr && *type != 0) { + output += Quote(type); + output.push_back(' '); + if (ops != nullptr && *ops != 0) { + output += Quote(ops); + output.push_back(' '); + } + } + output += outputName; + output += " = "; + Write(this, output); +} + +bool idTypeInfoFile::ReadType(idStr& type, idStr& ops, idStr& name) { + TypeInfoState* const state = State(this); + std::vector parts; + if (!ParseAssignment(*state, parts) || parts.empty()) { + state->hadError = true; + return false; + } + SetTypeParts(parts, type, ops, name); + return true; +} + +bool idTypeInfoFile::ExpectType(const char* type, const char* ops, + const char* name) { + idStr actualType, actualOps, actualName; + if (!ReadType(actualType, actualOps, actualName)) return false; + NormalizeExpected(actualType, actualOps, type, ops); + const bool result = StringsEqual(actualType.c_str(), type) + && StringsEqual(actualOps.c_str(), ops) + && StringsEqual(actualName.c_str(), name); + if (!result) State(this)->hadWarning = true; + return result; +} + +void idTypeInfoFile::WriteArrayElementType(const char* type, const char* ops, + const char* name, const int index) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << (name == nullptr ? "" : name) << '[' << index << ']'; + WriteType(type, ops, stream.str().c_str()); +} + +bool idTypeInfoFile::ReadArrayElementType(idStr& type, idStr& ops, + idStr& name, int& index) { + TypeInfoState* const state = State(this); + std::vector parts; + if (!ParseArrayAssignment(*state, parts, index, false) || parts.empty()) { + state->hadError = true; + index = -1; + return false; + } + SetTypeParts(parts, type, ops, name); + return true; +} + +bool idTypeInfoFile::CheckArrayElementType(idStr& type, idStr& ops, + idStr& name, int& index) { + TypeInfoState* const state = State(this); + const ParserMark mark = Mark(*state); + std::vector parts; + if (ParseArrayAssignment(*state, parts, index, true) && !parts.empty()) { + SetTypeParts(parts, type, ops, name); + return true; + } + Restore(*state, mark); + type.Clear(); + ops.Clear(); + name.Clear(); + index = -1; + return false; +} + +bool idTypeInfoFile::ExpectArrayElementType(const char* type, const char* ops, + const char* name, int& index) { + idStr actualType, actualOps, actualName; + if (!ReadArrayElementType(actualType, actualOps, actualName, index)) { + return false; + } + NormalizeExpected(actualType, actualOps, type, ops); + const bool result = StringsEqual(actualType.c_str(), type) + && StringsEqual(actualOps.c_str(), ops) + && StringsEqual(actualName.c_str(), name); + if (!result) State(this)->hadWarning = true; + return result; +} + +void idTypeInfoFile::WriteBool(const bool value) { + Write(this, value ? "true;" : "false;"); +} + +void idTypeInfoFile::ReadBool(bool& value) { + TypeInfoState* const state = State(this); + Token token; + if (NextToken(*state, token) && token.text == "true") value = true; + else if (token.text == "false") value = false; + else state->hadWarning = true; + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteChar(const char value) { + Write(this, std::to_string(static_cast(value)) + ";"); +} + +void idTypeInfoFile::ReadChar(char& value) { + long parsed = 0; TypeInfoState* state = State(this); + if (ReadLong(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteUnsignedChar(const unsigned char value) { + Write(this, std::to_string(static_cast(value)) + ";"); +} + +void idTypeInfoFile::ReadUnsignedChar(unsigned char& value) { + unsigned long parsed = 0; TypeInfoState* state = State(this); + if (ReadUnsignedLongValue(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteWChar(const wchar_t value) { + Write(this, std::to_string(static_cast(value)) + ";"); +} + +void idTypeInfoFile::ReadWChar(wchar_t& value) { + long parsed = 0; TypeInfoState* state = State(this); + if (ReadLong(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteShort(const short value) { + Write(this, std::to_string(static_cast(value)) + ";"); +} + +void idTypeInfoFile::ReadShort(short& value) { + long parsed = 0; TypeInfoState* state = State(this); + if (ReadLong(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteUnsignedShort(const unsigned short value) { + Write(this, std::to_string(static_cast(value)) + ";"); +} + +void idTypeInfoFile::ReadUnsignedShort(unsigned short& value) { + unsigned long parsed = 0; TypeInfoState* state = State(this); + if (ReadUnsignedLongValue(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteUnsignedLong(const unsigned long value) { + Write(this, std::to_string(value) + ";"); +} + +void idTypeInfoFile::ReadUnsignedLong(unsigned long& value) { + TypeInfoState* state = State(this); + ReadUnsignedLongValue(*state, value); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteInt(const int value) { + Write(this, std::to_string(value) + ";"); +} + +void idTypeInfoFile::ReadInt(int& value) { + long parsed = 0; TypeInfoState* state = State(this); + if (ReadLong(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteFloat(const float value) { + Write(this, FormatFixed(value, 6) + ";"); +} + +void idTypeInfoFile::ReadFloat(float& value) { + double parsed = 0.0; TypeInfoState* state = State(this); + if (ReadReal(*state, parsed)) value = static_cast(parsed); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteDouble(const double value) { + Write(this, FormatFixed(value, 6) + ";"); +} + +void idTypeInfoFile::ReadDouble(double& value) { + TypeInfoState* state = State(this); + ReadReal(*state, value); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteStr(const char* value) { + Write(this, Quote(value) + ";"); +} + +void idTypeInfoFile::ReadStr(idStr& value) { + TypeInfoState* const state = State(this); + Token token; + if (NextToken(*state, token) && token.quoted) Assign(value, token.text); + else value.Clear(); + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteValueString(const idStr& value) { + Write(this, std::string(value.c_str()) + ";"); +} + +void idTypeInfoFile::ReadValueString(idStr& value) { + TypeInfoState* const state = State(this); + value.Clear(); + bool first = true; + Token token; + while (NextToken(*state, token)) { + if (token.text == ";") return; + if (!first && token.whitespace) value.Append(' '); + const std::string text = token.quoted ? Quote(token.text.c_str()) : token.text; + value.Append(text.c_str()); + first = false; + } + state->hadError = true; +} + +void idTypeInfoFile::WriteNullPointer() { Write(this, "NULL;"); } + +void idTypeInfoFile::ReadNullPointer() { + TypeInfoState* state = State(this); + ExpectToken(*state, "NULL"); + ExpectToken(*state, ";"); +} + +bool idTypeInfoFile::CheckNullPointer() { + TypeInfoState* state = State(this); + const ParserMark mark = Mark(*state); + if (CheckToken(*state, "NULL") && ExpectToken(*state, ";")) return true; + Restore(*state, mark); + return false; +} + +void idTypeInfoFile::WriteUnknown() { Write(this, ";"); } + +void idTypeInfoFile::ReadUnknown() { + TypeInfoState* const state = State(this); + int depth = 0; + Token token; + while (NextToken(*state, token)) { + if (token.text == "{") ++depth; + else if (token.text == "}") { + --depth; + if (depth <= 0) return; + } else if (token.text == ";" && depth <= 0) return; + } +} + +bool idTypeInfoFile::CheckUnknown() { + TypeInfoState* state = State(this); + if (!CheckToken(*state, "<")) return false; + ReadUnknown(); + return true; +} + +void idTypeInfoFile::WriteVec3(const idVec3& value) { + WriteOpeningBrace(); + WriteType("float", "", "x"); WriteFloat(value.x); + WriteType("float", "", "y"); WriteFloat(value.y); + WriteType("float", "", "z"); WriteFloat(value.z); + WriteClosingBrace(); +} + +void idTypeInfoFile::ReadVec3(idVec3& value) { + ReadOpeningBrace(); + if (CheckType("float", "", "x")) ReadFloat(value.x); + if (CheckType("float", "", "y")) ReadFloat(value.y); + if (CheckType("float", "", "z")) ReadFloat(value.z); + ReadClosingBrace(); +} + +void idTypeInfoFile::WriteVecX(const idVecX& value) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << value.GetSize(); + for (int index = 0; index < value.GetSize(); ++index) { + stream << ' ' << FormatCompact(value[index]); + } + stream << ';'; + Write(this, stream.str()); +} + +void idTypeInfoFile::ReadVecX(idVecX& value) { + long count = 0; TypeInfoState* state = State(this); + if (!ReadLong(*state, count)) return; + value.SetSize(static_cast(count)); + for (int index = 0; index < value.GetSize(); ++index) { + double parsed = 0.0; + if (ReadReal(*state, parsed)) value[index] = static_cast(parsed); + } + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteMatX(const idMatX& value) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << value.GetNumRows() << ' ' << value.GetNumColumns(); + for (int row = 0; row < value.GetNumRows(); ++row) { + for (int column = 0; column < value.GetNumColumns(); ++column) { + stream << ' ' << FormatCompact(value[row][column]); + } + } + stream << ';'; + Write(this, stream.str()); +} + +void idTypeInfoFile::ReadMatX(idMatX& value) { + long rows = 0, columns = 0; TypeInfoState* state = State(this); + if (!ReadLong(*state, rows) || !ReadLong(*state, columns)) return; + value.SetSize(static_cast(rows), static_cast(columns)); + for (int row = 0; row < value.GetNumRows(); ++row) { + for (int column = 0; column < value.GetNumColumns(); ++column) { + double parsed = 0.0; + if (ReadReal(*state, parsed)) value[row][column] = static_cast(parsed); + } + } + ExpectToken(*state, ";"); +} + +void idTypeInfoFile::WriteAngles(const idAngles& value) { + WriteOpeningBrace(); + WriteType("float", "", "pitch"); WriteFloat(value.pitch); + WriteType("float", "", "yaw"); WriteFloat(value.yaw); + WriteType("float", "", "roll"); WriteFloat(value.roll); + WriteClosingBrace(); +} + +void idTypeInfoFile::ReadAngles(idAngles& value) { + ReadOpeningBrace(); + if (ExpectType("float", "", "pitch")) ReadFloat(value.pitch); + if (ExpectType("float", "", "yaw")) ReadFloat(value.yaw); + if (ExpectType("float", "", "roll")) ReadFloat(value.roll); + ReadClosingBrace(); +} + +void idTypeInfoFile::WriteColor(const idColor& value) { + const float* const values = reinterpret_cast(&value); + WriteOpeningBrace(); + WriteType("float", "", "r"); WriteFloat(values[0]); + WriteType("float", "", "g"); WriteFloat(values[1]); + WriteType("float", "", "b"); WriteFloat(values[2]); + WriteType("float", "", "a"); WriteFloat(values[3]); + WriteClosingBrace(); +} + +void idTypeInfoFile::ReadColor(idColor& value) { + float* const values = reinterpret_cast(&value); + ReadOpeningBrace(); + if (CheckType("float", "", "r")) ReadFloat(values[0]); + if (CheckType("float", "", "g")) ReadFloat(values[1]); + if (CheckType("float", "", "b")) ReadFloat(values[2]); + if (CheckType("float", "", "a")) ReadFloat(values[3]); + ReadClosingBrace(); +} + +void idTypeInfoFile::WriteComment(const char* comment) { + if (settings.writeComments && comment != nullptr && *comment != 0) { + Write(this, std::string("\t/* ") + comment + " */"); + } +} + +void idTypeInfoFile::ReadComment(idStr& comment) { + TypeInfoState* const state = State(this); + bool skipped = false; + SkipWhitespace(*state, skipped); + if (!state->lastComment.empty()) Assign(comment, state->lastComment); + else comment.Clear(); +} + +void idTypeInfoFile::Error(const char* format, ...) { + TypeInfoState* const state = State(this); + va_list args; va_start(args, format); + state->lastComment = FormatMessage(format, args); + va_end(args); + state->hadError = true; +} + +void idTypeInfoFile::Warning(const char* format, ...) { + TypeInfoState* const state = State(this); + va_list args; va_start(args, format); + state->lastComment = FormatMessage(format, args); + va_end(args); + state->hadWarning = true; +} + +bool idTypeInfoFile::HadError() const { + const TypeInfoState* const state = State(this); + return state != nullptr && state->hadError; +} + +const char* idTypeInfoFile::OutputTabs(bool& outputNewline, const int count) { + static thread_local std::string tabs; + tabs = Indentation(outputNewline, count); + return tabs.c_str(); +} + +const char* idTypeInfoStrings::GetStringForIndex(const int index) const { + std::lock_guard lock(stringTablesMutex); + const auto found = stringTables.find(this); + if (found == stringTables.end() || index < 0 + || index >= static_cast(found->second.values.size())) return ""; + return found->second.values[static_cast(index)].c_str(); +} + +int idTypeInfoStrings::FindString(const char* string) { + const std::string value = string == nullptr ? "" : string; + std::lock_guard lock(stringTablesMutex); + std::deque& values = stringTables[this].values; + for (std::size_t index = 0; index < values.size(); ++index) { + if (values[index] == value) return static_cast(index); + } + values.push_back(value); + return static_cast(values.size() - 1); +} + +void idTypeInfoStrings::Shutdown() { + std::lock_guard lock(stringTablesMutex); + stringTables.erase(this); + std::memset(strings, 0, sizeof(strings)); + std::memset(stringHash, 0, sizeof(stringHash)); +} diff --git a/source/shared/idlib/typeinfo/typeinfofile.h b/source/shared/idlib/typeinfo/typeinfofile.h new file mode 100644 index 0000000..6bde07c --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfofile.h @@ -0,0 +1,150 @@ +#pragma once + +#include + +class idStr; +class idVec3; +class idVecX; +class idMatX; +class idAngles; +class idColor; + +struct idTypeInfoSettings { + enum writeModifier_t { + WRITE_ALL_PROPERTIES = 0, + WRITE_DEF_AND_EDIT_ONLY = 1, + WRITE_EDIT_ONLY = 2 + }; + + idTypeInfoSettings(); + idTypeInfoSettings(bool resolveEntityPointers, + bool resolveModelPointers); + idTypeInfoSettings(bool resolveEntityPointers, + bool resolveModelPointers, bool skipMarkedObjects); + + writeModifier_t writeModifier; + bool resolveEntityPointers; + bool resolveModelPointers; + bool skipMarkedObjects; + bool skipScriptObjects; + bool writeComments; + bool writeType; +}; + +class idTypeInfoFile { +public: + explicit idTypeInfoFile(int indent = 0); + ~idTypeInfoFile(); + + idTypeInfoFile(const idTypeInfoFile&) = delete; + idTypeInfoFile& operator=(const idTypeInfoFile&) = delete; + + bool WriteMemoryFile(); + const char* GetTypeInfoString() const; + bool ReadMemory(const char* data, int length, const char* name, + int startLine = 1); + bool ReadMemoryFile(); + + void WriteOpeningBrace(); + void ReadOpeningBrace(); + bool CheckOpeningBrace(); + void WriteClosingBrace(); + void ReadClosingBrace(); + bool CheckClosingBrace(); + bool PeekClosingBrace(); + + void WriteSkipObject(bool skip); + bool CheckSkipObject(); + + bool CheckType(const char* type, const char* ops, const char* name); + void WriteType(const char* type, const char* ops, const char* name); + bool ReadType(idStr& type, idStr& ops, idStr& name); + bool ExpectType(const char* type, const char* ops, const char* name); + + void WriteArrayElementType(const char* type, const char* ops, + const char* name, int index); + bool ReadArrayElementType(idStr& type, idStr& ops, idStr& name, + int& index); + bool CheckArrayElementType(idStr& type, idStr& ops, idStr& name, + int& index); + bool ExpectArrayElementType(const char* type, const char* ops, + const char* name, int& index); + + void WriteBool(bool value); + void ReadBool(bool& value); + void WriteChar(char value); + void ReadChar(char& value); + void WriteUnsignedChar(unsigned char value); + void ReadUnsignedChar(unsigned char& value); + void WriteWChar(wchar_t value); + void ReadWChar(wchar_t& value); + void WriteShort(short value); + void ReadShort(short& value); + void WriteUnsignedShort(unsigned short value); + void ReadUnsignedShort(unsigned short& value); + void WriteUnsignedLong(unsigned long value); + void ReadUnsignedLong(unsigned long& value); + void WriteInt(int value); + void ReadInt(int& value); + void WriteFloat(float value); + void ReadFloat(float& value); + void WriteDouble(double value); + void ReadDouble(double& value); + + void WriteStr(const char* value); + void ReadStr(idStr& value); + void WriteValueString(const idStr& value); + void ReadValueString(idStr& value); + + void WriteNullPointer(); + void ReadNullPointer(); + bool CheckNullPointer(); + void WriteUnknown(); + void ReadUnknown(); + bool CheckUnknown(); + + void WriteVec3(const idVec3& value); + void ReadVec3(idVec3& value); + void WriteVecX(const idVecX& value); + void ReadVecX(idVecX& value); + void WriteMatX(const idMatX& value); + void ReadMatX(idMatX& value); + void WriteAngles(const idAngles& value); + void ReadAngles(idAngles& value); + void WriteColor(const idColor& value); + void ReadColor(idColor& value); + + void WriteComment(const char* comment); + void ReadComment(idStr& comment); + void Error(const char* format, ...); + void Warning(const char* format, ...); + bool HadError() const; + + static const char* OutputTabs(bool& newline, int indent); + + idTypeInfoSettings settings; + void* fp; + std::uint8_t src[136]; + int indent; + bool newline; +}; + +class idTypeInfoStrings { +public: + const char* GetStringForIndex(int index) const; + int FindString(const char* string); + void Shutdown(); + +private: + std::uint32_t strings[4]; + std::uint32_t stringHash[8]; +}; + +static_assert(sizeof(idTypeInfoSettings) == 12, + "Recovered idTypeInfoSettings layout changed"); +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idTypeInfoFile) == 160, + "Recovered idTypeInfoFile layout changed"); +static_assert(sizeof(idTypeInfoStrings) == 48, + "Recovered idTypeInfoStrings layout changed"); +#endif diff --git a/source/shared/idlib/typeinfo/typeinfoobject.cpp b/source/shared/idlib/typeinfo/typeinfoobject.cpp new file mode 100644 index 0000000..0f506ca --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfoobject.cpp @@ -0,0 +1,337 @@ +#include "typeinfoobject.h" + +#include +#include +#include +#include + +namespace { + +struct listLayout_t { + void* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; +}; + +void CopyTypeName(const char* source, char* destination, const int capacity) { + int out = 0; + const char* cursor = source == nullptr ? "" : source; + while (std::isspace(static_cast(*cursor))) ++cursor; + if (std::strncmp(cursor, "const ", 6) == 0) cursor += 6; + while (*cursor != '\0' && *cursor != '*' && *cursor != '&' + && !std::isspace(static_cast(*cursor)) + && out + 1 < capacity) { + destination[out++] = *cursor++; + } + destination[out] = '\0'; +} + +bool TemplateArgument(const char* type, char* argument, const int capacity) { + if (type == nullptr) return false; + const char* begin = std::strchr(type, '<'); + const char* end = std::strrchr(type, '>'); + if (begin == nullptr || end == nullptr || begin >= end) return false; + ++begin; + while (begin < end && std::isspace(static_cast(*begin))) ++begin; + int out = 0; + while (begin < end && *begin != ',' && out + 1 < capacity) { + argument[out++] = *begin++; + } + while (out > 0 && std::isspace(static_cast(argument[out - 1]))) { + --out; + } + argument[out] = '\0'; + return out > 0; +} + +bool IsPointerType(const char* type, const char* ops) { + return (type != nullptr && std::strchr(type, '*') != nullptr) + || (ops != nullptr && std::strchr(ops, '*') != nullptr); +} + +int FixedArrayCount(const char* ops) { + if (ops == nullptr) return 0; + const char* begin = std::strchr(ops, '['); + if (begin == nullptr) return 0; + char* end = nullptr; + const long count = std::strtol(begin + 1, &end, 10); + return end != begin + 1 && end != nullptr && *end == ']' && count > 0 + ? static_cast(count) : 0; +} + +} // namespace + +const classTypeInfo_t* idTypeInfoTools::FindClassInfo(const char* name) const { + if (typeInfo == nullptr || name == nullptr) return nullptr; + char cleanName[256] = {}; + CopyTypeName(name, cleanName, sizeof(cleanName)); + for (int index = 0; index < typeInfo->numClasses; ++index) { + const classTypeInfo_t& info = typeInfo->classes[index]; + if (info.name != nullptr && std::strcmp(info.name, cleanName) == 0) { + return &info; + } + } + return nullptr; +} + +const classVariableInfo_t* idTypeInfoTools::FindClassVariableInfo( + const classTypeInfo_t* classInfo, const char* name) const { + for (const classTypeInfo_t* current = classInfo; current != nullptr;) { + if (current->variables != nullptr) { + for (const classVariableInfo_t* variable = current->variables; + variable->name != nullptr; ++variable) { + if (std::strcmp(variable->name, name) == 0) return variable; + } + } + current = current->superType == nullptr || *current->superType == '\0' + ? nullptr : FindClassInfo(current->superType); + } + return nullptr; +} + +bool idTypeInfoTools::ResolvePath(const char* rootType, void* rootObject, + const char* path, resolvedPath_t& result) const { + static thread_local char resolvedElementType[256]; + result = {}; + if (rootObject == nullptr || path == nullptr) return false; + const classTypeInfo_t* currentClass = FindClassInfo(rootType); + if (currentClass == nullptr) return false; + unsigned char* currentObject = static_cast(rootObject); + const char* cursor = path; + + while (*cursor != '\0') { + char memberName[256] = {}; + int memberLength = 0; + while (*cursor != '\0' && *cursor != '.' && *cursor != '[' + && memberLength + 1 < static_cast(sizeof(memberName))) { + memberName[memberLength++] = *cursor++; + } + memberName[memberLength] = '\0'; + if (memberLength == 0) return false; + + const classVariableInfo_t* variable = + FindClassVariableInfo(currentClass, memberName); + if (variable == nullptr) return false; + unsigned char* valuePointer = currentObject + variable->offset; + const char* valueType = variable->type; + const char* valueOps = variable->ops; + + if (*cursor == '[') { + char* indexEnd = nullptr; + const long index = std::strtol(cursor + 1, &indexEnd, 10); + if (indexEnd == cursor + 1 || indexEnd == nullptr || *indexEnd != ']' + || index < 0) return false; + cursor = indexEnd + 1; + char elementType[256] = {}; + if (TemplateArgument(valueType, elementType, sizeof(elementType))) { + const listLayout_t* const list = + reinterpret_cast(valuePointer); + if (list->list == nullptr || index >= list->num) return false; + const int elementSize = SizeForType(elementType, ""); + if (elementSize <= 0) return false; + valuePointer = static_cast(list->list) + + index * elementSize; + std::strncpy(resolvedElementType, elementType, + sizeof(resolvedElementType) - 1); + resolvedElementType[sizeof(resolvedElementType) - 1] = '\0'; + valueType = resolvedElementType; + valueOps = ""; + currentClass = FindClassInfo(valueType); + } else { + const int arrayCount = FixedArrayCount(valueOps); + const int elementSize = SizeForType(valueType, ""); + if (arrayCount <= 0 || index >= arrayCount || elementSize <= 0) { + return false; + } + valuePointer += index * elementSize; + valueOps = ""; + currentClass = FindClassInfo(valueType); + } + } else { + if (IsPointerType(valueType, valueOps)) { + valuePointer = *reinterpret_cast(valuePointer); + if (valuePointer == nullptr) return false; + } + currentClass = FindClassInfo(valueType); + } + + if (*cursor == '.') { + ++cursor; + if (currentClass == nullptr) return false; + currentObject = valuePointer; + continue; + } + if (*cursor != '\0') return false; + result.pointer = valuePointer; + result.type = valueType; + result.ops = valueOps; + return true; + } + return false; +} + +bool idTypeInfoTools::GetPointerForPath(const char* rootType, const char* path, + void** objectPointer) const { + if (objectPointer == nullptr || *objectPointer == nullptr) return false; + resolvedPath_t result = {}; + if (!ResolvePath(rootType, *objectPointer, path, result)) return false; + *objectPointer = result.pointer; + return true; +} + +idTypeInfoObject::idTypeInfoObject(void* objectPointer, + const char* objectTypeName, const idTypeInfoTools* tools) + : objectPtr(objectPointer), objectType(objectTypeName), ti(tools), + modified(false) { +} + +void idTypeInfoObject::GetVariableName(const char* path, idStr& name, + int& arrayIndex) { + const char* const safePath = path == nullptr ? "" : path; + const char* begin = std::strrchr(safePath, '.'); + begin = begin == nullptr ? safePath : begin + 1; + name = begin; + arrayIndex = -1; + const char* bracket = std::strrchr(begin, '['); + const std::size_t length = std::strlen(begin); + if (bracket != nullptr && length > 0 && begin[length - 1] == ']') { + arrayIndex = std::atoi(bracket + 1); + char text[256] = {}; + const std::size_t baseLength = static_cast(bracket - begin); + const std::size_t amount = baseLength < sizeof(text) - 1 + ? baseLength : sizeof(text) - 1; + std::memcpy(text, begin, amount); + name = text; + } +} + +bool idTypeInfoObject::Resolve(const char* path, + idTypeInfoTools::resolvedPath_t& result) const { + return ti != nullptr && ti->ResolvePath(objectType.c_str(), objectPtr, + path, result); +} + +bool idTypeInfoObject::GetBool(const idTypeInfoVariable_bool& variable, + bool& value) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(variable.path, resolved) || resolved.pointer == nullptr) return false; + value = *static_cast(resolved.pointer); + return true; +} + +bool idTypeInfoObject::GetInt(const idTypeInfoVariable_int& variable, + int& value) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(variable.path, resolved) || resolved.pointer == nullptr) return false; + value = *static_cast(resolved.pointer); + return true; +} + +bool idTypeInfoObject::GetFloat(const idTypeInfoVariable_float& variable, + float& value) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(variable.path, resolved) || resolved.pointer == nullptr) return false; + value = *static_cast(resolved.pointer); + return true; +} + +bool idTypeInfoObject::GetStrPtrType( + const idTypeInfoVariable_StrPtr& variable, idStr& value, + const bool resolvePointers) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(variable.path, resolved)) return false; + if (resolved.pointer == nullptr) { + value.Clear(); + return true; + } + if (!resolvePointers) { + char address[32] = {}; + std::snprintf(address, sizeof(address), "%p", resolved.pointer); + value = address; + return true; + } + value = static_cast(resolved.pointer); + return true; +} + +bool idTypeInfoObject::GetTypeInfoObjectForListElement( + const idTypeInfoVariable_idList& variable, const int index, + idTypeInfoObject& object) const { + char path[1024] = {}; + std::snprintf(path, sizeof(path), "%s[%d]", variable.path, index); + void* pointer = objectPtr; + if (ti == nullptr || !ti->GetPointerForPath(objectType.c_str(), path, + &pointer)) return false; + object.objectPtr = pointer; + object.objectType = variable.argType; + object.ti = ti; + object.modified = false; + return true; +} + +bool idTypeInfoObject::GetStrType(const char*, const char*, const char* path, + idStr& value, const bool resolvePointers) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(path, resolved)) return false; + if (resolved.type != nullptr && std::strcmp(resolved.type, "idStr") == 0) { + value = *static_cast(resolved.pointer); + return true; + } + if (resolved.type != nullptr && IsPointerType(resolved.type, resolved.ops)) { + if (!resolvePointers) { + char address[32] = {}; + std::snprintf(address, sizeof(address), "%p", resolved.pointer); + value = address; + } else { + value = resolved.pointer == nullptr ? "" + : static_cast(resolved.pointer); + } + return true; + } + value = resolved.pointer == nullptr ? "" + : static_cast(resolved.pointer); + return true; +} + +bool idTypeInfoObject::GetValueText(const char* path, idStr& text) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(path, resolved) || resolved.pointer == nullptr) return false; + char value[256] = {}; + if (resolved.type != nullptr && std::strcmp(resolved.type, "bool") == 0) { + std::strcpy(value, *static_cast(resolved.pointer) + ? "true" : "false"); + } else if (resolved.type != nullptr + && (std::strcmp(resolved.type, "int") == 0 + || std::strcmp(resolved.type, "unsigned int") == 0)) { + std::snprintf(value, sizeof(value), "%d", + *static_cast(resolved.pointer)); + } else if (resolved.type != nullptr + && std::strcmp(resolved.type, "float") == 0) { + std::snprintf(value, sizeof(value), "%.9g", + *static_cast(resolved.pointer)); + } else if (resolved.type != nullptr + && std::strcmp(resolved.type, "idStr") == 0) { + text = static_cast(resolved.pointer)->c_str(); + return true; + } else { + std::snprintf(value, sizeof(value), "%p", resolved.pointer); + } + text = value; + return true; +} + +bool idTypeInfoObject::GetStr(const idTypeInfoVariable_idStr& variable, + idStr& value) const { + return GetStrType(variable.type, "", variable.path, value, true); +} + +bool idTypeInfoObject::GetListNum(const idTypeInfoVariable_idList& variable, + int& num) const { + idTypeInfoTools::resolvedPath_t resolved = {}; + if (!Resolve(variable.path, resolved) || resolved.pointer == nullptr) return false; + num = reinterpret_cast(resolved.pointer)->num; + return true; +} diff --git a/source/shared/idlib/typeinfo/typeinfoobject.h b/source/shared/idlib/typeinfo/typeinfoobject.h new file mode 100644 index 0000000..b871a31 --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfoobject.h @@ -0,0 +1,332 @@ +#pragma once + +#include "idlib/text/str.h" + +#include +#include +#include + +class idTypeInfoFile; + +struct constantInfo_t { + const char* type; + const char* name; + const char* value; +}; + +struct enumValueInfo_t { + const char* name; + int value; +}; + +struct enumTypeInfo_t { + const char* name; + int flags; + const enumValueInfo_t* values; +}; + +struct classVariableInfo_t { + const char* type; + const char* ops; + const char* name; + int offset; + int size; + int flags; + const char* comment; + int (*get)(void*); + void (*set)(void*, int); + void* (*reallocate)(void*, int, int, int, bool); +}; + +struct classMetaDataInfo_t { const char* metaData; }; + +struct classTypeInfo_t { + const char* name; + const char* superType; + int size; + const void* typeId; + const classVariableInfo_t* templateParms; + const classVariableInfo_t* variables; + const classMetaDataInfo_t* metaData; +}; + +struct typedefInfo_t { + const char* name; + const char* type; + const char* ops; + int size; +}; + +struct functionPointerInfo_t { const char* name; void* ptr; }; + +struct typeInfo_t { + constantInfo_t* constants; + int numConstants; + enumTypeInfo_t* enums; + int numEnums; + classTypeInfo_t* classes; + int numClasses; + typedefInfo_t* typedefs; + int numTypedefs; + functionPointerInfo_t* functionPointers; + int numFunctionPointers; + unsigned int superScriptVersion; +}; + +class idTypeInfoTools { +public: + explicit idTypeInfoTools(const typeInfo_t* info = nullptr) + : typeInfo(info), enumHash{}, classHash{}, enumObject{}, enumPointer{}, + classObject{}, classPointer{}, editDepth(0), designDepth(0), + defDepth(0), warnings{} {} + ~idTypeInfoTools(); + idTypeInfoTools(const idTypeInfoTools&) = delete; + idTypeInfoTools& operator=(const idTypeInfoTools&) = delete; + + struct resolvedPath_t { + void* pointer; + const char* type; + const char* ops; + }; + + bool ResolvePath(const char* rootType, void* rootObject, const char* path, + resolvedPath_t& result) const; + bool GetPointerForPath(const char* rootType, const char* path, + void** objectPointer) const; + const classTypeInfo_t* FindClassInfo(const char* name) const; + const classVariableInfo_t* FindClassVariableInfo( + const classTypeInfo_t* classInfo, const char* name) const; + + void Init(const typeInfo_t* info); + void Shutdown(); + void ClearWarnings() const; + void AddWarning(const char* format, ...) const; + int GetWarningCount() const; + const char* GetWarning(int index) const; + + static bool IsUninitializedType(const char* type, const char* ops); + int FindEnumIndex(const char* typeName) const; + const enumTypeInfo_t* FindEnumInfo(const char* typeName) const; + const enumValueInfo_t* FindEnumValueInfo(const enumTypeInfo_t* enumInfo, + const char* name, bool defaultIfNotFound = false) const; + const enumValueInfo_t* FindEnumValueInfo(const enumTypeInfo_t* enumInfo, + int value) const; + const enumValueInfo_t* FindEnumValueInfo(const char* enumName, int value, + bool defaultIfNotFound = false) const; + const char* FindEnumValueName(const char* enumTypeName, int value) const; + int FindEnumValue(const char* enumTypeName, const char* name, + int defaultValue = 0) const; + const char* GetEnumName(const char* enumTypeName, int enumValue, + const char* defaultValue = "unknown") const; + bool GetEnumBitFlags(int flags, const char* enumTypeName, + idStr& values, const char* separator = " | ") const; + + int FindClassIndex(const char* typeName) const; + int FindTypeDefIndex(const char* typeName) const; + int SizeForType(const char* type, const char* ops = "") const; + bool IsSubclassOf(const char* className, const char* superClassName) const; + const char* GetVariableNameFromOffset(const char* className, + int offset) const; + const char* GetVariableTypeFromOffset(const char* className, + int offset) const; + const char* GetVariableOpsFromOffset(const char* className, + int offset) const; + bool GetTypeForPath(const char* rootType, const char* path, + char* type, int typeSize, char* ops, int opsSize) const; + + void WriteObject(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object) const; + void ReadObject(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object) const; + bool WriteObjectVariable(idTypeInfoFile& file, const char* rootType, + void* object, const char* path) const; + + class stringList_t { + public: + stringList_t() : list(nullptr), num(0), size(0), granularity(16), + memTag(5), listStatic(0) {} + ~stringList_t() { delete[] list; } + int Append(const idStr& value) { + if (num == size) { + const int newSize = size == 0 ? granularity : size + granularity; + idStr* replacement = new (std::nothrow) idStr[newSize]; + if (replacement == nullptr) return -1; + for (int i = 0; i < num; ++i) replacement[i] = list[i]; + delete[] list; + list = replacement; + size = newSize; + } + list[num] = value; + return num++; + } + void Clear() { num = 0; } + int Num() const { return num; } + idStr& operator[](int index) { return list[index]; } + const idStr& operator[](int index) const { return list[index]; } + private: + idStr* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; + }; + + void FindClassVariablePathsForType(const classTypeInfo_t* classInfo, + const char* type, const char* ops, stringList_t& paths) const; + void FindClassVariablePathsForTypeIncludingInherited( + const classTypeInfo_t* classInfo, const char* type, const char* ops, + stringList_t& paths) const; + void FindClassVariablePathsForTemplateType( + const classTypeInfo_t* classInfo, const char* templateType, + const char* argumentType, stringList_t& paths, + stringList_t& argumentOps) const; + + const typeInfo_t* typeInfo; + +private: + struct opaqueList_t { + void* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; + }; + + bool WriteValue(idTypeInfoFile& file, const char* type, const char* ops, + const char* name, void* object, bool writeName) const; + bool ReadValue(idTypeInfoFile& file, const char* type, const char* ops, + const char* name, void* object, bool expectName) const; + void CollectPaths(const classTypeInfo_t* classInfo, const char* prefix, + const char* type, const char* ops, bool includeInherited, + stringList_t& paths) const; + + std::uint32_t enumHash[8]; + std::uint32_t classHash[8]; + opaqueList_t enumObject; + opaqueList_t enumPointer; + opaqueList_t classObject; + opaqueList_t classPointer; + int editDepth; + int designDepth; + int defDepth; + opaqueList_t warnings; +}; + +struct idPathTypeInfo { + idPathTypeInfo() + : type(), ops(), name(), arrayIndex(-1), flags(0), objectPtr(nullptr), + size(0), get(nullptr), set(nullptr), editDepth(0), designDepth(0), + defDepth(0), metaData{} {} + idStr type; + idStr ops; + idStr name; + int arrayIndex; + int flags; + unsigned char* objectPtr; + int size; + int (*get)(void*); + void (*set)(void*, int); + int editDepth; + int designDepth; + int defDepth; + std::uint32_t metaData[4]; +}; + +struct idTypeInfoVariable { + idTypeInfoVariable(const char* typeName = "", const char* typeOps = "", + const char* variablePath = "") + : type(typeName), ops(typeOps), path(variablePath) {} + const char* type; + const char* ops; + const char* path; +}; + +struct idTypeInfoVariable_bool : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; +struct idTypeInfoVariable_int : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; +struct idTypeInfoVariable_float : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; +struct idTypeInfoVariable_StrPtr : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; + +struct idTypeInfoVariableTemplate : idTypeInfoVariable { + idTypeInfoVariableTemplate(const char* typeName = "", + const char* typeOps = "", const char* variablePath = "", + const char* argumentType = "", const char* argumentOps = "") + : idTypeInfoVariable(typeName, typeOps, variablePath), + argType(argumentType), argOps(argumentOps) {} + const char* argType; + const char* argOps; +}; + +struct idTypeInfoVariable_idList : idTypeInfoVariableTemplate { + using idTypeInfoVariableTemplate::idTypeInfoVariableTemplate; +}; +struct idTypeInfoVariable_enum : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; +struct idTypeInfoVariable_idStr : idTypeInfoVariable { + using idTypeInfoVariable::idTypeInfoVariable; +}; + +class idTypeInfoObject { +public: + idTypeInfoObject(void* objectPointer, const char* objectTypeName, + const idTypeInfoTools* tools); + + bool GetBool(const idTypeInfoVariable_bool& variable, bool& value) const; + bool GetInt(const idTypeInfoVariable_int& variable, int& value) const; + bool GetFloat(const idTypeInfoVariable_float& variable, float& value) const; + bool GetStrPtrType(const idTypeInfoVariable_StrPtr& variable, idStr& value, + bool resolvePointers = true) const; + bool GetTypeInfoObjectForListElement( + const idTypeInfoVariable_idList& variable, int index, + idTypeInfoObject& object) const; + bool GetStrType(const char* type, const char* ops, const char* path, + idStr& value, bool resolvePointers = true) const; + bool GetValueText(const char* path, idStr& text) const; + bool GetStr(const idTypeInfoVariable_idStr& variable, idStr& value) const; + bool GetListNum(const idTypeInfoVariable_idList& variable, int& num) const; + + void* GetObjectPointer() const { return objectPtr; } + const char* GetObjectType() const { return objectType.c_str(); } + bool IsModified() const { return modified; } + void SetModified(bool value = true) { modified = value; } + +private: + static void GetVariableName(const char* path, idStr& name, + int& arrayIndex); + bool Resolve(const char* path, idTypeInfoTools::resolvedPath_t& result) const; + + void* objectPtr; + idStr objectType; + const idTypeInfoTools* ti; + bool modified; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(classVariableInfo_t) == 40, + "Recovered classVariableInfo_t ABI changed"); +static_assert(sizeof(classTypeInfo_t) == 28, + "Recovered classTypeInfo_t ABI changed"); +static_assert(sizeof(typeInfo_t) == 44, "Recovered typeInfo_t ABI changed"); +static_assert(sizeof(idTypeInfoVariable) == 12, + "Recovered idTypeInfoVariable ABI changed"); +static_assert(sizeof(idTypeInfoVariableTemplate) == 20, + "Recovered template variable ABI changed"); +static_assert(sizeof(idTypeInfoObject) == 44, + "Recovered idTypeInfoObject ABI changed"); +static_assert(sizeof(idTypeInfoTools::stringList_t) == 16, + "Recovered type-info string list ABI changed"); +static_assert(sizeof(idTypeInfoTools) == 160, + "Recovered idTypeInfoTools ABI changed"); +static_assert(sizeof(idPathTypeInfo) == 148, + "Recovered idPathTypeInfo ABI changed"); +#endif diff --git a/source/shared/idlib/typeinfo/typeinfotools.cpp b/source/shared/idlib/typeinfo/typeinfotools.cpp new file mode 100644 index 0000000..914c32b --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfotools.cpp @@ -0,0 +1,564 @@ +#include "typeinfoobject.h" +#include "typeinfofile.h" + +#ifdef strcmp +#undef strcmp +#endif +#ifdef snprintf +#undef snprintf +#endif +#ifdef vsnprintf +#undef vsnprintf +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::mutex warningMutex; +std::unordered_map> warningLists; + +std::string CleanType(const char* source) { + const char* cursor = source == nullptr ? "" : source; + while (std::isspace(static_cast(*cursor))) ++cursor; + if (std::strncmp(cursor, "const ", 6) == 0) cursor += 6; + std::string result; + while (*cursor != 0 && *cursor != '*' && *cursor != '&') { + result.push_back(*cursor++); + } + while (!result.empty() + && std::isspace(static_cast(result.back()))) { + result.pop_back(); + } + return result; +} + +bool HasPointer(const char* type, const char* ops) { + return (type != nullptr && std::strchr(type, '*') != nullptr) + || (ops != nullptr && std::strchr(ops, '*') != nullptr); +} + +int ArrayCount(const char* ops, std::string* stripped = nullptr) { + std::string value = ops == nullptr ? "" : ops; + const std::size_t close = value.rfind(']'); + const std::size_t open = value.rfind('['); + if (open == std::string::npos || close != value.size() - 1 || open >= close) { + if (stripped != nullptr) *stripped = value; + return 1; + } + char* end = nullptr; + const long count = std::strtol(value.c_str() + open + 1, &end, 10); + if (end == value.c_str() + open + 1 || *end != ']') { + if (stripped != nullptr) *stripped = value; + return 1; + } + if (stripped != nullptr) *stripped = value.substr(0, open); + return count > 0 ? static_cast(count) : 0; +} + +bool TemplateParts(const char* type, std::string& container, + std::string& argument) { + const std::string text = type == nullptr ? "" : type; + const std::size_t open = text.find('<'); + const std::size_t close = text.rfind('>'); + if (open == std::string::npos || close == std::string::npos || open >= close) { + return false; + } + container = text.substr(0, open); + argument = text.substr(open + 1, close - open - 1); + const std::size_t comma = argument.find(','); + if (comma != std::string::npos) argument.resize(comma); + while (!argument.empty() + && std::isspace(static_cast(argument.back()))) { + argument.pop_back(); + } + while (!argument.empty() + && std::isspace(static_cast(argument.front()))) { + argument.erase(argument.begin()); + } + return !container.empty() && !argument.empty(); +} + +void CopyText(char* destination, const int capacity, const char* source) { + if (destination == nullptr || capacity <= 0) return; + std::strncpy(destination, source == nullptr ? "" : source, + static_cast(capacity - 1)); + destination[capacity - 1] = 0; +} + +const classVariableInfo_t* VariableAtOffset(const idTypeInfoTools& tools, + const classTypeInfo_t* info, const int offset) { + for (const classTypeInfo_t* current = info; current != nullptr;) { + if (current->variables != nullptr) { + for (const classVariableInfo_t* variable = current->variables; + variable->name != nullptr; ++variable) { + if (offset >= variable->offset + && offset < variable->offset + variable->size) return variable; + } + } + current = current->superType != nullptr && *current->superType != 0 + ? tools.FindClassInfo(current->superType) : nullptr; + } + return nullptr; +} + +} // namespace + +idTypeInfoTools::~idTypeInfoTools() { Shutdown(); } + +void idTypeInfoTools::Init(const typeInfo_t* info) { + Shutdown(); + typeInfo = info; +} + +void idTypeInfoTools::Shutdown() { + ClearWarnings(); + typeInfo = nullptr; + std::memset(enumHash, 0, sizeof(enumHash)); + std::memset(classHash, 0, sizeof(classHash)); + enumObject = {}; + enumPointer = {}; + classObject = {}; + classPointer = {}; + editDepth = designDepth = defDepth = 0; + warnings = {}; +} + +void idTypeInfoTools::ClearWarnings() const { + std::lock_guard lock(warningMutex); + warningLists.erase(this); +} + +void idTypeInfoTools::AddWarning(const char* format, ...) const { + char buffer[2048] = {}; + va_list args; + va_start(args, format); + std::vsnprintf(buffer, sizeof(buffer), format == nullptr ? "" : format, args); + va_end(args); + std::lock_guard lock(warningMutex); + warningLists[this].push_back(buffer); +} + +int idTypeInfoTools::GetWarningCount() const { + std::lock_guard lock(warningMutex); + const auto found = warningLists.find(this); + return found == warningLists.end() ? 0 + : static_cast(found->second.size()); +} + +const char* idTypeInfoTools::GetWarning(const int index) const { + std::lock_guard lock(warningMutex); + const auto found = warningLists.find(this); + if (found == warningLists.end() || index < 0 + || index >= static_cast(found->second.size())) return ""; + return found->second[static_cast(index)].c_str(); +} + +bool idTypeInfoTools::IsUninitializedType(const char* type, const char* ops) { + if (HasPointer(type, ops)) return true; + static const char* const types[] = { + "idLinkList", "idList", "idHashIndex", "idStr", "idStrList", + "idDynamicBlockAlloc", "idBlockAlloc", "idVecX", "idMatX", nullptr + }; + const std::string clean = CleanType(type); + for (int index = 0; types[index] != nullptr; ++index) { + if (clean == types[index] + || clean.compare(0, std::strlen(types[index]), types[index]) == 0) { + return true; + } + } + return false; +} + +int idTypeInfoTools::FindEnumIndex(const char* typeName) const { + if (typeInfo == nullptr || typeName == nullptr) return -1; + for (int index = 0; index < typeInfo->numEnums; ++index) { + if (typeInfo->enums[index].name != nullptr + && std::strcmp(typeInfo->enums[index].name, typeName) == 0) return index; + } + return -1; +} + +const enumTypeInfo_t* idTypeInfoTools::FindEnumInfo(const char* typeName) const { + const int index = FindEnumIndex(typeName); + return index < 0 ? nullptr : &typeInfo->enums[index]; +} + +const enumValueInfo_t* idTypeInfoTools::FindEnumValueInfo( + const enumTypeInfo_t* enumInfo, const char* name, + const bool defaultIfNotFound) const { + if (enumInfo == nullptr || enumInfo->values == nullptr) return nullptr; + const enumValueInfo_t* first = enumInfo->values; + for (const enumValueInfo_t* value = first; value->name != nullptr; ++value) { + if (name != nullptr && std::strcmp(value->name, name) == 0) return value; + } + return defaultIfNotFound ? first : nullptr; +} + +const enumValueInfo_t* idTypeInfoTools::FindEnumValueInfo( + const enumTypeInfo_t* enumInfo, const int expected) const { + if (enumInfo == nullptr || enumInfo->values == nullptr) return nullptr; + for (const enumValueInfo_t* value = enumInfo->values; + value->name != nullptr; ++value) { + if (value->value == expected) return value; + } + return nullptr; +} + +const enumValueInfo_t* idTypeInfoTools::FindEnumValueInfo( + const char* enumName, const int value, + const bool defaultIfNotFound) const { + const enumTypeInfo_t* const info = FindEnumInfo(enumName); + const enumValueInfo_t* const found = FindEnumValueInfo(info, value); + return found != nullptr || !defaultIfNotFound ? found + : info != nullptr ? info->values : nullptr; +} + +const char* idTypeInfoTools::FindEnumValueName(const char* enumTypeName, + const int value) const { + const enumValueInfo_t* const info = FindEnumValueInfo(enumTypeName, value); + return info == nullptr || info->name == nullptr ? "unknown" : info->name; +} + +int idTypeInfoTools::FindEnumValue(const char* enumTypeName, const char* name, + const int defaultValue) const { + const enumValueInfo_t* const value = FindEnumValueInfo( + FindEnumInfo(enumTypeName), name, false); + return value == nullptr ? defaultValue : value->value; +} + +const char* idTypeInfoTools::GetEnumName(const char* enumTypeName, + const int enumValue, const char* defaultValue) const { + const enumValueInfo_t* const value = FindEnumValueInfo( + FindEnumInfo(enumTypeName), enumValue); + return value == nullptr || value->name == nullptr ? defaultValue : value->name; +} + +bool idTypeInfoTools::GetEnumBitFlags(const int flags, + const char* enumTypeName, idStr& values, const char* separator) const { + const enumTypeInfo_t* const info = FindEnumInfo(enumTypeName); + if (info == nullptr || info->values == nullptr) return false; + values.Clear(); + for (const enumValueInfo_t* value = info->values; value->name != nullptr; + ++value) { + if (value->value != 0 && (flags & value->value) == value->value) { + if (values.Length() != 0) values.Append(separator == nullptr ? " | " : separator); + values.Append(value->name); + } + } + return values.Length() != 0 || flags == 0; +} + +int idTypeInfoTools::FindClassIndex(const char* typeName) const { + const classTypeInfo_t* const info = FindClassInfo(typeName); + return info == nullptr || typeInfo == nullptr ? -1 + : static_cast(info - typeInfo->classes); +} + +int idTypeInfoTools::FindTypeDefIndex(const char* typeName) const { + if (typeInfo == nullptr || typeName == nullptr) return -1; + for (int index = 0; index < typeInfo->numTypedefs; ++index) { + if (typeInfo->typedefs[index].name != nullptr + && std::strcmp(typeInfo->typedefs[index].name, typeName) == 0) return index; + } + return -1; +} + +int idTypeInfoTools::SizeForType(const char* type, const char* ops) const { + std::string strippedOps; + const int count = ArrayCount(ops, &strippedOps); + if (HasPointer(type, strippedOps.c_str())) return 4 * count; + const std::string clean = CleanType(type); + if (clean == "bool" || clean == "char" || clean == "unsigned char") return count; + if (clean == "short" || clean == "unsigned short" || clean == "wchar_t") return 2 * count; + if (clean == "int" || clean == "unsigned int" || clean == "long" + || clean == "unsigned long" || clean == "float" || clean == "size_t") return 4 * count; + if (clean == "double") return 8 * count; + if (clean == "idStr") return static_cast(sizeof(idStr)) * count; + const int typeDef = FindTypeDefIndex(clean.c_str()); + if (typeDef >= 0) return typeInfo->typedefs[typeDef].size * count; + if (FindEnumInfo(clean.c_str()) != nullptr) return 4 * count; + const classTypeInfo_t* const classInfo = FindClassInfo(clean.c_str()); + return classInfo == nullptr ? -1 : classInfo->size * count; +} + +bool idTypeInfoTools::IsSubclassOf(const char* className, + const char* superClassName) const { + for (const classTypeInfo_t* info = FindClassInfo(className); info != nullptr;) { + if (std::strcmp(info->name, superClassName == nullptr ? "" : superClassName) == 0) return true; + info = info->superType != nullptr && *info->superType != 0 + ? FindClassInfo(info->superType) : nullptr; + } + return false; +} + +const char* idTypeInfoTools::GetVariableNameFromOffset(const char* className, + const int offset) const { + const classVariableInfo_t* const variable = VariableAtOffset( + *this, FindClassInfo(className), offset); + return variable == nullptr ? "" : variable->name; +} + +const char* idTypeInfoTools::GetVariableTypeFromOffset(const char* className, + const int offset) const { + const classVariableInfo_t* const variable = VariableAtOffset( + *this, FindClassInfo(className), offset); + return variable == nullptr ? "" : variable->type; +} + +const char* idTypeInfoTools::GetVariableOpsFromOffset(const char* className, + const int offset) const { + const classVariableInfo_t* const variable = VariableAtOffset( + *this, FindClassInfo(className), offset); + return variable == nullptr ? "" : variable->ops; +} + +bool idTypeInfoTools::GetTypeForPath(const char* rootType, const char* path, + char* outputType, const int typeSize, char* outputOps, + const int opsSize) const { + const classTypeInfo_t* current = FindClassInfo(rootType); + const char* cursor = path == nullptr ? "" : path; + const classVariableInfo_t* variable = nullptr; + while (*cursor != 0 && current != nullptr) { + char name[256] = {}; + int length = 0; + while (*cursor != 0 && *cursor != '.' && *cursor != '[' + && length < static_cast(sizeof(name) - 1)) name[length++] = *cursor++; + variable = FindClassVariableInfo(current, name); + if (variable == nullptr) return false; + if (*cursor == '[') { + while (*cursor != 0 && *cursor != ']') ++cursor; + if (*cursor == ']') ++cursor; + std::string container, argument; + current = TemplateParts(variable->type, container, argument) + ? FindClassInfo(argument.c_str()) : FindClassInfo(variable->type); + } else { + current = FindClassInfo(variable->type); + } + if (*cursor == '.') ++cursor; + else if (*cursor != 0) return false; + } + if (variable == nullptr) return false; + CopyText(outputType, typeSize, variable->type); + CopyText(outputOps, opsSize, variable->ops); + return true; +} + +bool idTypeInfoTools::WriteValue(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object, + const bool writeName) const { + const std::string clean = CleanType(type); + std::string elementOps; + const int arrayCount = ArrayCount(ops, &elementOps); + if (arrayCount != 1 || (ops != nullptr && std::strchr(ops, '[') != nullptr)) { + if (writeName) file.WriteType(type, ops, name); + file.WriteOpeningBrace(); + const int elementSize = SizeForType(type, elementOps.c_str()); + if (elementSize <= 0) { file.WriteClosingBrace(); return false; } + for (int index = 0; index < arrayCount; ++index) { + file.WriteArrayElementType(type, elementOps.c_str(), + name == nullptr ? "item" : name, index); + WriteValue(file, type, elementOps.c_str(), name, + static_cast(object) + index * elementSize, + false); + } + file.WriteClosingBrace(); + return true; + } + if (writeName) file.WriteType(type, ops, name); + if (HasPointer(type, ops)) { + void* const target = object == nullptr ? nullptr : *static_cast(object); + if (target == nullptr) { file.WriteNullPointer(); return true; } + return WriteValue(file, type, "", name, target, false); + } + if (object == nullptr) { file.WriteUnknown(); return false; } + if (clean == "bool") file.WriteBool(*static_cast(object)); + else if (clean == "char") file.WriteChar(*static_cast(object)); + else if (clean == "unsigned char") file.WriteUnsignedChar(*static_cast(object)); + else if (clean == "wchar_t") file.WriteWChar(*static_cast(object)); + else if (clean == "short") file.WriteShort(*static_cast(object)); + else if (clean == "unsigned short") file.WriteUnsignedShort(*static_cast(object)); + else if (clean == "unsigned int" || clean == "unsigned long" || clean == "size_t") file.WriteUnsignedLong(*static_cast(object)); + else if (clean == "int" || clean == "long") file.WriteInt(*static_cast(object)); + else if (clean == "float") file.WriteFloat(*static_cast(object)); + else if (clean == "double") file.WriteDouble(*static_cast(object)); + else if (clean == "idStr") file.WriteStr(static_cast(object)->c_str()); + else if (FindEnumInfo(clean.c_str()) != nullptr) file.WriteInt(*static_cast(object)); + else { + const classTypeInfo_t* const classInfo = FindClassInfo(clean.c_str()); + if (classInfo == nullptr) { + file.WriteUnknown(); + AddWarning("Unknown type '%s' while writing '%s'", type, name); + return false; + } + file.WriteOpeningBrace(); + std::vector hierarchy; + for (const classTypeInfo_t* current = classInfo; current != nullptr; + current = current->superType != nullptr && *current->superType != 0 + ? FindClassInfo(current->superType) : nullptr) hierarchy.push_back(current); + for (auto level = hierarchy.rbegin(); level != hierarchy.rend(); ++level) { + if ((*level)->variables == nullptr) continue; + for (const classVariableInfo_t* variable = (*level)->variables; + variable->name != nullptr; ++variable) { + WriteValue(file, variable->type, variable->ops, variable->name, + static_cast(object) + variable->offset, true); + file.WriteComment(variable->comment); + } + } + file.WriteClosingBrace(); + } + return true; +} + +bool idTypeInfoTools::ReadValue(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object, + const bool expectName) const { + if (expectName && !file.ExpectType(type, ops, name)) return false; + const std::string clean = CleanType(type); + std::string elementOps; + const int arrayCount = ArrayCount(ops, &elementOps); + if (arrayCount != 1 || (ops != nullptr && std::strchr(ops, '[') != nullptr)) { + file.ReadOpeningBrace(); + const int elementSize = SizeForType(type, elementOps.c_str()); + if (elementSize <= 0) { file.ReadUnknown(); return false; } + for (int index = 0; index < arrayCount; ++index) { + int readIndex = -1; + if (!file.ExpectArrayElementType(type, elementOps.c_str(), + name == nullptr ? "item" : name, readIndex) + || readIndex != index) return false; + ReadValue(file, type, elementOps.c_str(), name, + static_cast(object) + index * elementSize, + false); + } + file.ReadClosingBrace(); + return true; + } + if (HasPointer(type, ops)) { + if (file.CheckNullPointer()) { + if (object != nullptr) *static_cast(object) = nullptr; + return true; + } + void* target = object == nullptr ? nullptr : *static_cast(object); + if (target == nullptr) { file.ReadUnknown(); return false; } + return ReadValue(file, type, "", name, target, false); + } + if (object == nullptr) { file.ReadUnknown(); return false; } + if (clean == "bool") file.ReadBool(*static_cast(object)); + else if (clean == "char") file.ReadChar(*static_cast(object)); + else if (clean == "unsigned char") file.ReadUnsignedChar(*static_cast(object)); + else if (clean == "wchar_t") file.ReadWChar(*static_cast(object)); + else if (clean == "short") file.ReadShort(*static_cast(object)); + else if (clean == "unsigned short") file.ReadUnsignedShort(*static_cast(object)); + else if (clean == "unsigned int" || clean == "unsigned long" || clean == "size_t") file.ReadUnsignedLong(*static_cast(object)); + else if (clean == "int" || clean == "long") file.ReadInt(*static_cast(object)); + else if (clean == "float") file.ReadFloat(*static_cast(object)); + else if (clean == "double") file.ReadDouble(*static_cast(object)); + else if (clean == "idStr") file.ReadStr(*static_cast(object)); + else if (FindEnumInfo(clean.c_str()) != nullptr) file.ReadInt(*static_cast(object)); + else { + const classTypeInfo_t* const classInfo = FindClassInfo(clean.c_str()); + if (classInfo == nullptr) { file.ReadUnknown(); return false; } + file.ReadOpeningBrace(); + std::vector hierarchy; + for (const classTypeInfo_t* current = classInfo; current != nullptr; + current = current->superType != nullptr && *current->superType != 0 + ? FindClassInfo(current->superType) : nullptr) hierarchy.push_back(current); + for (auto level = hierarchy.rbegin(); level != hierarchy.rend(); ++level) { + if ((*level)->variables == nullptr) continue; + for (const classVariableInfo_t* variable = (*level)->variables; + variable->name != nullptr; ++variable) { + ReadValue(file, variable->type, variable->ops, variable->name, + static_cast(object) + variable->offset, true); + } + } + file.ReadClosingBrace(); + } + return true; +} + +void idTypeInfoTools::WriteObject(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object) const { + WriteValue(file, type, ops, name, object, true); +} + +void idTypeInfoTools::ReadObject(idTypeInfoFile& file, const char* type, + const char* ops, const char* name, void* object) const { + ReadValue(file, type, ops, name, object, true); +} + +bool idTypeInfoTools::WriteObjectVariable(idTypeInfoFile& file, + const char* rootType, void* object, const char* path) const { + resolvedPath_t resolved{}; + if (!ResolvePath(rootType, object, path, resolved)) return false; + const char* name = std::strrchr(path == nullptr ? "" : path, '.'); + name = name == nullptr ? path : name + 1; + return WriteValue(file, resolved.type, resolved.ops, name, + resolved.pointer, true); +} + +void idTypeInfoTools::CollectPaths(const classTypeInfo_t* classInfo, + const char* prefix, const char* type, const char* ops, + const bool includeInherited, stringList_t& paths) const { + if (classInfo == nullptr) return; + if (includeInherited && classInfo->superType != nullptr + && *classInfo->superType != 0) { + CollectPaths(FindClassInfo(classInfo->superType), prefix, type, ops, + true, paths); + } + if (classInfo->variables == nullptr) return; + for (const classVariableInfo_t* variable = classInfo->variables; + variable->name != nullptr; ++variable) { + std::string path = prefix == nullptr ? "" : prefix; + if (!path.empty()) path.push_back('.'); + path += variable->name; + if (std::strcmp(variable->type, type == nullptr ? "" : type) == 0 + && std::strcmp(variable->ops == nullptr ? "" : variable->ops, + ops == nullptr ? "" : ops) == 0) paths.Append(idStr(path.c_str())); + const classTypeInfo_t* nested = FindClassInfo(variable->type); + if (nested != nullptr && !HasPointer(variable->type, variable->ops)) { + CollectPaths(nested, path.c_str(), type, ops, true, paths); + } + } +} + +void idTypeInfoTools::FindClassVariablePathsForType( + const classTypeInfo_t* classInfo, const char* type, const char* ops, + stringList_t& paths) const { + paths.Clear(); + CollectPaths(classInfo, "", type, ops, false, paths); +} + +void idTypeInfoTools::FindClassVariablePathsForTypeIncludingInherited( + const classTypeInfo_t* classInfo, const char* type, const char* ops, + stringList_t& paths) const { + paths.Clear(); + CollectPaths(classInfo, "", type, ops, true, paths); +} + +void idTypeInfoTools::FindClassVariablePathsForTemplateType( + const classTypeInfo_t* classInfo, const char* templateType, + const char* argumentType, stringList_t& paths, + stringList_t& argumentOps) const { + paths.Clear(); + argumentOps.Clear(); + if (classInfo == nullptr || classInfo->variables == nullptr) return; + for (const classVariableInfo_t* variable = classInfo->variables; + variable->name != nullptr; ++variable) { + std::string container, argument; + if (TemplateParts(variable->type, container, argument) + && container == (templateType == nullptr ? "" : templateType) + && argument == (argumentType == nullptr ? "" : argumentType)) { + paths.Append(idStr(variable->name)); + argumentOps.Append(idStr(variable->ops == nullptr ? "" : variable->ops)); + } + } +} diff --git a/source/shared/idlib/typeinfo/typeinfotree.cpp b/source/shared/idlib/typeinfo/typeinfotree.cpp new file mode 100644 index 0000000..ff2ed25 --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfotree.cpp @@ -0,0 +1,519 @@ +#include "typeinfotree.h" +#include "typeinfofile.h" + +#include +#include +#include +#include + +idTypeInfoTree::idTypeInfoTree() : root(nullptr), nodeBlockAlloc{} { + nodeBlockAlloc.allowAllocs = true; +} + +idTypeInfoTree::idTypeInfoTree(const char* text, const int length) + : idTypeInfoTree() { + if (text == nullptr || length <= 0) return; + idTypeInfoFile file; + file.settings.resolveEntityPointers = false; + file.settings.resolveModelPointers = false; + if (file.ReadMemory(text, length, "", 1)) Parse(file); +} + +idTypeInfoTree::~idTypeInfoTree() { + Clear(); +} + +void idTypeInfoTree::WriteType_r(idTypeInfoFile& file, + idTypeInfoNode* node, const bool onlyDiff) const { + if (node == nullptr || (onlyDiff && !node->diff)) return; + file.WriteType(node->type.c_str(), node->ops.c_str(), node->name.c_str()); + file.WriteSkipObject(node->skip); + if (node->children != nullptr) { + file.WriteOpeningBrace(); + file.WriteComment(node->comment.c_str()); + for (idTypeInfoNode* child = node->children; child != nullptr; + child = child->next) { + WriteType_r(file, child, onlyDiff); + } + file.WriteClosingBrace(); + } else if (node->value.Length() != 0) { + file.WriteValueString(node->value); + file.WriteComment(node->comment.c_str()); + } else { + file.WriteOpeningBrace(); + file.WriteComment(node->comment.c_str()); + file.WriteClosingBrace(); + } +} + +void idTypeInfoTree::Write(idTypeInfoFile& file, const bool onlyDiff) const { + for (idTypeInfoNode* node = root; node != nullptr; node = node->next) { + WriteType_r(file, node, onlyDiff); + } +} + +idTypeInfoNode* idTypeInfoTree::ReadType_r(idTypeInfoFile& file, + idTypeInfoNode* parentNode, const bool overwrite) { + idTypeInfoNode* first = parentNode == nullptr ? root : parentNode->children; + idTypeInfoNode* tail = first; + while (tail != nullptr && tail->next != nullptr) tail = tail->next; + + while (!file.CheckClosingBrace()) { + idStr type, ops, name; + int arrayIndex = -1; + if (!file.CheckArrayElementType(type, ops, name, arrayIndex)) break; + if (arrayIndex >= 0) { + char suffix[48]; + std::snprintf(suffix, sizeof(suffix), "[%d]", arrayIndex); + name.Append(suffix); + } + + const bool skip = file.CheckSkipObject(); + idStr value, comment; + const bool hasChildren = file.CheckOpeningBrace(); + if (hasChildren) file.ReadComment(comment); + else { + file.ReadValueString(value); + file.ReadComment(comment); + } + + idTypeInfoNode* node = nullptr; + if (overwrite) { + node = FindAndOverwriteNode(type.c_str(), ops.c_str(), + name.c_str(), value.c_str(), comment.c_str(), parentNode); + } + if (node == nullptr) { + node = InsertNode(type.c_str(), ops.c_str(), name.c_str(), + value.c_str(), comment.c_str(), parentNode, tail); + if (first == nullptr) first = node; + tail = node; + } + if (node == nullptr) break; + node->skip = skip; + if (hasChildren) ReadType_r(file, node, overwrite); + } + return first; +} + +void idTypeInfoTree::Parse(idTypeInfoFile& file) { + Clear(); + root = ReadType_r(file, nullptr, false); +} + +void idTypeInfoTree::ParseOverwrite(idTypeInfoFile& file) { + root = ReadType_r(file, nullptr, root != nullptr); + RemoveOutOfBoundsMembers(); +} + +bool idTypeInfoTree::EqualNoCase(const char* left, const char* right, + const int prefixLength) { + const unsigned char* a = reinterpret_cast( + left == nullptr ? "" : left); + const unsigned char* b = reinterpret_cast( + right == nullptr ? "" : right); + int compared = 0; + while ((prefixLength < 0 || compared < prefixLength) + && (*a != 0 || *b != 0)) { + if (std::tolower(*a) != std::tolower(*b)) return false; + ++a; + ++b; + ++compared; + } + return prefixLength >= 0 || *a == *b; +} + +idTypeInfoNode* idTypeInfoTree::InsertNode(const char* type, const char* ops, + const char* name, const char* value, const char* comment, + idTypeInfoNode* parentNode, idTypeInfoNode* nodeAfter) { + idTypeInfoNode* const node = new (std::nothrow) idTypeInfoNode; + if (node == nullptr) return nullptr; + node->type = type == nullptr ? "" : type; + node->ops = ops == nullptr ? "" : ops; + node->name = name == nullptr ? "" : name; + node->value = value == nullptr ? "" : value; + node->comment = comment == nullptr ? "" : comment; + node->next = nullptr; + node->children = nullptr; + node->parent = parentNode; + node->skip = false; + node->diff = true; + ++nodeBlockAlloc.total; + ++nodeBlockAlloc.active; + + idTypeInfoNode** head = parentNode == nullptr ? &root : &parentNode->children; + if (nodeAfter != nullptr) { + node->next = nodeAfter->next; + nodeAfter->next = node; + } else if (*head == nullptr) { + *head = node; + } else { + idTypeInfoNode* tail = *head; + while (tail->next != nullptr) tail = tail->next; + tail->next = node; + } + return node; +} + +idTypeInfoNode* idTypeInfoTree::SetRoot(const char* type, const char* name, + const char* value, const char* ops, const char* comment) { + Clear(); + return InsertNode(type, ops, name, value, comment, nullptr); +} + +idTypeInfoNode* idTypeInfoTree::FindAndOverwriteNode(const char* type, + const char* ops, const char* name, const char* value, + const char* comment, idTypeInfoNode* parentNode) { + idTypeInfoNode* node = parentNode == nullptr ? root : parentNode->children; + while (node != nullptr && std::strcmp(node->name.c_str(), name) != 0) { + node = node->next; + } + if (node == nullptr) return nullptr; + node->type = type; + node->ops = ops; + node->value = value; + node->comment = comment; + return node; +} + +idTypeInfoPath* idTypeInfoTree::ParsePath(const char* path) { + if (path == nullptr || *path == '\0') return nullptr; + idTypeInfoPath* first = nullptr; + idTypeInfoPath* last = nullptr; + const char* cursor = path; + while (*cursor != '\0') { + const char* begin = cursor; + int bracketDepth = 0; + while (*cursor != '\0') { + if (*cursor == '[') ++bracketDepth; + if (*cursor == ']') --bracketDepth; + if (*cursor == '.' && bracketDepth == 0) break; + ++cursor; + } + char segment[256] = {}; + const std::size_t length = static_cast(cursor - begin); + if (length == 0 || length >= sizeof(segment)) { + delete first; + return nullptr; + } + std::memcpy(segment, begin, length); + idTypeInfoPath* const part = new (std::nothrow) idTypeInfoPath(segment); + if (part == nullptr) { + delete first; + return nullptr; + } + if (last == nullptr) first = part; + else last->next = part; + last = part; + if (*cursor == '.') ++cursor; + } + return first; +} + +idTypeInfoNode* idTypeInfoTree::FindPath(idTypeInfoNode* baseNode, + const idTypeInfoPath* path) { + if (baseNode == nullptr || path == nullptr) return nullptr; + if (std::strcmp(baseNode->name.c_str(), path->name.c_str()) == 0) { + path = path->next; + if (path == nullptr) return baseNode; + } + idTypeInfoNode* children = baseNode->children; + while (path != nullptr) { + while (children != nullptr + && std::strcmp(children->name.c_str(), path->name.c_str()) != 0) { + children = children->next; + } + if (children == nullptr) return nullptr; + path = path->next; + if (path == nullptr) return children; + children = children->children; + } + return nullptr; +} + +idTypeInfoNode* idTypeInfoTree::FindPath(idTypeInfoNode* startNode, + const char* path) const { + idTypeInfoPath* const parsed = ParsePath(path); + idTypeInfoNode* const result = FindPath(startNode, parsed); + delete parsed; + return result; +} + +idTypeInfoNode* idTypeInfoTree::CreatePath(const char* path, + const idTypeInfoTools*) { + if (root == nullptr) return nullptr; + idTypeInfoPath* parsed = ParsePath(path); + if (parsed == nullptr) return nullptr; + idTypeInfoPath* part = parsed; + idTypeInfoNode* current = root; + if (std::strcmp(current->name.c_str(), part->name.c_str()) == 0) part = part->next; + while (part != nullptr) { + idTypeInfoNode* child = current->children; + while (child != nullptr + && std::strcmp(child->name.c_str(), part->name.c_str()) != 0) { + child = child->next; + } + if (child == nullptr) { + child = InsertNode("", "", part->name.c_str(), "", "", current); + if (child == nullptr) break; + } + current = child; + part = part->next; + } + delete parsed; + return part == nullptr ? current : nullptr; +} + +void idTypeInfoTree::FreeTree_r(idTypeInfoNode* node) { + while (node != nullptr) { + idTypeInfoNode* const next = node->next; + FreeTree_r(node->children); + delete node; + --nodeBlockAlloc.active; + node = next; + } +} + +void idTypeInfoTree::Clear() { + FreeTree_r(root); + root = nullptr; + nodeBlockAlloc.total = 0; + nodeBlockAlloc.active = 0; +} + +void idTypeInfoTree::GetPath(const idTypeInfoNode* baseNode, + const idTypeInfoNode* node, idStr& path) const { + path.Clear(); + if (baseNode == nullptr || node == nullptr) return; + const idTypeInfoNode* chain[128] = {}; + int count = 0; + for (const idTypeInfoNode* current = node; + current != nullptr && current != baseNode && count < 128; + current = current->parent) chain[count++] = current; + for (int index = count - 1; index >= 0; --index) { + path.Append(chain[index]->name); + if (index > 0) path.Append('.'); + } +} + +idTypeInfoNode* idTypeInfoTree::FindMatchingLeaf(const idTypeInfoTree& tree, + const idTypeInfoNode* node) { + if (tree.root == nullptr || node == nullptr) return nullptr; + const idTypeInfoNode* chain[128] = {}; + int count = 0; + for (const idTypeInfoNode* current = node; current != nullptr && count < 128; + current = current->parent) chain[count++] = current; + idTypeInfoNode* candidate = tree.root; + if (count == 0 || std::strcmp(candidate->name.c_str(), + chain[count - 1]->name.c_str()) != 0) return nullptr; + for (int index = count - 2; index >= 0; --index) { + candidate = candidate->children; + while (candidate != nullptr && std::strcmp(candidate->name.c_str(), + chain[index]->name.c_str()) != 0) candidate = candidate->next; + if (candidate == nullptr) return nullptr; + } + return candidate; +} + +void idTypeInfoTree::MarkDifferent(idTypeInfoNode* node) { + for (; node != nullptr; node = node->parent) node->diff = true; +} + +void idTypeInfoTree::Diff_r(const idTypeInfoTree& other, idTypeInfoNode* node) { + for (idTypeInfoNode* current = node; current != nullptr; current = current->next) { + current->diff = false; + if (current->children != nullptr) Diff_r(other, current->children); + idTypeInfoNode* const match = FindMatchingLeaf(other, current); + if (match == nullptr || (current->children == nullptr + && std::strcmp(match->value.c_str(), current->value.c_str()) != 0)) { + MarkDifferent(current); + } + } +} + +void idTypeInfoTree::Diff(const idTypeInfoTree& other) { + Diff_r(other, root); +} + +void idTypeInfoTree::ForceRootDifferent(const bool set) { + if (root != nullptr) root->diff = set; +} + +bool idTypeInfoTree::IsSkipped(const char* path) const { + idTypeInfoNode* const node = FindPath(root, path); + return node != nullptr && node->skip; +} + +bool idTypeInfoTree::GetInt(const char* path, int& result) const { + result = 0; + idTypeInfoNode* const node = FindPath(root, path); + if (node == nullptr) return false; + result = std::atoi(node->value.c_str()); + return true; +} + +bool idTypeInfoTree::GetStr(const char* path, idStr& result) const { + result.Clear(); + idTypeInfoNode* const node = FindPath(root, path); + if (node == nullptr) return false; + result = node->value; + return true; +} + +bool idTypeInfoTree::SetStr(const char* path, const char* newValue) { + idTypeInfoNode* node = FindPath(root, path); + if (node == nullptr) node = CreatePath(path); + if (node == nullptr) return false; + node->value = newValue == nullptr ? "" : newValue; + MarkDifferent(node); + return true; +} + +bool idTypeInfoTree::GetBool(const char* path, bool& result) const { + result = false; + idTypeInfoNode* const node = FindPath(root, path); + if (node == nullptr) return false; + result = EqualNoCase(node->value.c_str(), "true") + || std::atoi(node->value.c_str()) == 1; + return true; +} + +bool idTypeInfoTree::GetVec3(const char* path, idVec3& result) const { + result.Zero(); + bool found = false; + for (int index = 0; index < 3; ++index) { + char componentPath[1024] = {}; + std::snprintf(componentPath, sizeof(componentPath), "%s.%c", path, + 'x' + index); + idTypeInfoNode* const node = FindPath(root, componentPath); + if (node != nullptr && node->value.Length() > 0) { + result[index] = static_cast(std::atof(node->value.c_str())); + found = true; + } + } + return found; +} + +bool idTypeInfoTree::SetVec3(const char* path, const idVec3& value) { + bool changed = false; + for (int index = 0; index < 3; ++index) { + char componentPath[1024] = {}; + char text[64] = {}; + std::snprintf(componentPath, sizeof(componentPath), "%s.%c", path, + 'x' + index); + std::snprintf(text, sizeof(text), "%.9g", value[index]); + changed = SetStr(componentPath, text) || changed; + } + return changed; +} + +bool idTypeInfoTree::GetMat3(const char* path, idMat3& result) const { + result = idMat3(1.0f); + bool found = false; + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + char componentPath[1024] = {}; + std::snprintf(componentPath, sizeof(componentPath), + "%s.mat.mat[%d].%c", path, row, 'x' + column); + idTypeInfoNode* const node = FindPath(root, componentPath); + if (node != nullptr && node->value.Length() > 0) { + result[row][column] = static_cast( + std::atof(node->value.c_str())); + found = true; + } + } + } + return found; +} + +bool idTypeInfoTree::SetMat3(const char* path, const idMat3& value) { + bool changed = false; + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + char componentPath[1024] = {}; + char text[64] = {}; + std::snprintf(componentPath, sizeof(componentPath), + "%s.mat.mat[%d].%c", path, row, 'x' + column); + std::snprintf(text, sizeof(text), "%.9g", value[row][column]); + changed = SetStr(componentPath, text) || changed; + } + } + return changed; +} + +bool idTypeInfoTree::GetStrType(const char* path, idStr& result) const { + if (!GetStr(path, result)) return false; + const char* text = result.c_str(); + const int length = result.Length(); + if (length >= 2 && text[0] == '"' && text[length - 1] == '"') { + idStr decoded; + for (int index = 1; index < length - 1; ++index) { + if (text[index] == '\\' && index + 1 < length - 1) ++index; + decoded.Append(text[index]); + } + result = decoded; + } else if (std::strcmp(text, "NULL") == 0) { + result.Clear(); + } + return true; +} + +bool idTypeInfoTree::SetStrType(const char* path, const char* value) { + if (value == nullptr) return SetStr(path, "NULL"); + idStr quoted("\""); + for (const char* cursor = value; *cursor != '\0'; ++cursor) { + if (*cursor == '\\' || *cursor == '"') quoted.Append('\\'); + quoted.Append(*cursor); + } + quoted.Append('"'); + return SetStr(path, quoted.c_str()); +} + +void idTypeInfoTree::GetTypes_r(idTypeInfoNode* node, const char* type, + const char* ops, idTypeInfoNodeList& nodes) const { + for (idTypeInfoNode* current = node; current != nullptr; current = current->next) { + const int prefix = static_cast(std::strlen(type == nullptr ? "" : type)); + if (EqualNoCase(current->type.c_str(), type, prefix) + && EqualNoCase(current->ops.c_str(), ops)) nodes.Append(current); + GetTypes_r(current->children, type, ops, nodes); + } +} + +void idTypeInfoTree::GetTypes(const char* type, const char* ops, + idTypeInfoNodeList& nodes) const { + GetTypes_r(root, type, ops, nodes); +} + +void idTypeInfoTree::RemoveOutOfBoundsMembers_r(idTypeInfoNode* node) { + for (idTypeInfoNode* current = node; current != nullptr; current = current->next) { + int count = -1; + for (idTypeInfoNode* child = current->children; child != nullptr; + child = child->next) { + if (std::strcmp(child->name.c_str(), "num") == 0) { + count = std::atoi(child->value.c_str()); + break; + } + } + if (count >= 0) { + idTypeInfoNode** link = ¤t->children; + while (*link != nullptr) { + idTypeInfoNode* child = *link; + int index = -1; + if (std::strncmp(child->name.c_str(), "item[", 5) == 0) { + index = std::atoi(child->name.c_str() + 5); + } + if (index >= count) { + *link = child->next; + child->next = nullptr; + FreeTree_r(child); + } else { + link = &child->next; + } + } + } + RemoveOutOfBoundsMembers_r(current->children); + } +} + +void idTypeInfoTree::RemoveOutOfBoundsMembers() { + RemoveOutOfBoundsMembers_r(root); +} diff --git a/source/shared/idlib/typeinfo/typeinfotree.h b/source/shared/idlib/typeinfo/typeinfotree.h new file mode 100644 index 0000000..219e996 --- /dev/null +++ b/source/shared/idlib/typeinfo/typeinfotree.h @@ -0,0 +1,155 @@ +#pragma once + +#include "../math/vector.h" +#include "idlib/typeinfo/typeinfoobject.h" + +#include + +class idTypeInfoFile; +struct idTypeInfoSettings; + +struct idTypeInfoNode { + idStr type; + idStr ops; + idStr name; + idStr value; + idStr comment; + idTypeInfoNode* next; + idTypeInfoNode* children; + idTypeInfoNode* parent; + bool skip; + bool diff; +}; + +struct idTypeInfoPath { + explicit idTypeInfoPath(const char* pathName = "") + : name(pathName), next(nullptr) {} + ~idTypeInfoPath() { delete next; } + + idStr name; + idTypeInfoPath* next; +}; + +template +class idTypeInfoNodeList { +public: + explicit idTypeInfoNodeList(int listGranularity = 16) + : list(nullptr), num(0), size(0), + granularity(static_cast(listGranularity)), memTag(5), + listStatic(0) {} + ~idTypeInfoNodeList() { delete[] list; } + + int Append(const type& value) { + if (num == size) { + const int newSize = size == 0 ? granularity : size + granularity; + type* const replacement = new (std::nothrow) type[newSize]; + if (replacement == nullptr) return -1; + for (int index = 0; index < num; ++index) replacement[index] = list[index]; + delete[] list; + list = replacement; + size = newSize; + } + list[num] = value; + return num++; + } + void Clear() { num = 0; } + int Num() const { return num; } + type& operator[](int index) { return list[index]; } + const type& operator[](int index) const { return list[index]; } + +private: + type* list; + int num; + int size; + short granularity; + unsigned char memTag; + unsigned char listStatic; +}; + +class idTypeInfoTree { +public: + idTypeInfoTree(); + idTypeInfoTree(const char* text, int length); + ~idTypeInfoTree(); + + void Clear(); + idTypeInfoNode* SetRoot(const char* type, const char* name, + const char* value = "", const char* ops = "", + const char* comment = ""); + idTypeInfoNode* InsertNode(const char* type, const char* ops, + const char* name, const char* value, const char* comment, + idTypeInfoNode* parentNode, idTypeInfoNode* nodeAfter = nullptr); + idTypeInfoNode* FindAndOverwriteNode(const char* type, const char* ops, + const char* name, const char* value, const char* comment, + idTypeInfoNode* parentNode); + + idTypeInfoNode* FindPath(idTypeInfoNode* startNode, const char* path) const; + idTypeInfoNode* CreatePath(const char* path, + const idTypeInfoTools* tools = nullptr); + void GetPath(const idTypeInfoNode* baseNode, const idTypeInfoNode* node, + idStr& path) const; + + void Diff(const idTypeInfoTree& other); + void Write(idTypeInfoFile& file, bool onlyDiff = false) const; + void Parse(idTypeInfoFile& file); + void ParseOverwrite(idTypeInfoFile& file); + void ForceRootDifferent(bool set); + bool IsSkipped(const char* path) const; + bool GetInt(const char* path, int& value) const; + bool GetStr(const char* path, idStr& value) const; + bool SetStr(const char* path, const char* value); + bool GetBool(const char* path, bool& value) const; + bool GetVec3(const char* path, idVec3& value) const; + bool SetVec3(const char* path, const idVec3& value); + bool GetMat3(const char* path, idMat3& value) const; + bool SetMat3(const char* path, const idMat3& value); + bool GetStrType(const char* path, idStr& value) const; + bool SetStrType(const char* path, const char* value); + void GetTypes(const char* type, const char* ops, + idTypeInfoNodeList& nodes) const; + void RemoveOutOfBoundsMembers(); + + idTypeInfoNode* GetRoot() const { return root; } + +private: + struct recoveredBlockAllocator_t { + void* blocks; + void* free; + int total; + int active; + bool allowAllocs; + bool clearAllocs; + }; + + static idTypeInfoPath* ParsePath(const char* path); + static idTypeInfoNode* FindPath(idTypeInfoNode* baseNode, + const idTypeInfoPath* path); + static bool EqualNoCase(const char* left, const char* right, + int prefixLength = -1); + static void MarkDifferent(idTypeInfoNode* node); + static idTypeInfoNode* FindMatchingLeaf(const idTypeInfoTree& tree, + const idTypeInfoNode* node); + void Diff_r(const idTypeInfoTree& other, idTypeInfoNode* node); + void WriteType_r(idTypeInfoFile& file, idTypeInfoNode* node, + bool onlyDiff) const; + idTypeInfoNode* ReadType_r(idTypeInfoFile& file, + idTypeInfoNode* parentNode, bool overwrite); + void FreeTree_r(idTypeInfoNode* node); + void GetTypes_r(idTypeInfoNode* node, const char* type, const char* ops, + idTypeInfoNodeList& nodes) const; + void RemoveOutOfBoundsMembers_r(idTypeInfoNode* node); + + idTypeInfoNode* root; + recoveredBlockAllocator_t nodeBlockAlloc; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idTypeInfoNode) == 176, + "Recovered idTypeInfoNode ABI changed"); +static_assert(sizeof(idTypeInfoPath) == 36, + "Recovered idTypeInfoPath ABI changed"); +static_assert(sizeof(idTypeInfoNodeList) == 16, + "Recovered type-info node list ABI changed"); +static_assert(sizeof(idTypeInfoTree) == 24, + "Recovered idTypeInfoTree ABI changed"); +#endif diff --git a/source/shared/idlib/varargs.h b/source/shared/idlib/varargs.h new file mode 100644 index 0000000..4e27335 --- /dev/null +++ b/source/shared/idlib/varargs.h @@ -0,0 +1,277 @@ +#pragma once + +#include "idlib/math/vector.h" +#include "idlib/text/str.h" + +#include +#include +#include +#include +#include + +enum argTypes_t { + ARG_BOOL = 1, + ARG_CHAR = 2, + ARG_INTEGER = 3, + ARG_FLOAT = 4, + ARG_VECTOR = 5, + ARG_QUAT = 6, + ARG_ANGLES = 7, + ARG_STRING = 8, + ARG_DECL = 9, + ARG_ENTITY = 10, + ARG_CLASS = 11, + ARG_VECTOR4 = 12, + ARG_ANIMALIAS = 13, + ARG_SPAWNID = 14, + ARG_TYPES_MAX = 15 +}; + +// Compact heterogeneous argument storage recovered from animation, voice and +// FSM call sites. MAX_ARGS is part of the object ABI (2, 4 and 6 occur). +template +class idVarArgs { +public: + idVarArgs() + : numArgs(0), argSize(0), buffSize(0), args(nullptr) { + std::memset(argOffsets, 0, sizeof(argOffsets)); + std::memset(argTypes, 0, sizeof(argTypes)); + std::memset(argExTypes, 0, sizeof(argExTypes)); + } + + idVarArgs(const idVarArgs& other) + : idVarArgs() { + Copy(other); + } + + idVarArgs(idVarArgs&& other) noexcept + : numArgs(other.numArgs), argSize(other.argSize), buffSize(other.buffSize), + args(other.args) { + std::memcpy(argOffsets, other.argOffsets, sizeof(argOffsets)); + std::memcpy(argTypes, other.argTypes, sizeof(argTypes)); + std::memcpy(argExTypes, other.argExTypes, sizeof(argExTypes)); + other.ResetWithoutFree(); + } + + ~idVarArgs() { Free(); } + + idVarArgs& operator=(const idVarArgs& other) { + Copy(other); + return *this; + } + + idVarArgs& operator=(idVarArgs&& other) noexcept { + if (this != &other) { + Free(); + numArgs = other.numArgs; + argSize = other.argSize; + buffSize = other.buffSize; + args = other.args; + std::memcpy(argOffsets, other.argOffsets, sizeof(argOffsets)); + std::memcpy(argTypes, other.argTypes, sizeof(argTypes)); + std::memcpy(argExTypes, other.argExTypes, sizeof(argExTypes)); + other.ResetWithoutFree(); + } + return *this; + } + + void ClearArgs() { + Free(); + std::memset(argOffsets, 0, sizeof(argOffsets)); + std::memset(argTypes, 0, sizeof(argTypes)); + std::memset(argExTypes, 0, sizeof(argExTypes)); + } + + int NumArgs() const { return numArgs; } + int GetArgSize() const { return argSize; } + int GetArgType(const int index) const { + return IsValidIndex(index) ? argTypes[index] : 0; + } + int GetArgExType(const int index) const { + return IsValidIndex(index) ? argExTypes[index] : 0; + } + + void AddArg(const bool value, const std::uint8_t exType = 0) { + const std::uint8_t stored = value ? 1 : 0; + AddRaw(ARG_BOOL, exType, &stored, sizeof(stored)); + } + void AddArg(const char value, const std::uint8_t exType = 0) { + AddRaw(ARG_CHAR, exType, &value, sizeof(value)); + } + void AddArg(const int value, const std::uint8_t exType = 0) { + AddRaw(ARG_INTEGER, exType, &value, sizeof(value)); + } + void AddArg(const float value, const std::uint8_t exType = 0) { + AddRaw(ARG_FLOAT, exType, &value, sizeof(value)); + } + void AddArg(const idVec3& value, const std::uint8_t exType = 0) { + AddRaw(ARG_VECTOR, exType, &value, 12); + } + void AddArg(const idQuat& value, const std::uint8_t exType = 0) { + AddRaw(ARG_QUAT, exType, &value, 16); + } + void AddArg(const idAngles& value, const std::uint8_t exType = 0) { + AddRaw(ARG_ANGLES, exType, &value, 12); + } + void AddArg(const idVec4& value, const std::uint8_t exType = 0) { + AddRaw(ARG_VECTOR4, exType, &value, 16); + } + void AddArg(const char* value, const std::uint8_t exType = 0) { + const char* const text = value == nullptr ? "" : value; + AddRaw(ARG_STRING, exType, text, + static_cast(std::strlen(text) + 1)); + } + + void AddHandleArg(const int value, const std::uint8_t type, + const std::uint8_t exType = 0) { + AddRaw(type, exType, &value, sizeof(value)); + } + + bool GetArg(const int index, bool& value) const { + std::uint8_t stored = 0; + if (!GetRaw(index, ARG_BOOL, &stored, sizeof(stored))) return false; + value = stored != 0; + return true; + } + bool GetArg(const int index, char& value) const { + return GetRaw(index, ARG_CHAR, &value, sizeof(value)); + } + bool GetArg(const int index, int& value) const { + return GetRaw(index, ARG_INTEGER, &value, sizeof(value)); + } + bool GetArg(const int index, float& value) const { + return GetRaw(index, ARG_FLOAT, &value, sizeof(value)); + } + bool GetArg(const int index, idVec3& value) const { + return GetRaw(index, ARG_VECTOR, &value, 12); + } + bool GetArg(const int index, idQuat& value) const { + return GetRaw(index, ARG_QUAT, &value, 16); + } + bool GetArg(const int index, idAngles& value) const { + return GetRaw(index, ARG_ANGLES, &value, 12); + } + bool GetArg(const int index, idVec4& value) const { + return GetRaw(index, ARG_VECTOR4, &value, 16); + } + bool GetArg(const int index, idStr& value) const { + const char* text = nullptr; + if (!GetArg(index, text)) return false; + value = text; + return true; + } + bool GetArg(const int index, const char*& value) const { + if (!IsValidIndex(index) || argTypes[index] != ARG_STRING) return false; + value = reinterpret_cast(args + argOffsets[index]); + return true; + } + bool GetArg(const int index, void*& value) const { + if (!IsValidIndex(index) || (argTypes[index] != ARG_DECL + && argTypes[index] != ARG_ENTITY && argTypes[index] != ARG_CLASS)) { + return false; + } + std::memcpy(&value, args + argOffsets[index], sizeof(value)); + return true; + } + bool GetHandleArg(const int index, int& value, + const std::uint8_t type) const { + return GetRaw(index, type, &value, sizeof(value)); + } + + bool Equal(const idVarArgs& other) const { + return numArgs == other.numArgs && argSize == other.argSize + && std::memcmp(argOffsets, other.argOffsets, sizeof(argOffsets)) == 0 + && std::memcmp(argTypes, other.argTypes, sizeof(argTypes)) == 0 + && std::memcmp(argExTypes, other.argExTypes, sizeof(argExTypes)) == 0 + && (argSize == 0 || std::memcmp(args, other.args, argSize) == 0); + } + + bool operator==(const idVarArgs& other) const { return Equal(other); } + bool operator!=(const idVarArgs& other) const { return !Equal(other); } + +private: + bool IsValidIndex(const int index) const { + return index >= 0 && index < numArgs; + } + + bool Grow(const unsigned int amount) { + if (amount == 0) return true; + const unsigned int needed = static_cast(argSize) + amount; + if (needed > std::numeric_limits::max()) return false; + if (needed <= buffSize) return true; + unsigned int newSize = std::max(needed, + static_cast(buffSize) * 2u); + unsigned char* const replacement = static_cast( + _aligned_malloc(newSize, 16)); + if (replacement == nullptr) return false; + if (args != nullptr && argSize > 0) { + std::memcpy(replacement, args, argSize); + } + _aligned_free(args); + args = replacement; + buffSize = static_cast(newSize); + return true; + } + + bool AddRaw(const std::uint8_t type, const std::uint8_t exType, + const void* source, const unsigned int bytes) { + if (numArgs >= MAX_ARGS || source == nullptr || !Grow(bytes)) return false; + argTypes[numArgs] = type; + argExTypes[numArgs] = exType; + argOffsets[numArgs] = argSize; + std::memcpy(args + argSize, source, bytes); + argSize = static_cast(argSize + bytes); + ++numArgs; + return true; + } + + bool GetRaw(const int index, const std::uint8_t type, + void* destination, const unsigned int bytes) const { + if (!IsValidIndex(index) || argTypes[index] != type + || destination == nullptr + || static_cast(argOffsets[index]) + bytes > argSize) { + return false; + } + std::memcpy(destination, args + argOffsets[index], bytes); + return true; + } + + void Copy(const idVarArgs& other) { + if (this == &other) return; + ClearArgs(); + if (other.buffSize > 0 && !Grow(other.buffSize)) return; + numArgs = other.numArgs; + argSize = other.argSize; + std::memcpy(argOffsets, other.argOffsets, sizeof(argOffsets)); + std::memcpy(argTypes, other.argTypes, sizeof(argTypes)); + std::memcpy(argExTypes, other.argExTypes, sizeof(argExTypes)); + if (argSize > 0) std::memcpy(args, other.args, argSize); + } + + void Free() { + _aligned_free(args); + ResetWithoutFree(); + } + + void ResetWithoutFree() { + numArgs = 0; + argSize = 0; + buffSize = 0; + args = nullptr; + } + + std::uint16_t numArgs; + std::uint16_t argSize; + std::uint16_t buffSize; + std::uint16_t argOffsets[MAX_ARGS]; + std::uint8_t argTypes[MAX_ARGS]; + std::uint8_t argExTypes[MAX_ARGS]; + unsigned char* args; +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idVarArgs<2>) == 20, "Recovered idVarArgs<2> ABI changed"); +static_assert(sizeof(idVarArgs<4>) == 28, "Recovered idVarArgs<4> ABI changed"); +static_assert(sizeof(idVarArgs<6>) == 36, "Recovered idVarArgs<6> ABI changed"); +#endif + diff --git a/source/shared/idlib/xml/xmlattribute.h b/source/shared/idlib/xml/xmlattribute.h new file mode 100644 index 0000000..d5b703b --- /dev/null +++ b/source/shared/idlib/xml/xmlattribute.h @@ -0,0 +1,30 @@ +#pragma once + +#include "../text/str.h" + +class idXMLAttribute { +public: + idXMLAttribute(const char* attributeName = "", const char* attributeValue = "") + : name(attributeName), value(attributeValue) { + name.TrimWhitespaceRecovered(); + } + + const char* GetName() const { return name.c_str(); } + const char* GetValue() const { return value.c_str(); } + void SetValue(const char* newValue) { value = newValue; } + void FormatEntities() { + name.ReplaceRecovered("&", "&"); + name.ReplaceRecovered("<", "<"); + name.ReplaceRecovered(">", ">"); + value.ReplaceRecovered("&", "&"); + value.ReplaceRecovered("<", "<"); + value.ReplaceRecovered(">", ">"); + } + +private: + idStr name; + idStr value; +}; + +static_assert(sizeof(idXMLAttribute) == 64, + "Recovered idXMLAttribute ABI changed"); diff --git a/source/shared/idlib/xml/xmlelement.cpp b/source/shared/idlib/xml/xmlelement.cpp new file mode 100644 index 0000000..13a1be1 --- /dev/null +++ b/source/shared/idlib/xml/xmlelement.cpp @@ -0,0 +1,71 @@ +#include "xmlelement.h" + +idXMLElement::idXMLElement(const char* const elementName) + : name(elementName), value(), attributes(), children() { +} + +idXMLElement::idXMLElement(const char* const elementName, + const char* const elementValue) + : name(elementName), value(elementValue), attributes(), children() { +} + +idXMLElement::~idXMLElement() { + for (int index = 0; index < children.Num(); ++index) { + idXMLElement* const child = children[index]; + if (child != nullptr) { + child->~idXMLElement(); + std::free(child); + } + } +} + +idXMLAttribute* idXMLElement::AddAttribute(const char* const attributeName, + const char* const attributeValue) { + return attributes.Append(idXMLAttribute(attributeName, attributeValue)); +} + +idXMLElement* idXMLElement::AddChild(const char* const childName, + const char* const childValue) { + idXMLElement* const child = Create(childName, childValue); + if (child == nullptr) { + return nullptr; + } + if (children.Append(child) == nullptr) { + Destroy(child); + return nullptr; + } + return child; +} + +idXMLElement* idXMLElement::Create(const char* const elementName, + const char* const elementValue) { + void* const storage = std::malloc(sizeof(idXMLElement)); + return storage == nullptr ? nullptr + : new (storage) idXMLElement(elementName, elementValue); +} + +void idXMLElement::Destroy(idXMLElement* const element) { + if (element != nullptr) { + element->~idXMLElement(); + std::free(element); + } +} + +void idXMLElement::FormatStrings() { + FormatStrings_R(this); +} + +void idXMLElement::FormatStrings_R(idXMLElement* const element) { + for (int index = 0; index < element->children.Num(); ++index) { + FormatStrings_R(element->children[index]); + } + element->name.ReplaceRecovered("&", "&"); + element->name.ReplaceRecovered("<", "<"); + element->name.ReplaceRecovered(">", ">"); + element->value.ReplaceRecovered("&", "&"); + element->value.ReplaceRecovered("<", "<"); + element->value.ReplaceRecovered(">", ">"); + for (int index = 0; index < element->attributes.Num(); ++index) { + element->attributes[index].FormatEntities(); + } +} diff --git a/source/shared/idlib/xml/xmlelement.h b/source/shared/idlib/xml/xmlelement.h new file mode 100644 index 0000000..f0fcfad --- /dev/null +++ b/source/shared/idlib/xml/xmlelement.h @@ -0,0 +1,129 @@ +#pragma once + +#include "xmlattribute.h" + +#include +#include +#include + +template +class idRecoveredList { +public: + explicit idRecoveredList(const std::uint8_t tag = 44, + const std::int16_t initialGranularity = 16) + : list(nullptr), num(0), size(0), granularity(initialGranularity), + memTag(tag), listStatic(0) { + } + + ~idRecoveredList() { + Clear(); + } + + idRecoveredList(const idRecoveredList&) = delete; + idRecoveredList& operator=(const idRecoveredList&) = delete; + + int Num() const { return num; } + type& operator[](const int index) { return list[index]; } + const type& operator[](const int index) const { return list[index]; } + + type* Append(const type& value) { + if (num == size && !Grow()) { + return nullptr; + } + list[num] = value; + return &list[num++]; + } + + void Clear() { + if (list != nullptr) { + for (int index = 0; index < size; ++index) { + list[index].~type(); + } + std::free(list); + } + list = nullptr; + num = 0; + size = 0; + } + +private: + type* list; + int num; + int size; + std::int16_t granularity; + std::uint8_t memTag; + std::uint8_t listStatic; + + bool Grow() { + const int amount = granularity > 0 ? granularity : 16; + const int newSize = size + amount; + type* const replacement = static_cast( + std::malloc(sizeof(type) * static_cast(newSize)) + ); + if (replacement == nullptr) { + return false; + } + for (int index = 0; index < newSize; ++index) { + new (&replacement[index]) type(); + } + for (int index = 0; index < num; ++index) { + replacement[index] = list[index]; + } + if (list != nullptr) { + for (int index = 0; index < size; ++index) { + list[index].~type(); + } + std::free(list); + } + list = replacement; + size = newSize; + return true; + } +}; + +static_assert(sizeof(idRecoveredList) == 16, + "Recovered idList ABI changed"); + +class idXMLElement { +public: + explicit idXMLElement(const char* elementName = ""); + idXMLElement(const char* elementName, const char* elementValue); + ~idXMLElement(); + + idXMLElement(const idXMLElement&) = delete; + idXMLElement& operator=(const idXMLElement&) = delete; + + const char* GetName() const { return name.c_str(); } + const char* GetValue() const { return value.c_str(); } + void SetValue(const char* newValue) { value = newValue; } + + idXMLAttribute* AddAttribute(const char* attributeName, + const char* attributeValue); + idXMLElement* AddChild(const char* childName, const char* childValue = ""); + bool AdoptChild(idXMLElement* child) { return children.Append(child) != nullptr; } + void AppendValue(const char* text) { value.Append(text); } + void AppendValue(char character) { value.Append(character); } + int NumAttributes() const { return attributes.Num(); } + int NumChildren() const { return children.Num(); } + idXMLAttribute& GetAttribute(const int index) { return attributes[index]; } + const idXMLAttribute& GetAttribute(const int index) const { return attributes[index]; } + idXMLElement* GetChild(const int index) { return children[index]; } + const idXMLElement* GetChild(const int index) const { return children[index]; } + + void FormatStrings(); + + static idXMLElement* Create(const char* elementName, + const char* elementValue = ""); + static void Destroy(idXMLElement* element); + +private: + idStr name; + idStr value; + idRecoveredList attributes; + idRecoveredList children; + + void FormatStrings_R(idXMLElement* element); +}; + +static_assert(sizeof(idXMLElement) == 96, + "Recovered idXMLElement ABI changed"); diff --git a/source/shared/idlib/xml/xmlreader.cpp b/source/shared/idlib/xml/xmlreader.cpp new file mode 100644 index 0000000..932c55d --- /dev/null +++ b/source/shared/idlib/xml/xmlreader.cpp @@ -0,0 +1,201 @@ +#include "xmlreader.h" + +#include +#include +#include +#include + +idXMLReader::idXMLReader(const char* const fileName) + : sourceName(fileName), document(nullptr), cursor(nullptr), end(nullptr) { + FILE* file = nullptr; + if (fileName == nullptr || fopen_s(&file, fileName, "rb") != 0 + || file == nullptr) { + return; + } + std::fseek(file, 0, SEEK_END); + const long length = std::ftell(file); + std::fseek(file, 0, SEEK_SET); + if (length >= 0) { + document = static_cast(std::malloc(static_cast(length) + 1)); + if (document != nullptr) { + const std::size_t read = std::fread(document, 1, + static_cast(length), file); + document[read] = '\0'; + cursor = document; + end = document + read; + } + } + std::fclose(file); +} + +idXMLReader::~idXMLReader() { + std::free(document); +} + +void idXMLReader::SkipWhitespace() { + while (cursor < end + && std::isspace(static_cast(*cursor)) != 0) { + ++cursor; + } +} + +bool idXMLReader::Consume(const char character) { + if (cursor < end && *cursor == character) { + ++cursor; + return true; + } + return false; +} + +bool idXMLReader::Consume(const char* const text) { + const std::size_t length = std::strlen(text); + if (static_cast(end - cursor) >= length + && std::memcmp(cursor, text, length) == 0) { + cursor += length; + return true; + } + return false; +} + +bool idXMLReader::SkipMisc() { + SkipWhitespace(); + if (Consume(""); + if (close == nullptr || close > end) return false; + cursor = close + 2; + return true; + } + if (Consume(""); + if (close == nullptr || close > end) return false; + cursor = close + 3; + return true; + } + return false; +} + +idStr idXMLReader::ReadName() { + idStr result; + while (cursor < end) { + const unsigned char character = static_cast(*cursor); + if (std::isalnum(character) == 0 && character != '_' + && character != '-' && character != ':' && character != '.' + && character != '&' && character != ';' && character != '#') { + break; + } + result.Append(*cursor++); + } + return result; +} + +idStr idXMLReader::ReadQuotedValue() { + idStr result; + if (cursor >= end || (*cursor != '"' && *cursor != '\'')) { + return result; + } + const char quote = *cursor++; + while (cursor < end && *cursor != quote) { + result.Append(*cursor++); + } + Consume(quote); + return result; +} + +idXMLElement* idXMLReader::ReadElement_R() { + SkipWhitespace(); + while (SkipMisc()) { + SkipWhitespace(); + } + if (!Consume('<') || (cursor < end && *cursor == '/')) { + return nullptr; + } + const idStr name = ReadName(); + if (name.Length() == 0) { + return nullptr; + } + idXMLElement* const element = idXMLElement::Create(name.c_str()); + if (element == nullptr) { + return nullptr; + } + + for (;;) { + SkipWhitespace(); + if (Consume("/>")) { + return element; + } + if (Consume('>')) { + break; + } + const idStr attributeName = ReadName(); + SkipWhitespace(); + if (attributeName.Length() == 0 || !Consume('=')) { + idXMLElement::Destroy(element); + return nullptr; + } + SkipWhitespace(); + const idStr attributeValue = ReadQuotedValue(); + if (element->AddAttribute(attributeName.c_str(), attributeValue.c_str()) == nullptr) { + idXMLElement::Destroy(element); + return nullptr; + } + } + + for (;;) { + if (cursor >= end) { + idXMLElement::Destroy(element); + return nullptr; + } + if (Consume(""); + if (close == nullptr || close > end) { + idXMLElement::Destroy(element); + return nullptr; + } + cursor = close + 3; + continue; + } + if (Consume(""); + if (close == nullptr || close > end) { + idXMLElement::Destroy(element); + return nullptr; + } + while (cursor < close) element->AppendValue(*cursor++); + cursor = close + 3; + continue; + } + if (Consume("') || std::strcmp(name.c_str(), closeName.c_str()) != 0) { + idXMLElement::Destroy(element); + return nullptr; + } + return element; + } + if (*cursor == '<') { + idXMLElement* const child = ReadElement_R(); + if (child == nullptr || !element->AdoptChild(child)) { + idXMLElement::Destroy(child); + idXMLElement::Destroy(element); + return nullptr; + } + } else { + element->AppendValue(*cursor++); + } + } +} + +idXMLElement* idXMLReader::ReadDocument() { + if (document == nullptr) { + return nullptr; + } + cursor = document; + while (SkipMisc()) { + } + idXMLElement* const root = ReadElement_R(); + if (root != nullptr) { + root->FormatStrings(); + } + return root; +} diff --git a/source/shared/idlib/xml/xmlreader.h b/source/shared/idlib/xml/xmlreader.h new file mode 100644 index 0000000..0cc4352 --- /dev/null +++ b/source/shared/idlib/xml/xmlreader.h @@ -0,0 +1,26 @@ +#pragma once + +#include "xmlelement.h" + +class idXMLReader { +public: + explicit idXMLReader(const char* fileName); + ~idXMLReader(); + + idXMLElement* ReadDocument(); + bool IsLoaded() const { return document != nullptr; } + +private: + idStr sourceName; + char* document; + const char* cursor; + const char* end; + + void SkipWhitespace(); + bool SkipMisc(); + bool Consume(char character); + bool Consume(const char* text); + idStr ReadName(); + idStr ReadQuotedValue(); + idXMLElement* ReadElement_R(); +}; diff --git a/source/shared/idlib/xml/xmlwriter.cpp b/source/shared/idlib/xml/xmlwriter.cpp new file mode 100644 index 0000000..429bba0 --- /dev/null +++ b/source/shared/idlib/xml/xmlwriter.cpp @@ -0,0 +1,218 @@ +#include "xmlwriter.h" + +#include +#include +#include + +idXMLWriter::idXMLWriter() + : xmlFile(nullptr), tabLevel(0), hasRoot(false), + firstOpenTag(nullptr), lastOpenTag(nullptr) { +} + +idXMLWriter::~idXMLWriter() { + CloseFile(); +} + +bool idXMLWriter::OpenFile(const char* const fileName, const bool append, + const bool writeDeclaration) { + if (xmlFile != nullptr || fileName == nullptr) { + return false; + } + if (fopen_s(&xmlFile, fileName, append ? "ab" : "wb") != 0 + || xmlFile == nullptr) { + xmlFile = nullptr; + return false; + } + tabLevel = 0; + hasRoot = append; + if (!append && writeDeclaration) { + std::fputs("\n", xmlFile); + } + return true; +} + +bool idXMLWriter::CloseFile() { + if (xmlFile == nullptr) { + return true; + } + while (firstOpenTag != nullptr) { + CloseElement(); + } + const bool success = std::fclose(xmlFile) == 0; + xmlFile = nullptr; + tabLevel = 0; + hasRoot = false; + return success; +} + +bool idXMLWriter::TestRoot(const char*) { + if (xmlFile == nullptr) { + return false; + } + if (tabLevel == 0 && hasRoot) { + return false; + } + if (tabLevel == 0) { + hasRoot = true; + } + return true; +} + +bool idXMLWriter::WriteIndent() { + if (xmlFile == nullptr) { + return false; + } + for (int index = 0; index < tabLevel; ++index) { + if (std::fputs(" ", xmlFile) < 0) { + return false; + } + } + return true; +} + +bool idXMLWriter::WriteEscaped(const char* text) { + if (xmlFile == nullptr) { + return false; + } + const char* cursor = text == nullptr ? "" : text; + while (*cursor != '\0') { + const char* escaped = nullptr; + switch (*cursor) { + case '&': escaped = "&"; break; + case '<': escaped = "<"; break; + case '>': escaped = ">"; break; + case '"': escaped = """; break; + case '\'': escaped = "'"; break; + default: break; + } + if (escaped != nullptr) { + if (std::fputs(escaped, xmlFile) < 0) return false; + } else if (std::fputc(*cursor, xmlFile) == EOF) { + return false; + } + ++cursor; + } + return true; +} + +bool idXMLWriter::WriteAttributes( + const idRecoveredList& attributes) { + for (int index = 0; index < attributes.Num(); ++index) { + if (std::fputc(' ', xmlFile) == EOF + || !WriteEscaped(attributes[index].GetName()) + || std::fputs("=\"", xmlFile) < 0 + || !WriteEscaped(attributes[index].GetValue()) + || std::fputc('"', xmlFile) == EOF) { + return false; + } + } + return true; +} + +bool idXMLWriter::PushTag(const char* const name) { + void* const storage = std::malloc(sizeof(stackXMLTag_t)); + if (storage == nullptr) { + return false; + } + stackXMLTag_t* const tag = new (storage) stackXMLTag_t; + tag->tagName = name; + tag->next = firstOpenTag; + firstOpenTag = tag; + if (lastOpenTag == nullptr) { + lastOpenTag = tag; + } + return true; +} + +bool idXMLWriter::OpenElement(const char* const name) { + idRecoveredList noAttributes; + return OpenElement(name, noAttributes); +} + +bool idXMLWriter::OpenElement(const char* const name, + const idRecoveredList& attributes) { + if (!TestRoot(name) || !WriteIndent() || std::fputc('<', xmlFile) == EOF + || !WriteEscaped(name) || !WriteAttributes(attributes) + || std::fputs(">\n", xmlFile) < 0 || !PushTag(name)) { + return false; + } + ++tabLevel; + return true; +} + +bool idXMLWriter::WriteElement(const char* const name, const char* const value) { + idRecoveredList noAttributes; + return WriteElement(name, value, noAttributes); +} + +bool idXMLWriter::WriteElement(const char* const name, const char* const value, + const idRecoveredList& attributes) { + if (!TestRoot(name) || !WriteIndent() || std::fputc('<', xmlFile) == EOF + || !WriteEscaped(name) || !WriteAttributes(attributes)) { + return false; + } + if (value == nullptr || value[0] == '\0') { + return std::fputs("/>\n", xmlFile) >= 0; + } + return std::fputc('>', xmlFile) != EOF && WriteEscaped(value) + && std::fputs("= 0 && WriteEscaped(name) + && std::fputs(">\n", xmlFile) >= 0; +} + +bool idXMLWriter::CloseElement() { + if (xmlFile == nullptr || firstOpenTag == nullptr) { + return false; + } + stackXMLTag_t* const tag = firstOpenTag; + firstOpenTag = tag->next; + if (firstOpenTag == nullptr) { + lastOpenTag = nullptr; + } + --tabLevel; + const bool success = WriteIndent() && std::fputs("= 0 + && WriteEscaped(tag->tagName.c_str()) && std::fputs(">\n", xmlFile) >= 0; + tag->~stackXMLTag_t(); + std::free(tag); + return success; +} + +bool idXMLWriter::CloseDocument() { + while (firstOpenTag != nullptr) { + if (!CloseElement()) { + return false; + } + } + return CloseFile(); +} + +bool idXMLWriter::WriteElement_R(const idXMLElement* const element) { + if (element == nullptr) { + return false; + } + if (element->NumChildren() == 0) { + idRecoveredList attributes; + for (int index = 0; index < element->NumAttributes(); ++index) { + attributes.Append(element->GetAttribute(index)); + } + return WriteElement(element->GetName(), element->GetValue(), attributes); + } + idRecoveredList attributes; + for (int index = 0; index < element->NumAttributes(); ++index) { + attributes.Append(element->GetAttribute(index)); + } + if (!OpenElement(element->GetName(), attributes)) { + return false; + } + if (element->GetValue()[0] != '\0') { + if (!WriteIndent() || !WriteEscaped(element->GetValue()) + || std::fputc('\n', xmlFile) == EOF) return false; + } + for (int index = 0; index < element->NumChildren(); ++index) { + if (!WriteElement_R(element->GetChild(index))) return false; + } + return CloseElement(); +} + +bool idXMLWriter::WriteDocument(const idXMLElement* const root) { + return xmlFile != nullptr && !hasRoot && WriteElement_R(root); +} diff --git a/source/shared/idlib/xml/xmlwriter.h b/source/shared/idlib/xml/xmlwriter.h new file mode 100644 index 0000000..0a8181b --- /dev/null +++ b/source/shared/idlib/xml/xmlwriter.h @@ -0,0 +1,47 @@ +#pragma once + +#include "xmlelement.h" + +#include + +class idXMLWriter { +public: + idXMLWriter(); + ~idXMLWriter(); + + bool OpenFile(const char* fileName, bool append = false, + bool writeDeclaration = true); + bool CloseFile(); + bool OpenElement(const char* name); + bool OpenElement(const char* name, + const idRecoveredList& attributes); + bool WriteElement(const char* name, const char* value); + bool WriteElement(const char* name, const char* value, + const idRecoveredList& attributes); + bool CloseElement(); + bool CloseDocument(); + bool WriteDocument(const idXMLElement* root); + +private: + struct stackXMLTag_t { + idStr tagName; + stackXMLTag_t* next; + }; + + std::FILE* xmlFile; + int tabLevel; + bool hasRoot; + stackXMLTag_t* firstOpenTag; + stackXMLTag_t* lastOpenTag; + + bool TestRoot(const char* name); + bool WriteIndent(); + bool WriteEscaped(const char* text); + bool WriteAttributes(const idRecoveredList& attributes); + bool PushTag(const char* name); + bool WriteElement_R(const idXMLElement* element); +}; + +#if INTPTR_MAX == INT32_MAX +static_assert(sizeof(idXMLWriter) == 20, "Recovered idXMLWriter ABI changed"); +#endif