diff --git a/.gitignore b/.gitignore index 9f6fc12..6176a70 100644 --- a/.gitignore +++ b/.gitignore @@ -542,7 +542,7 @@ target_wrapper.* # QtCreator CMake CMakeLists.txt.user* -# QtCreator 4.8< compilation database +# QtCreator 4.8< compilation database compile_commands.json # QtCreator local machine specific files for imported projects @@ -584,7 +584,7 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ -.vs\ +.vs\\ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/source2gen/include/sdk/interfaces/common/CUtlVector.h b/source2gen/include/sdk/interfaces/common/CUtlVector.h index d29172f..27834fc 100644 --- a/source2gen/include/sdk/interfaces/common/CUtlVector.h +++ b/source2gen/include/sdk/interfaces/common/CUtlVector.h @@ -1,6 +1,7 @@ // Copyright (C) 2024 neverlosecc // See end of file for extended copyright information. #pragma once +#include #include template diff --git a/source2gen/include/sdk/interfaces/schemasystem/schema.h b/source2gen/include/sdk/interfaces/schemasystem/schema.h index 4faf723..d46a052 100644 --- a/source2gen/include/sdk/interfaces/schemasystem/schema.h +++ b/source2gen/include/sdk/interfaces/schemasystem/schema.h @@ -5,6 +5,7 @@ #include "tools/platform.h" #include #include +#include #include #include #include @@ -382,7 +383,7 @@ class CSchemaType { } // @todo: @og: find out to what class pointer points. - [[nodiscard]] CSchemaType* GetRefClass(); + [[nodiscard]] CSchemaType* GetRefClass() const; [[nodiscard]] ETypeCategory GetTypeCategory() const { #if defined(CS2) || defined(DOTA2) || defined(DEADLOCK) @@ -421,13 +422,13 @@ class CSchemaType_Ptr : public CSchemaType { CSchemaType* m_pObjectType; }; -[[nodiscard]] inline CSchemaType* CSchemaType::GetRefClass() { +[[nodiscard]] inline CSchemaType* CSchemaType::GetRefClass() const { if (GetTypeCategory() != ETypeCategory::Schema_Ptr) return nullptr; - auto ptr = reinterpret_cast(this)->m_pObjectType; + auto ptr = reinterpret_cast(this)->m_pObjectType; while (ptr && ptr->GetTypeCategory() == ETypeCategory::Schema_Ptr) - ptr = reinterpret_cast(ptr)->m_pObjectType; + ptr = reinterpret_cast(ptr)->m_pObjectType; return ptr; } @@ -617,7 +618,7 @@ struct SchemaClassInfoData_t { SchemaClassFieldData_t* m_pFields; // 0x0028 SchemaStaticFieldData_t* m_pStaticFields; // 0x0030 - SchemaBaseClassInfoData_t* m_pBaseClassses; // 0x0038 + SchemaBaseClassInfoData_t* m_pBaseClasses; // 0x0038 SchemaFieldMetadataOverrideSetData_t* m_pFieldMetadataOverrides; // 0x0040 SchemaMetadataEntryData_t* m_pStaticMetadata; // 0x0048 CSchemaSystemTypeScope* m_pTypeScope; // 0x0050 @@ -638,40 +639,41 @@ static_assert(offsetof(SchemaClassInfoData_t, m_pFn) == 0x68); class CSchemaClassInfo : public SchemaClassInfoData_t { public: - [[nodiscard]] std::string_view GetName() { + [[nodiscard]] std::string_view GetName() const { if (m_pszName) return {m_pszName}; return {}; } - [[nodiscard]] std::string_view GetModule() { + [[nodiscard]] std::string_view GetModule() const { if (m_pszModule) return {m_pszModule}; return {}; } [[nodiscard]] std::optional GetBaseClass() const { - if (m_nBaseClassSize && m_pBaseClassses) - return m_pBaseClassses->m_pClass; + if (m_nBaseClassSize && m_pBaseClasses) + return m_pBaseClasses->m_pClass; return std::nullopt; } - [[nodiscard]] std::vector GetFields() { + // TODO: when we have AddressSanitizer, try returning std::span instead of std::vector. Repeats for other functions. + [[nodiscard]] std::vector GetFields() const { return {m_pFields, m_pFields + m_nFieldSize}; } - [[nodiscard]] std::vector GetStaticFields() { + [[nodiscard]] std::vector GetStaticFields() const { return {m_pStaticFields, m_pStaticFields + m_nStaticFieldsSize}; } - [[nodiscard]] std::vector GetStaticMetadata() { + [[nodiscard]] std::vector GetStaticMetadata() const { return {m_pStaticMetadata, m_pStaticMetadata + m_nStaticMetadataSize}; } [[nodiscard]] std::string_view GetPrevClassName() const { - if (!m_pBaseClassses || !m_pBaseClassses->m_pClass) + if (!m_pBaseClasses || !m_pBaseClasses->m_pClass) return {}; - return m_pBaseClassses->m_pClass->GetName(); + return m_pBaseClasses->m_pClass->GetName(); } [[nodiscard]] bool IsA(CSchemaType* pInheritance) const { @@ -686,26 +688,26 @@ class CSchemaClassInfo : public SchemaClassInfoData_t { } [[nodiscard]] bool RecursiveHasVirtualTable() const { - return HasVirtualTable() || (m_pBaseClassses && m_pBaseClassses->m_pClass && m_pBaseClassses->m_pClass->HasVirtualTable()); + return HasVirtualTable() || (m_pBaseClasses && m_pBaseClasses->m_pClass && m_pBaseClasses->m_pClass->HasVirtualTable()); } [[nodiscard]] bool IsInherits(const std::string_view from) const { - if (!m_nBaseClassSize || !m_pBaseClassses || !m_pBaseClassses->m_pClass) + if (!m_nBaseClassSize || !m_pBaseClasses || !m_pBaseClasses->m_pClass) return false; - if (m_pBaseClassses->m_pClass->GetName() == from) + if (m_pBaseClasses->m_pClass->GetName() == from) return true; return false; } [[nodiscard]] bool IsRecursiveInherits(const std::string_view from) const { - return IsInherits(from) || (m_pBaseClassses && m_pBaseClassses->m_pClass && m_pBaseClassses->m_pClass->IsRecursiveInherits(from)); + return IsInherits(from) || (m_pBaseClasses && m_pBaseClasses->m_pClass && m_pBaseClasses->m_pClass->IsRecursiveInherits(from)); } [[nodiscard]] int GetSize() const { return m_nSizeOf; } - [[nodiscard]] std::uint8_t GetAligment() const { + [[nodiscard]] std::uint8_t GetAlignment() const { return m_unAlignOf == std::numeric_limits::max() ? 8 : m_unAlignOf; } @@ -765,36 +767,46 @@ static_assert(sizeof(CSchemaPtrMap) == platform_specific{.windows = 0x class CSchemaSystemTypeScope { public: void* InsertNewClassBinding(const std::string_view szName, void* a2) { + assert((std::strlen(szName.data()) == szName.size()) && "need a zero-terminated string"); + return Virtual::Get(this, 0)(this, szName.data(), a2); } void* InsertNewEnumBinding(const std::string_view szName, void* a2) { + assert((std::strlen(szName.data()) == szName.size()) && "need a zero-terminated string"); + return Virtual::Get(this, 1)(this, szName.data(), a2); } - [[nodiscard]] CSchemaClassInfo* FindDeclaredClass(const std::string_view szName) { + [[nodiscard]] CSchemaClassInfo* FindDeclaredClass(const std::string_view szName) const { + assert((std::strlen(szName.data()) == szName.size()) && "need a zero-terminated string"); + if constexpr (kSchemaSystemVersion == 2) { CSchemaClassInfo* class_info; - Virtual::Get(this, 2)(this, &class_info, szName.data()); + Virtual::Get(this, 2)(this, &class_info, szName.data()); return class_info; } else { - return Virtual::Get(this, 2)(this, szName.data()); + return Virtual::Get(this, 2)(this, szName.data()); } } - [[nodiscard]] CSchemaEnumInfo* FindDeclaredEnum(const std::string_view szName) { + [[nodiscard]] CSchemaEnumInfo* FindDeclaredEnum(const std::string_view szName) const { + assert((std::strlen(szName.data()) == szName.size()) && "need a zero-terminated string"); + if constexpr (kSchemaSystemVersion == 2) { CSchemaEnumInfo* enum_info; - Virtual::Get(this, 3)(this, &enum_info, szName.data()); + Virtual::Get(this, 3)(this, &enum_info, szName.data()); return enum_info; } else { - return Virtual::Get(this, 3)(this, szName.data()); + return Virtual::Get(this, 3)(this, szName.data()); } } [[nodiscard]] CSchemaType* FindSchemaTypeByName(const std::string_view szName) { + assert((std::strlen(szName.data()) == szName.size()) && "need a zero-terminated string"); + if constexpr (kSchemaSystemVersion == 2) { CSchemaType* schema_type; diff --git a/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h b/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h index 306cb36..b6f4a28 100644 --- a/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h +++ b/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h @@ -4,7 +4,10 @@ #pragma warning(push) #pragma warning(disable:4191) + +#include "tools/platform.h" #include "tools/virtual.h" +#include class IMemAlloc { public: diff --git a/source2gen/include/sdk/sdk.h b/source2gen/include/sdk/sdk.h index 8ddb796..f06b92c 100644 --- a/source2gen/include/sdk/sdk.h +++ b/source2gen/include/sdk/sdk.h @@ -23,11 +23,13 @@ #include #include #include +#include namespace sdk { inline CSchemaSystem* g_schema = nullptr; - void GenerateTypeScopeSdk(CSchemaSystemTypeScope* current); + void GenerateTypeScopeSdk(std::string_view module_name, const std::unordered_set& enums, + const std::unordered_set& classes); } // namespace sdk // source2gen - Source2 games SDK generator diff --git a/source2gen/include/tools/codegen.h b/source2gen/include/tools/codegen.h index 5800532..c9627f0 100644 --- a/source2gen/include/tools/codegen.h +++ b/source2gen/include/tools/codegen.h @@ -1,6 +1,7 @@ // Copyright (C) 2024 neverlosecc // See end of file for extended copyright information. #pragma once +#include #include #include #include @@ -124,10 +125,7 @@ namespace codegen { if (decrement_tabs_count) dec_tabs_count(kTabsPerBlock); - push_line("};"); - if (move_cursor_to_next_line) - next_line(); - + push_line("};", move_cursor_to_next_line); return *this; } @@ -183,8 +181,8 @@ namespace codegen { } // @todo: @es3n1n: add func params - self_ref begin_function(const std::string& prefix, const std::string& type_name, const std::string& func_name, const bool increment_tabs_count = true, - const bool move_cursor_to_next_line = true) { + self_ref begin_function(const std::string& prefix, const std::string& type_name, const std::string& func_name, + const bool increment_tabs_count = true, const bool move_cursor_to_next_line = true) { return begin_block(std::format("{}{} {}()", prefix, type_name, escape_name(func_name)), "", increment_tabs_count, move_cursor_to_next_line); } @@ -209,7 +207,7 @@ namespace codegen { R"(*reinterpret_cast<{}*>(interfaces::g_schema->FindTypeScopeForModule("{}")->FindDeclaredClass("{}")->GetStaticFields()[{}]->m_pInstance))", type_name, mod_name, decl_class, index); return_value(getter, false); - end_function(false, false); + end_function(false, true); // @note: @es3n1n: restore tabs count // @@ -293,8 +291,7 @@ namespace codegen { result.resize(name.size()); for (std::size_t i = 0; i < name.size(); i++) - result[i] = - std::ranges::find(kBlacklistedCharacters, name[i]) == std::end(kBlacklistedCharacters) ? name[i] : '_'; + result[i] = std::ranges::find(kBlacklistedCharacters, name[i]) == std::end(kBlacklistedCharacters) ? name[i] : '_'; return result; } diff --git a/source2gen/include/tools/field_parser.h b/source2gen/include/tools/field_parser.h index 9463feb..27cad56 100644 --- a/source2gen/include/tools/field_parser.h +++ b/source2gen/include/tools/field_parser.h @@ -2,10 +2,14 @@ // See end of file for extended copyright information. #pragma once -#include +#include +#include +#include +#include #include - -enum class fieldtype_t : uint8_t; +#include +#include +#include namespace field_parser { class field_info_t { @@ -65,6 +69,10 @@ namespace field_parser { } }; + /// @return @ref std::nullopt if type_name is not a built-in type + [[nodiscard]] + std::optional type_name_to_cpp(std::string_view type_name); + field_info_t parse(const std::string& type_name, const std::string& name, const std::vector& array_sizes); field_info_t parse(const fieldtype_t& type_name, const std::string& name, const std::size_t& array_sizes = 1); } // namespace field_parser diff --git a/source2gen/include/tools/util.h b/source2gen/include/tools/util.h index 0bf3bd5..aa7d322 100644 --- a/source2gen/include/tools/util.h +++ b/source2gen/include/tools/util.h @@ -2,7 +2,9 @@ // See end of file for extended copyright information. #pragma once +#include #include +#include #include namespace util { @@ -19,6 +21,12 @@ namespace util { return std::to_string(num); } + + [[nodiscard]] inline std::string EscapePath(std::string_view path) { + std::string result(path); + std::ranges::replace(result, ':', '_'); + return result; + } } // namespace util // source2gen - Source2 games SDK generator diff --git a/source2gen/include/tools/virtual.h b/source2gen/include/tools/virtual.h index 3109960..76e70ae 100644 --- a/source2gen/include/tools/virtual.h +++ b/source2gen/include/tools/virtual.h @@ -2,12 +2,17 @@ // See end of file for extended copyright information. #pragma once -#include +#include namespace Virtual { template inline T Get(void* instance, const unsigned int index) { - return (*static_cast(instance))[index]; + return (*static_cast(static_cast(instance)))[index]; + } + + template + inline T Get(const void* instance, const unsigned int index) { + return (*static_cast(static_cast(instance)))[index]; } template diff --git a/source2gen/src/sdk/sdk.cpp b/source2gen/src/sdk/sdk.cpp index c9ff89f..29c58f2 100644 --- a/source2gen/src/sdk/sdk.cpp +++ b/source2gen/src/sdk/sdk.cpp @@ -4,15 +4,36 @@ // ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken #include "sdk/sdk.h" #include +#include #include +#include #include +#include #include +#include +#include +#include namespace { + enum class NameSource { + include, + forward_declaration, + }; + + struct NameLookup { + std::string module{}; + + /// Decayed + std::string type_name{}; + + NameSource source{}; + + auto operator<=>(const NameLookup&) const = default; + }; + using namespace std::string_view_literals; constexpr std::string_view kOutDirName = "sdk"sv; - constinit std::array include_paths = {""sv, R"("!GlobalTypes.hpp")"sv}; constexpr uint32_t kMaxReferencesForClassEmbed = 2; constexpr std::size_t kMinFieldCountForClassEmbed = 2; @@ -97,7 +118,7 @@ namespace { // @note: @es3n1n: some more utils // - std::string GetMetadataValue(const SchemaMetadataEntryData_t metadata_entry) { + std::string GetMetadataValue(const SchemaMetadataEntryData_t& metadata_entry) { std::string value; const auto value_hash_name = fnv32::hash_runtime(metadata_entry.m_szName); @@ -139,660 +160,752 @@ namespace { return value; }; -} // namespace -namespace sdk { - namespace { - void PrintClassInfo(codegen::generator_t::self_ref builder, CSchemaClassBinding* class_info) { - builder - .comment(std::format("Registered binary: {} (project '{}')", g_schema->GetClassInfoBinaryName(class_info), - g_schema->GetClassProjectName(class_info))) - .comment(std::format("Alignment: {}", class_info->GetAligment())) - .comment(std::format("Size: {:#x}", class_info->m_nSizeOf)); - - if ((class_info->m_nClassFlags & SCHEMA_CF1_HAS_VIRTUAL_MEMBERS) != 0) // @note: @og: its means that class probably does have vtable - builder.comment("Has VTable"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_IS_ABSTRACT) != 0) - builder.comment("Is Abstract"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_HAS_TRIVIAL_CONSTRUCTOR) != 0) - builder.comment("Has Trivial Constructor"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_HAS_TRIVIAL_DESTRUCTOR) != 0) - builder.comment("Has Trivial Destructor"); + void PrintClassInfo(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_info) { + builder.comment(std::format("Alignment: {}", class_info.GetAlignment())).comment(std::format("Size: {:#x}", class_info.m_nSizeOf)); + + if ((class_info.m_nClassFlags & SCHEMA_CF1_HAS_VIRTUAL_MEMBERS) != 0) // @note: @og: its means that class probably does have vtable + builder.comment("Has VTable"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_IS_ABSTRACT) != 0) + builder.comment("Is Abstract"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_HAS_TRIVIAL_CONSTRUCTOR) != 0) + builder.comment("Has Trivial Constructor"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_HAS_TRIVIAL_DESTRUCTOR) != 0) + builder.comment("Has Trivial Destructor"); #if defined(CS2) || defined(DOTA2) - if ((class_info->m_nClassFlags & SCHEMA_CF1_CONSTRUCT_ALLOWED) != 0) - builder.comment("Construct allowed"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_CONSTRUCT_DISALLOWED) != 0) - builder.comment("Construct disallowed"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MConstructibleClassBase) != 0) - builder.comment("MConstructibleClassBase"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasCustomAlignedNewDelete) != 0) - builder.comment("MClassHasCustomAlignedNewDelete"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasEntityLimitedDataDesc) != 0) - builder.comment("MClassHasEntityLimitedDataDesc"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MDisableDataDescValidation) != 0) - builder.comment("MDisableDataDescValidation"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MIgnoreTypeScopeMetaChecks) != 0) - builder.comment("MIgnoreTypeScopeMetaChecks"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkNoBase) != 0) - builder.comment("MNetworkNoBase"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkAssumeNotNetworkable) != 0) - builder.comment("MNetworkAssumeNotNetworkable"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_ALLOWED) != 0) + builder.comment("Construct allowed"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_DISALLOWED) != 0) + builder.comment("Construct disallowed"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MConstructibleClassBase) != 0) + builder.comment("MConstructibleClassBase"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasCustomAlignedNewDelete) != 0) + builder.comment("MClassHasCustomAlignedNewDelete"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasEntityLimitedDataDesc) != 0) + builder.comment("MClassHasEntityLimitedDataDesc"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MDisableDataDescValidation) != 0) + builder.comment("MDisableDataDescValidation"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MIgnoreTypeScopeMetaChecks) != 0) + builder.comment("MIgnoreTypeScopeMetaChecks"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkNoBase) != 0) + builder.comment("MNetworkNoBase"); + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkAssumeNotNetworkable) != 0) + builder.comment("MNetworkAssumeNotNetworkable"); #endif - if (class_info->m_nStaticMetadataSize > 0) - builder.comment(""); + if (class_info.m_nStaticMetadataSize > 0) + builder.comment(""); + + for (const auto& metadata : class_info.GetStaticMetadata()) { + if (const auto value = GetMetadataValue(metadata); !value.empty()) + builder.comment(std::format("{} \"{}\"", metadata.m_szName, value)); + else + builder.comment(metadata.m_szName); + } + } + + void PrintEnumInfo(codegen::generator_t::self_ref builder, const CSchemaEnumBinding& enum_binding) { + builder.comment(std::format("Enumerator count: {}", enum_binding.m_nEnumeratorCount)) + .comment(std::format("Alignment: {}", enum_binding.m_unAlignOf)) + .comment(std::format("Size: {:#x}", enum_binding.m_unSizeOf)); + + if (enum_binding.m_nStaticMetadataSize > 0) + builder.comment(""); + + for (const auto& metadata : enum_binding.GetStaticMetadata()) { + builder.comment(metadata.m_szName); + } + } + + void AssembleEnum(codegen::generator_t::self_ref builder, const CSchemaEnumBinding& schema_enum_binding) { + // @note: @es3n1n: get type name by align size + // + const auto get_type_name = [&schema_enum_binding]() -> std::string { + std::string type_storage; + + switch (schema_enum_binding.m_unAlignOf) { + case 1: + type_storage = "std::uint8_t"; + break; + case 2: + type_storage = "std::uint16_t"; + break; + case 4: + type_storage = "std::uint32_t"; + break; + case 8: + type_storage = "std::uint64_t"; + break; + default: + type_storage = "INVALID_TYPE"; + } + + return type_storage; + }; + + // @note: @es3n1n: print meta info + // + PrintEnumInfo(builder, schema_enum_binding); + + // @note: @es3n1n: begin enum class + // + builder.begin_enum_class(schema_enum_binding.m_pszName, get_type_name()); - for (const auto& metadata : class_info->GetStaticMetadata()) { - if (const auto value = GetMetadataValue(metadata); !value.empty()) - builder.comment(std::format("{} \"{}\"", metadata.m_szName, value)); + // @note: @og: build max based on numeric_limits of unAlignOf + // + const auto print_enum_item = [schema_enum_binding, &builder](const SchemaEnumeratorInfoData_t& field) { + switch (schema_enum_binding.m_unAlignOf) { + case 1: + builder.enum_item(field.m_szName, field.m_uint8); + break; + case 2: + builder.enum_item(field.m_szName, field.m_uint16); + break; + case 4: + builder.enum_item(field.m_szName, field.m_uint32); + break; + case 8: + builder.enum_item(field.m_szName, field.m_uint64); + break; + default: + builder.enum_item(field.m_szName, field.m_uint64); + } + }; + + // @note: @es3n1n: assemble enum items + // + for (const auto& field : schema_enum_binding.GetEnumeratorValues()) { + // @note: @og: dump enum metadata + // + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; + + if (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(field_metadata.m_szName); else - builder.comment(metadata.m_szName); + builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); } + + print_enum_item(field); } - void PrintEnumInfo(codegen::generator_t::self_ref builder, CSchemaEnumBinding* enum_binding) { - builder - .comment(std::format("Registered binary: {} (project '{}')", g_schema->GetEnumBinaryName(enum_binding), - g_schema->GetEnumProjectName(enum_binding))) - .comment(std::format("Enumerator count: {}", enum_binding->m_nEnumeratorCount)) - .comment(std::format("Alignment: {}", enum_binding->m_unAlignOf)) - .comment(std::format("Size: {:#x}", enum_binding->m_unSizeOf)); + // @note: @es3n1n: we are done with this enum + // + builder.end_enum_class(); + } - if (enum_binding->m_nStaticMetadataSize > 0) - builder.comment(""); + /// @return {type_name, array_sizes} + std::pair> ParseArray(const CSchemaType& type) { + const auto* ptr = type.GetRefClass(); + const auto& actual_type = ptr ? *ptr : type; - for (const auto& metadata : enum_binding->GetStaticMetadata()) { - builder.comment(metadata.m_szName); + std::string base_type; + std::vector sizes; + + if (actual_type.GetTypeCategory() == ETypeCategory::Schema_FixedArray) { + // dump all sizes. + auto* schema = reinterpret_cast(&actual_type); + while (true) { + sizes.emplace_back(schema->m_nElementCount); + schema = reinterpret_cast(schema->m_pElementType); + + if (schema->GetTypeCategory() != ETypeCategory::Schema_FixedArray) { + base_type = schema->m_pszName; + break; + } } } - void AssembleEnums(codegen::generator_t::self_ref builder, CUtlTSHash enums) { - for (auto schema_enum_binding : enums.GetElements()) { - // @note: @es3n1n: get type name by align size - // - const auto get_type_name = [schema_enum_binding]() -> std::string { - std::string type_storage; + return {base_type, sizes}; + }; - switch (schema_enum_binding->m_unAlignOf) { - case 1: - type_storage = "std::uint8_t"; - break; - case 2: - type_storage = "std::uint16_t"; - break; - case 4: - type_storage = "std::uint32_t"; - break; - case 8: - type_storage = "std::uint64_t"; - break; - default: - type_storage = "INVALID_TYPE"; - } + /// @return Lifetime is bound to string viewed by @p type_name + [[nodiscard]] + std::string_view DecayTypeName(std::string_view type_name) { + if (const auto found = type_name.find('['); found != std::string_view::npos) { + // "array[123]" -> "array" + type_name = type_name.substr(0, found); + } + if (const auto found = type_name.find('*'); found != std::string_view::npos) { + // "pointer***" -> "pointer" + type_name = type_name.substr(0, found); + } - return type_storage; - }; + return type_name; + } - // @note: @es3n1n: print meta info - // - PrintEnumInfo(builder, schema_enum_binding); + /// @return @ref std::nullopt if the type is not contained in a module visible in @p scope + std::optional GetModuleOfTypeInScope(const CSchemaSystemTypeScope& scope, std::string_view type_name) { + assert((DecayTypeName(type_name) == type_name) && + "you need to decay your type names before using them for lookups. you probably need to decay them anyway if you intend to you them for " + "anything really, so do it before calling this function."); + + if (const auto* class_ = scope.FindDeclaredClass(std::string{type_name})) { + return class_->m_pszModule; + } else if (const auto* enum_ = scope.FindDeclaredEnum(std::string{type_name})) { + return enum_->m_pszModule; + } else { + return std::nullopt; + } + } - // @note: @es3n1n: begin enum class - // - builder.begin_enum_class(schema_enum_binding->m_pszName, get_type_name()); + /// @return @ref std::nullopt if the type is not contained in a module visible in its scope, e.g. because it is a built-in type + std::optional GetModuleOfType(const CSchemaType& type) { + if (type.m_pTypeScope != nullptr) { + return GetModuleOfTypeInScope(*type.m_pTypeScope, DecayTypeName(type.m_pszName)); + } else { + return std::nullopt; + } + }; - // @note: @og: build max based on numeric_limits of unAlignOf - // - const auto print_enum_item = [schema_enum_binding, &builder](const SchemaEnumeratorInfoData_t& field) { - switch (schema_enum_binding->m_unAlignOf) { - case 1: - builder.enum_item(field.m_szName, field.m_uint8); - break; - case 2: - builder.enum_item(field.m_szName, field.m_uint16); - break; - case 4: - builder.enum_item(field.m_szName, field.m_uint32); - break; - case 8: - builder.enum_item(field.m_szName, field.m_uint64); - break; - default: - builder.enum_item(field.m_szName, field.m_uint64); - } - }; - - // @note: @es3n1n: assemble enum items - // - for (const auto& field : schema_enum_binding->GetEnumeratorValues()) { - // @note: @og: dump enum metadata - // - for (auto j = 0; j < field.m_nMetadataSize; j++) { - auto field_metadata = field.m_pMetadata[j]; - - if (auto data = GetMetadataValue(field_metadata); data.empty()) - builder.comment(field_metadata.m_szName); - else - builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); - } + [[nodiscard]] std::string EscapeTypeName(const std::string_view type_name) { + // TODO: when we have a package manager: use a library + const auto StringReplace = [](std::string str, std::string_view search, std::string_view replace) { + const std::size_t pos = str.find(search); + if (pos != std::string::npos) { + str.replace(pos, search.length(), replace); + } + return str; + }; + + // This is a hack to support nested types. + // When we define nested types, they're not actually nested, but contain their outer class' name in their name, + // e.g. "struct Player { struct Hand {}; };" is emitted as + // "struct Player {}; struct Player__Hand{};". + // But when used as a property, types expect `Hand` in `Player`, i.e. `Player::Hand m_hand;` + // Instead of doing this hackery, we should probably declare nested classes as nested classes. + return StringReplace(std::string{type_name}, "::", "__"); + } - print_enum_item(field); - } + /// Adds the module specifier to @p type_name, if @p type_name is declared in @p scope. Otherwise returns @p type_name unmodified. + std::string MaybeWithModuleName(const CSchemaSystemTypeScope& scope, const std::string_view type_name) { + const auto escaped_type_name = EscapeTypeName(type_name); + return GetModuleOfTypeInScope(scope, type_name) + .transform([&](const auto module_name) { return std::format("{}::{}", module_name, escaped_type_name); }) + .value_or(escaped_type_name); + }; - // @note: @es3n1n: we are done with this enum - // - builder.end_enum_class(); + /// Decomposes a templated type into its components, keeping template + /// syntax for later reassembly by @ref ReassembleRetypedTemplate(). + /// e.g. "HashMap>" -> ["HashMap", '<', "int", ',', "Vector", '<', "float", '>', '>'] + /// @return std::string for types, char for syntax (',', '<', '>'). Spaces are removed. + [[nodiscard]] + std::vector> DecomposeTemplate(std::string_view type_name) { + // TODO: use a library for this once we have a package manager + const auto trim = [](std::string_view str) { + if (const auto found = str.find_first_not_of(" "); found != std::string_view::npos) { + str.remove_prefix(found); + } else { + return std::string_view{}; } - } - void AssembleClasses(CSchemaSystemTypeScope* current, codegen::generator_t::self_ref builder, CUtlTSHash classes) { - struct class_t { - CSchemaClassInfo* target_{}; - std::set refs_; - uint32_t used_count_{}; - std::list> cached_fields_; - - struct cached_datamap_t { - std::string type_; - std::string name_; - ptrdiff_t offset_; - }; - std::list cached_datamap_fields_; - - [[nodiscard]] CSchemaClassInfo* GetParent() const { - if (!target_->m_pBaseClassses) - return nullptr; - - return target_->m_pBaseClassses->m_pClass; - } + if (const auto found = str.find_last_not_of(" "); found != std::string_view::npos) { + str.remove_suffix(str.size() - (found + 1)); + } - void AddRefToClass(CSchemaType* type) { - if (type->GetTypeCategory() == ETypeCategory::Schema_DeclaredClass) { - refs_.insert(reinterpret_cast(type)->m_pClassInfo); - } + return str; + }; + + /// Preserves separators in output. Removes space. + const auto split_trim = [trim](std::string_view str, std::string_view separators) -> std::vector> { + std::vector> result{}; + std::string_view remainder = str; - // auto ptr = type->GetRefClass(); - // if (ptr && ptr->m_nTypeCategory == Schema_DeclaredClass) - // { - // refs_.insert(ptr->m_pClassInfo); - // return; - // } + while (true) { + if (const auto found = remainder.find_first_of(separators); found != std::string_view::npos) { + if (const auto part = trim(remainder.substr(0, found)); !part.empty()) { + result.emplace_back(std::string{part}); + } + result.emplace_back(remainder[found]); + remainder.remove_prefix(found + 1); + } else { + if (const auto part = trim(remainder); !part.empty()) { + result.emplace_back(std::string{part}); + } + break; } + } - [[nodiscard]] bool IsDependsOn(const class_t& other) const { - // if current class inherit other. - const auto parent = this->GetParent(); - if (parent == other.target_) - return true; + return result; + }; - // if current class contains ref to other. - if (this->refs_.contains(other.target_)) - return true; + return split_trim(type_name, "<,>"); + } - // otherwise, order doesn`t matter. - return false; - } + /// e.g. "HashMap>" -> ["HashMap", "int", "CUtlVector", "float"] + /// @return An empty list if @p type_name is not a template or has no template parameters + [[nodiscard]] + std::vector ParseTemplateRecursive(std::string_view type_name) { + std::vector result{}; - [[nodiscard]] SchemaClassFieldData_t* GetFirstField() const { - if (target_->m_nFieldSize) - return &target_->m_pFields[0]; - return nullptr; - } + // remove the topmost type and all syntax entries + for (const auto& el : DecomposeTemplate(type_name)) { + if (std::holds_alternative(el)) { + result.emplace_back(std::move(std::get(el))); + } + } - // @note: @es3n1n: Returns the struct size without its parent's size - // - [[nodiscard]] std::ptrdiff_t ClassSizeWithoutParent() const { - if (const CSchemaClassInfo* class_parent = this->GetParent(); class_parent) - return this->target_->m_nSizeOf - class_parent->m_nSizeOf; - return this->target_->m_nSizeOf; - } - }; + return result; + } + + /// Adds module qualifiers and resolves built-in types + std::string ReassembleRetypedTemplate(const CSchemaSystemTypeScope& scope, const std::vector>& decomposed) { + std::string result{}; + + for (const auto& el : decomposed) { + std::visit( + [&](const auto& e) { + if constexpr (std::is_same_v, char>) { + result += e; + } else { + if (const auto built_in = field_parser::type_name_to_cpp(e)) { + result += built_in.value(); + } else { + // e is a dirty name, e.g. "CPlayer*[10]". We need to add the module, but keep it dirty. + const auto type_name = DecayTypeName(e); + const auto type_name_with_module = MaybeWithModuleName(scope, type_name); + const auto dirty_type_name_with_module = std::string{e}.replace(e.find(type_name), type_name.length(), type_name_with_module); + result += dirty_type_name_with_module; + } + } + }, + el); + } - // @note: @soufiw: - // sort all classes based on refs and inherit, and then print it. - // ================== - std::list classes_to_dump; - bool did_forward_decls = false; + return result; + } + /// @return {type_name, array_sizes} where type_name is a fully qualified name + std::pair> GetType(const CSchemaType& type) { + const auto [type_name, array_sizes] = ParseArray(type); - for (const auto* schema_class_binding : classes.GetElements()) { - assert(schema_class_binding != nullptr); + assert(type_name.empty() == array_sizes.empty()); - const auto class_info = current->FindDeclaredClass(schema_class_binding->m_pszName); + const auto type_name_with_modules = + ReassembleRetypedTemplate(*type.m_pTypeScope, DecomposeTemplate(type_name.empty() ? type.m_pszName : type_name)); - auto& class_dump = classes_to_dump.emplace_back(); - class_dump.target_ = class_info; - } + if (!type_name.empty() && !array_sizes.empty()) + return {type_name_with_modules, array_sizes}; - for (auto& class_dump : classes_to_dump) { - const auto class_info = class_dump.target_; + return {type_name_with_modules, {}}; + }; - for (auto k = 0; k < class_info->m_nFieldSize; k++) { - const auto field = &class_info->m_pFields[k]; - if (!field) - continue; + // We assume that everything that is not a pointer is odr-used. + // This assumption not correct, e.g. template classes that internally store pointers are + // not always odr-users of a type. It's good enough for what we do though. + [[nodiscard]] + constexpr auto IsOdrUse(std::string_view type_name) { + return !type_name.contains('*'); + } - // forward declare all classes. - // @todo: maybe we need to forward declare only pointers to classes? - auto ptr = field->m_pSchemaType->GetRefClass(); + /// @return All names used by @p type. Returns multiple names for template + /// types. + [[nodiscard]] + std::set GetRequiredNamesForType(const CSchemaType& type) { + // m_pTypeScope can be nullptr for built-in types + if (type.m_pTypeScope != nullptr) { + std::set result{}; - if (auto actual_type = ptr ? ptr : field->m_pSchemaType; actual_type->GetTypeCategory() == ETypeCategory::Schema_DeclaredClass) { - builder.forward_declaration(actual_type->m_pszName); - did_forward_decls = true; - } + const auto destructured = ParseTemplateRecursive(type.m_pszName); + + // This is a slight hack to break dependency cycles. Some template types don't odr-use + // their template arguments during their declaration, think std::unique_ptr. + // There's no foolproof way to detect those template types, so we're hardcoding the ones that are + // known to cause circular dependencies. + static std::unordered_set non_odr_containers{"CHandle"}; + bool is_used_in_non_odr_container = false; - class_dump.AddRefToClass(field->m_pSchemaType); + for (const auto& dirty_type_name : destructured) { + const auto type_name = DecayTypeName(dirty_type_name); - auto field_class = std::ranges::find_if(classes_to_dump, [field](const class_t& cls) { - return cls.target_ == reinterpret_cast(field->m_pSchemaType)->m_pClassInfo; - }); - if (field_class != classes_to_dump.end()) - field_class->used_count_++; + if (auto module{GetModuleOfTypeInScope(*type.m_pTypeScope, type_name)}) { + const auto source = + (!is_used_in_non_odr_container && IsOdrUse(dirty_type_name)) ? NameSource::include : NameSource::forward_declaration; + + result.emplace(NameLookup{.module = std::move(module.value()), .type_name = std::string{type_name}, .source = source}); } + + is_used_in_non_odr_container = non_odr_containers.contains(type_name); } - if (did_forward_decls) - builder.next_line(); + return result; + } else { + return {}; + } + } - bool did_change = false; - do { - did_change = false; + /// @return All names that are required to define @p classes + std::set GetRequiredNamesForClass(const CSchemaClassBinding& class_) { + std::set result{}; - // swap until we done. - for (auto first = classes_to_dump.begin(); first != classes_to_dump.end(); ++first) { - bool second_below_first = false; + for (const auto& field : std::span{class_.m_pFields, static_cast(class_.m_nFieldSize)}) { + const auto names = GetRequiredNamesForType(*field.m_pSchemaType); + result.insert(names.begin(), names.end()); + } - for (auto second = classes_to_dump.begin(); second != classes_to_dump.end(); ++second) { - if (second == first) { - second_below_first = true; - continue; - } + for (const auto& field : std::span{class_.m_pStaticFields, static_cast(class_.m_nStaticFieldsSize)}) { + const auto names = GetRequiredNamesForType(*field.m_pSchemaType); + result.insert(names.begin(), names.end()); + } - // swap if second class below first, and first depends on second. - bool first_depend = first->IsDependsOn(*second); + if (const auto* base_classes = class_.m_pBaseClasses; base_classes != nullptr) { + assert(base_classes->m_pClass->m_pSchemaType != nullptr && "didn't think this could happen, feel free to touch"); + // source2gen doesn't support multiple inheritance, only check class[0] + const auto includes = GetRequiredNamesForType(*base_classes[0].m_pClass->m_pSchemaType); + result.insert(includes.begin(), includes.end()); + } - // swap if first class below second, and second depends on first. - bool second_depend = second->IsDependsOn(*first); + const auto is_self = [self_module{GetModuleOfType(*class_.m_pSchemaType).value()}, self_type_name{class_.GetName()}](const auto& that) { + return (that.module == self_module) && (that.type_name == self_type_name); + }; - if (first_depend && second_depend) { - // classes depends on each other, forward declare them. - // @todo: verify that cyclic dependencies is a pointers. - continue; - } + // don't forward-declare or include self. happens for self-referencing types, e.g. entity2::CEntityComponentHelper + if (const auto found = std::ranges::find_if(result, is_self); found != result.end()) { + result.erase(found); + } - if (second_below_first ? first_depend : second_depend) { - std::iter_swap(first, second); - did_change = true; - } - } - } - } while (did_change); - // ================== - - // returns {type_name, array_sizes} - auto parse_array = [&](CSchemaType* type) -> std::pair> { - const auto ptr = type->GetRefClass(); - const auto actual_type = ptr ? ptr : type; - - std::string base_type; - std::vector sizes; - - if (actual_type->GetTypeCategory() == ETypeCategory::Schema_FixedArray) { - // dump all sizes. - auto schema = reinterpret_cast(actual_type); - while (true) { - sizes.emplace_back(schema->m_nElementCount); - schema = reinterpret_cast(schema->m_pElementType); - - if (schema->GetTypeCategory() != ETypeCategory::Schema_FixedArray) { - base_type = schema->m_pszName; - break; - } - } - } + return result; + } - return {base_type, sizes}; - }; + void AssembleClass(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { + struct cached_datamap_t { + std::string type_; + std::string name_; + std::ptrdiff_t offset_; + }; - // returns {type_name, array_sizes} - auto get_type = [&](CSchemaType* type) -> std::pair> { - auto [type_name, mods] = parse_array(type); + // @note: @es3n1n: get class info, assemble it + // + const auto* class_parent = class_.m_pBaseClasses ? class_.m_pBaseClasses->m_pClass : nullptr; + const auto& class_info = class_; + const auto is_struct = std::string_view{class_info.m_pszName}.ends_with("_t"); - assert((!type_name.empty() && !mods.empty()) || (type_name.empty() && mods.empty())); + PrintClassInfo(builder, class_info); - if (!type_name.empty() && !mods.empty()) - return {type_name, mods}; + // @note: @es3n1n: get parent name + // + const std::string parent_class_name = (class_parent != nullptr) ? MaybeWithModuleName(*class_parent->m_pTypeScope, class_parent->m_pszName) : ""; - return {std::string{type->m_pszName}, {}}; - }; + // @note: @es3n1n: start class + // + if (is_struct) + builder.begin_struct_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); + else + builder.begin_class_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); - for (auto& class_dump : classes_to_dump) { - // @note: @es3n1n: get class info, assemble it - // - const auto class_parent = class_dump.GetParent(); - const auto class_info = class_dump.target_; - const auto is_struct = std::string_view{class_info->m_pszName}.ends_with("_t"); - PrintClassInfo(builder, class_info); - - // @note: @es3n1n: get parent name - // - std::string parent_cls_name; - if (auto parent = class_info->m_pBaseClassses ? class_info->m_pBaseClassses->m_pClass : nullptr; parent) - parent_cls_name = parent->m_pszName; - - // @note: @es3n1n: start class - // - if (is_struct) - builder.begin_struct_with_base_type(class_info->m_pszName, parent_cls_name, ""); - else - builder.begin_class_with_base_type(class_info->m_pszName, parent_cls_name, ""); - - // @note: @es3n1n: field assembling state - // - struct { - std::size_t last_field_size = 0ull; - std::size_t last_field_offset = 0ull; - bool assembling_bitfield = false; - std::size_t total_bits_count_in_union = 0ull; - - std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var - } state; - - // @note: @es3n1n: if we need to pad first field or if there's no fields in this class - // and we need to properly pad it to make sure its size is the same as we expect it - // - std::optional first_field_offset = std::nullopt; - if (const auto first_field = class_dump.GetFirstField(); first_field) - first_field_offset = first_field->m_nSingleInheritanceOffset; - - const std::ptrdiff_t parent_class_size = class_parent ? class_parent->m_nSizeOf : 0; - - std::ptrdiff_t expected_pad_size = first_field_offset.value_or(class_dump.ClassSizeWithoutParent()); - if (expected_pad_size) // @note: @es3n1n: if there's a pad size we should account the parent class size - expected_pad_size -= parent_class_size; - - // @note: @es3n1n: and finally insert a pad - // - if (expected_pad_size > 0) // @fixme: @es3n1n: this is wrong, i probably should check for collisions instead - builder.access_modifier("private") - .struct_padding(parent_class_size, expected_pad_size, false, true) - .reset_tabs_count() - .comment(std::format("{:#x}", parent_class_size)) - .restore_tabs_count(); - - // @todo: @es3n1n: if for some mysterious reason this class describes fields - // of the base class we should handle it too. - if (class_parent && first_field_offset.has_value() && first_field_offset.value() < class_parent->m_nSizeOf) { - builder.comment( - std::format("Collision detected({:#x}->{:#x}), output may be wrong.", first_field_offset.value_or(0), class_parent->m_nSizeOf)); - state.collision_end_offset = class_parent->m_nSizeOf; - } + // @note: @es3n1n: field assembling state + // + struct { + std::size_t last_field_size = 0ull; + std::size_t last_field_offset = 0ull; + bool assembling_bitfield = false; + std::size_t total_bits_count_in_union = 0ull; - // @note: @es3n1n: begin public members - // - builder.access_modifier("public"); + std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var + } state; - for (const auto& field : class_info->GetFields()) { - // @fixme: @es3n1n: todo proper collision fix and remove this block - if (state.collision_end_offset && field.m_nSingleInheritanceOffset < state.collision_end_offset) { - builder.comment(std::format("Skipped field \"{}\" @ {:#x} because of the struct collision", field.m_pszName, - field.m_nSingleInheritanceOffset)); - continue; - } + std::list> cached_fields{}; + std::list cached_datamap_fields{}; - // @note: @es3n1n: obtaining size - // - const int field_size = field.m_pSchemaType->GetSize().value_or(0); - - // @note: @es3n1n: parsing type - // - const auto [type, mod] = get_type(field.m_pSchemaType); - const auto var_info = field_parser::parse(type, field.m_pszName, mod); - - // @note: @es3n1n: insert padding if needed - // - const auto expected_offset = state.last_field_offset + state.last_field_size; - if (state.last_field_offset && state.last_field_size && - expected_offset < static_cast(field.m_nSingleInheritanceOffset) && !state.assembling_bitfield) { - - builder.access_modifier("private") - .struct_padding(expected_offset, field.m_nSingleInheritanceOffset - expected_offset, false, true) - .reset_tabs_count() - .comment(std::format("{:#x}", expected_offset)) - .restore_tabs_count() - .access_modifier("public"); - } + // @note: @es3n1n: if we need to pad first field or if there's no fields in this class + // and we need to properly pad it to make sure its size is the same as we expect it + // + std::optional first_field_offset = std::nullopt; + if (const auto* first_field = (class_.m_pFields == nullptr) ? nullptr : &class_.m_pFields[0]; first_field) + first_field_offset = first_field->m_nSingleInheritanceOffset; - // @note: @es3n1n: begin union if we're assembling bitfields - // - if (!state.assembling_bitfield && var_info.is_bitfield()) { - builder.begin_bitfield_block(); - state.assembling_bitfield = true; - } + const std::ptrdiff_t parent_class_size = class_parent ? class_parent->m_nSizeOf : 0; + const auto class_size_without_parent = class_.m_nSizeOf - parent_class_size; - // @note: @es3n1n: if we are done with bitfields we should insert a pad and finish union - // - if (state.assembling_bitfield && !var_info.is_bitfield()) { - const auto expected_union_size_bytes = field.m_nSingleInheritanceOffset - state.last_field_offset; - const auto expected_union_size_bits = expected_union_size_bytes * 8; + std::ptrdiff_t expected_pad_size = first_field_offset.value_or(class_size_without_parent); + if (expected_pad_size) // @note: @es3n1n: if there's a pad size we should account the parent class size + expected_pad_size -= parent_class_size; - const auto actual_union_size_bits = state.total_bits_count_in_union; + // @note: @es3n1n: and finally insert a pad + // + if (expected_pad_size > 0) // @fixme: @es3n1n: this is wrong, i probably should check for collisions instead + builder.access_modifier("private") + .struct_padding(parent_class_size, expected_pad_size, false, true) + .reset_tabs_count() + .comment(std::format("{:#x}", parent_class_size)) + .restore_tabs_count(); + + // @todo: @es3n1n: if for some mysterious reason this class describes fields + // of the base class we should handle it too. + if (class_parent && first_field_offset.has_value() && first_field_offset.value() < class_parent->m_nSizeOf) { + builder.comment( + std::format("Collision detected({:#x}->{:#x}), output may be wrong.", first_field_offset.value_or(0), class_parent->m_nSizeOf)); + state.collision_end_offset = class_parent->m_nSizeOf; + } - if (expected_union_size_bits < state.total_bits_count_in_union) - throw std::runtime_error( - std::format("Unexpected union size: {}. Expected: {}", state.total_bits_count_in_union, expected_union_size_bits)); + // @note: @es3n1n: begin public members + // + builder.access_modifier("public"); + + for (const auto& field : class_info.GetFields()) { + // @fixme: @es3n1n: todo proper collision fix and remove this block + if (state.collision_end_offset && field.m_nSingleInheritanceOffset < state.collision_end_offset) { + builder.comment( + std::format("Skipped field \"{}\" @ {:#x} because of the struct collision", field.m_pszName, field.m_nSingleInheritanceOffset)); + continue; + } - if (expected_union_size_bits > state.total_bits_count_in_union) - builder.struct_padding(std::nullopt, 0, true, false, expected_union_size_bits - actual_union_size_bits); + // @note: @es3n1n: obtaining size + // fall back to 1 because there are no 0-sized types + // + const int field_size = field.m_pSchemaType->GetSize().value_or(1); + + // @note: @es3n1n: parsing type + // + const auto [type_name, array_sizes] = GetType(*field.m_pSchemaType); + const auto var_info = field_parser::parse(type_name, field.m_pszName, array_sizes); + + // @note: @es3n1n: insert padding if needed + // + const auto expected_offset = state.last_field_offset + state.last_field_size; + if (state.last_field_offset && state.last_field_size && expected_offset < static_cast(field.m_nSingleInheritanceOffset) && + !state.assembling_bitfield) { + + builder.access_modifier("private") + .struct_padding(expected_offset, field.m_nSingleInheritanceOffset - expected_offset, false, true) + .reset_tabs_count() + .comment(std::format("{:#x}", expected_offset)) + .restore_tabs_count() + .access_modifier("public"); + } - state.last_field_offset += expected_union_size_bytes; - state.last_field_size = expected_union_size_bytes; + // @note: @es3n1n: begin union if we're assembling bitfields + // + if (!state.assembling_bitfield && var_info.is_bitfield()) { + builder.begin_bitfield_block(); + state.assembling_bitfield = true; + } - builder.end_bitfield_block(false) - .reset_tabs_count() - .comment(std::format("{:d} bits", expected_union_size_bits)) - .restore_tabs_count(); + // @note: @es3n1n: if we are done with bitfields we should insert a pad and finish union + // + if (state.assembling_bitfield && !var_info.is_bitfield()) { + const auto expected_union_size_bytes = field.m_nSingleInheritanceOffset - state.last_field_offset; + const auto expected_union_size_bits = expected_union_size_bytes * 8; - state.total_bits_count_in_union = 0ull; - state.assembling_bitfield = false; - } + const auto actual_union_size_bits = state.total_bits_count_in_union; - // @note: @es3n1n: dump metadata - // - for (auto j = 0; j < field.m_nMetadataSize; j++) { - auto field_metadata = field.m_pMetadata[j]; + if (expected_union_size_bits < state.total_bits_count_in_union) + throw std::runtime_error( + std::format("Unexpected union size: {}. Expected: {}", state.total_bits_count_in_union, expected_union_size_bits)); - if (auto data = GetMetadataValue(field_metadata); data.empty()) - builder.comment(field_metadata.m_szName); - else - builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); - } + if (expected_union_size_bits > state.total_bits_count_in_union) + builder.struct_padding(std::nullopt, 0, true, false, expected_union_size_bits - actual_union_size_bits); - // if this prop is a non-pointer class, check if its worth directly embedding the accumulated offset of it into the metadata - auto prop_class = - std::ranges::find_if(classes_to_dump, [type](const class_t& cls) { return cls.target_->GetName().compare(type) == 0; }); - if (prop_class != classes_to_dump.end()) { - // verify for min/max fields count, we don't want to bloat the dump by embeding too much stuff - if (prop_class->cached_fields_.size() >= kMinFieldCountForClassEmbed && - prop_class->cached_fields_.size() <= kMaxFieldCountForClassEmbed) { - // if a class is used in too many classes its likely not very useful, so ignore it - if (prop_class->used_count_ <= kMaxReferencesForClassEmbed) { - for (const auto& [cached_field_name, cached_field_offset] : prop_class->cached_fields_) { - const auto accumulated_offset = cached_field_offset + field.m_nSingleInheritanceOffset; - builder.comment(std::format("-> {} - {:#x}", cached_field_name, accumulated_offset)); - } - } - } - } + state.last_field_offset += expected_union_size_bytes; + state.last_field_size = expected_union_size_bytes; - // @note: @es3n1n: update state - // - if (field.m_nSingleInheritanceOffset && field_size) { - state.last_field_offset = field.m_nSingleInheritanceOffset; - state.last_field_size = static_cast(field_size); - } - if (var_info.is_bitfield()) - state.total_bits_count_in_union += var_info.m_bitfield_size; - - // @note: @es3n1n: push prop - // - builder.prop(var_info.m_type, var_info.formatted_name(), false); - if (!var_info.is_bitfield()) { - builder.reset_tabs_count().comment(std::format("{:#x}", field.m_nSingleInheritanceOffset), false).restore_tabs_count(); - class_dump.cached_fields_.emplace_back(var_info.formatted_name(), field.m_nSingleInheritanceOffset); - } - builder.next_line(); - } + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); - // @note: @es3n1n: if struct ends with union we should end union before ending the class - // - if (state.assembling_bitfield) { - const auto actual_union_size_bits = state.total_bits_count_in_union; + state.total_bits_count_in_union = 0ull; + state.assembling_bitfield = false; + } - // @note: @es3n1n: apply 8 bytes align - // - const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); + // @note: @es3n1n: dump metadata + // + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; - if (expected_union_size_bits > actual_union_size_bits) - builder.struct_padding(std::nullopt, 0, true, false, expected_union_size_bits - actual_union_size_bits); + if (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(field_metadata.m_szName); + else + builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); + } - builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + // @note: @es3n1n: update state + // + if (field.m_nSingleInheritanceOffset && field_size) { + state.last_field_offset = field.m_nSingleInheritanceOffset; + state.last_field_size = static_cast(field_size); + } + if (var_info.is_bitfield()) + state.total_bits_count_in_union += var_info.m_bitfield_size; - state.total_bits_count_in_union = 0; - state.assembling_bitfield = false; - } + // @note: @es3n1n: push prop + // + builder.prop(var_info.m_type, var_info.formatted_name(), false); - // @note: @es3n1n: dump static fields - // - if (class_info->m_nStaticFieldsSize) { - if (class_info->m_nFieldSize) - builder.next_line(); - builder.comment("Static fields:"); - } + if (!var_info.is_bitfield()) { + builder.reset_tabs_count().comment(std::format("{:#x}", field.m_nSingleInheritanceOffset), false).restore_tabs_count(); + cached_fields.emplace_back(var_info.formatted_name(), field.m_nSingleInheritanceOffset); + } + builder.next_line(); + } + + // @note: @es3n1n: if struct ends with union we should end union before ending the class + // + if (state.assembling_bitfield) { + const auto actual_union_size_bits = state.total_bits_count_in_union; - for (auto s = 0; s < class_info->m_nStaticFieldsSize; s++) { - auto static_field = &class_info->m_pStaticFields[s]; + // @note: @es3n1n: apply 8 bytes align + // + const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); - auto [type, mod] = get_type(static_field->m_pSchemaType); - const auto var_info = field_parser::parse(type, static_field->m_pszName, mod); - builder.static_field_getter(var_info.m_type, var_info.m_name, current->BGetScopeName(), class_info->m_pszName, s); - } + if (expected_union_size_bits > actual_union_size_bits) + builder.struct_padding(std::nullopt, 0, true, false, expected_union_size_bits - actual_union_size_bits); + + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); - if (class_info->m_pFieldMetadataOverrides && class_info->m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { - const auto& dm = class_info->m_pFieldMetadataOverrides; + state.total_bits_count_in_union = 0; + state.assembling_bitfield = false; + } - for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { - auto* t = &dm->m_pTypeDescription[s]; - if (!t) - continue; + // @note: @es3n1n: dump static fields + // + if (class_info.m_nStaticFieldsSize) { + if (class_info.m_nFieldSize) + builder.next_line(); + builder.comment("Static fields:"); + } - if (t->GetFieldName().empty()) - continue; + // The current class may be defined in multiple scopes. It doesn't matter which one we use, as all definitions are the same.. + // TODO: verify the above statement. Are static fields really shared between scopes? + const std::string scope_name{class_info.m_pTypeScope->BGetScopeName()}; - const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); + for (auto s = 0; s < class_info.m_nStaticFieldsSize; s++) { + auto static_field = &class_info.m_pStaticFields[s]; - std::string field_type = var_info.m_type; - if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { - field_type = t->m_pDataMap->m_pszClassName; - } + auto [type, mod] = GetType(*static_field->m_pSchemaType); + const auto var_info = field_parser::parse(type, static_field->m_pszName, mod); + builder.static_field_getter(var_info.m_type, var_info.m_name, scope_name, class_info.m_pszName, s); + } - std::string field_name = var_info.formatted_name(); + if (class_info.m_pFieldMetadataOverrides && class_info.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { + const auto& dm = class_info.m_pFieldMetadataOverrides; - // @note: @og: if schema dump already has this field, then just skip it - if (const auto it = std::ranges::find_if( - class_dump.cached_fields_, [t, field_name](const auto& f) { return f.first == field_name && f.second == t->m_iOffset; }); - it != class_dump.cached_fields_.end()) - continue; + for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { + auto* t = &dm->m_pTypeDescription[s]; + if (!t) + continue; - class_dump.cached_datamap_fields_.emplace_back(field_type, field_name, t->m_iOffset); - } + if (t->GetFieldName().empty()) + continue; - if (!class_dump.cached_datamap_fields_.empty()) { - if (class_info->m_nFieldSize) - builder.next_line(); + const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); - builder.comment("Datamap fields:"); - for (auto& [field_type, field_name, field_offset] : class_dump.cached_datamap_fields_) { - builder.comment(std::format("{} {}; // {:#x}", field_type, field_name, field_offset)); - } - } + std::string field_type = var_info.m_type; + if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { + field_type = t->m_pDataMap->m_pszClassName; } - if (!class_info->m_nFieldSize && !class_info->m_nStaticMetadataSize) - builder.comment("No schema binary for binding"); + std::string field_name = var_info.formatted_name(); + + // @note: @og: if schema dump already has this field, then just skip it + if (const auto it = + std::ranges::find_if(cached_fields, [t, field_name](const auto& f) { return f.first == field_name && f.second == t->m_iOffset; }); + it != cached_fields.end()) + continue; - builder.end_block(); + cached_datamap_fields.emplace_back(field_type, field_name, t->m_iOffset); } - } - template - std::string StringifyUtlTsHashCount(const CUtlTSHashV1& item) { - return util::PrettifyNum(item.PeakAlloc()); - } + if (!cached_datamap_fields.empty()) { + if (class_info.m_nFieldSize) + builder.next_line(); - template - std::string StringifyUtlTsHashCount(const CUtlTSHashV2& item) { - return std::format("{} (Allocated) | {} (Unallocated)", util::PrettifyNum(item.BlocksAllocated()), util::PrettifyNum(item.PeakAlloc())); + builder.comment("Datamap fields:"); + for (auto& [field_type, field_name, field_offset] : cached_datamap_fields) { + builder.comment(std::format("{} {}; // {:#x}", field_type, field_name, field_offset)); + } + } } - } // namespace - void GenerateTypeScopeSdk(CSchemaSystemTypeScope* current) { - // @note: @es3n1n: getting current scope name & formatting it + if (!class_info.m_nFieldSize && !class_info.m_nStaticMetadataSize) + builder.comment("No schema binary for binding"); + + builder.end_block(); + } + + void GenerateEnumSdk(std::string_view module_name, const CSchemaEnumBinding& enum_) { + const std::string out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, enum_.m_pszName)); + + // @note: @es3n1n: init codegen // - constexpr std::string_view module_file_prefix = platform_specific{.windows = "", .linux = "lib"}.get(); - constexpr std::string_view module_file_suffix = platform_specific{.windows = ".dll", .linux = ".so"}.get(); - auto scope_name = current->BGetScopeName(); - if (scope_name.ends_with(module_file_suffix)) - scope_name = scope_name.substr(module_file_prefix.size(), scope_name.size() - (module_file_prefix.size() + module_file_suffix.size())); + auto builder = codegen::get(); + builder.pragma("once"); - // @note: @es3n1n: print debug info + builder.include(""); + + // @note: @es3n1n: print banner + // + builder.next_line() + .comment("/////////////////////////////////////////////////////////////") + .comment(std::format("Module: {}", module_name)) + .comment(std::string{kCreatedBySource2genMessage}) + .comment("/////////////////////////////////////////////////////////////") + .next_line(); + + builder.begin_namespace(std::format("source2sdk::{}", module_name)); + + // @note: @es3n1n: assemble props // - std::cout << std::format("{}: Assembling {}", __FUNCTION__, scope_name) << std::endl; + AssembleEnum(builder, enum_); - // @note: @es3n1n: build file path + builder.end_namespace(); + + // @note: @es3n1n: write generated data to output file // - if (!std::filesystem::exists(kOutDirName)) - std::filesystem::create_directories(kOutDirName); - const std::string out_file_path = std::format("{}/{}.hpp", kOutDirName, scope_name); + std::ofstream f(out_file_path, std::ios::out); + f << builder.str(); + if (f.bad()) { + std::cerr << std::format("Could not write to {}: {}", out_file_path, std::strerror(errno)) << std::endl; + // This std::exit() is bad. Instead, we could return the dumped + // header name and content to the caller in a std::expected. Let the + // caller write the file. That would also allow the caller to choose + // the output directory and handle errors. + std::exit(1); + } + } + + void GenerateClassSdk(std::string_view module_name, const CSchemaClassBinding& class_) { + const std::filesystem::path out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, class_.m_pszName)); // @note: @es3n1n: init codegen // auto builder = codegen::get(); builder.pragma("once"); - // @note: @es3n1n: include files - // - for (auto&& include_path : include_paths) - builder.include(include_path.data()); + const auto names = GetRequiredNamesForClass(class_); - // @note: @es3n1n: get stuff from schema that we'll use later - // @todo @es3n1n: consider moving these to heap as they're too damn large - // - const auto current_classes = current->GetClassBindings(); - const auto current_enums = current->GetEnumBindings(); + for (const auto& include : names | std::views::filter([](const auto& el) { return el.source == NameSource::include; })) { + builder.include(std::format("\"{}/{}.hpp\"", include.module, util::EscapePath(include.type_name))); + } + + builder.include(""); + + for (const auto& forward_declaration : names | std::views::filter([](const auto& el) { return el.source == NameSource::forward_declaration; })) { + builder.begin_namespace(std::format("source2sdk::{}", forward_declaration.module)); + builder.forward_declaration(forward_declaration.type_name); + builder.end_namespace(); + } // @note: @es3n1n: print banner // builder.next_line() .comment("/////////////////////////////////////////////////////////////") - .comment(std::format("Binary: {}", current->GetScopeName())) - .comment(std::format("Classes count: {}", StringifyUtlTsHashCount(current_classes))) - .comment(std::format("Enums count: {}", StringifyUtlTsHashCount(current_enums))) - .comment(kCreatedBySource2genMessage.data()) + .comment(std::format("Module: {}", module_name)) + .comment(std::string{kCreatedBySource2genMessage}) .comment("/////////////////////////////////////////////////////////////") .next_line(); + builder.begin_namespace(std::format("source2sdk::{}", module_name)); + // @note: @es3n1n: assemble props // - AssembleEnums(builder, current_enums); - AssembleClasses(current, builder, current_classes); + AssembleClass(builder, class_); + + builder.end_namespace(); // @note: @es3n1n: write generated data to output file // std::ofstream f(out_file_path, std::ios::out); f << builder.str(); if (f.bad()) { - std::cerr << std::format("Could not write to {}: {}", out_file_path, std::strerror(errno)) << std::endl; + std::cerr << std::format("Could not write to {}: {}", out_file_path.string(), std::strerror(errno)) << std::endl; // This std::exit() is bad. Instead, we could return the dumped // header name and content to the caller in a std::expected. Let the // caller write the file. That would also allow the caller to choose @@ -800,6 +913,24 @@ namespace sdk { std::exit(1); } } +} // namespace + +namespace sdk { + void GenerateTypeScopeSdk(std::string_view module_name, const std::unordered_set& enums, + const std::unordered_set& classes) { + // @note: @es3n1n: print debug info + // + std::cout << std::format("{}: Assembling module {} with {} enum(s) and {} class(es)", __FUNCTION__, module_name, enums.size(), classes.size()) + << std::endl; + + const std::filesystem::path out_directory_path = std::format("{}/{}", kOutDirName, module_name); + + if (!std::filesystem::exists(out_directory_path)) + std::filesystem::create_directories(out_directory_path); + + std::ranges::for_each(enums, [=](const auto* el) { GenerateEnumSdk(module_name, *el); }); + std::ranges::for_each(classes, [=](const auto* el) { GenerateClassSdk(module_name, *el); }); + } } // namespace sdk // source2gen - Source2 games SDK generator diff --git a/source2gen/src/startup/startup.cpp b/source2gen/src/startup/startup.cpp index 4fcf466..4113d53 100644 --- a/source2gen/src/startup/startup.cpp +++ b/source2gen/src/startup/startup.cpp @@ -7,7 +7,14 @@ #include #include +#include +#include #include +#include +#include +#include +#include +#include namespace { [[nodiscard]] auto get_required_modules() { @@ -50,6 +57,56 @@ namespace { } // namespace namespace source2_gen { + struct module_dump { + std::unordered_set enums{}; + std::unordered_set classes{}; + }; + + /// @return Key is the module name + std::unordered_map collect_modules(std::span type_scopes) { + struct unique_module_dump { + /// Key is the enum name. Used for de-duplication. + std::unordered_map enums{}; + /// Key is the class name. Used for de-duplication. + std::unordered_map classes{}; + }; + + // Key is the module name, e.g. SchemaEnumInfoData_t::m_pszModule. + std::unordered_map dumped_modules{}; + + for (const auto* current_scope : type_scopes) { + auto current_enums = current_scope->GetEnumBindings(); + for (auto el : current_enums.GetElements()) { + auto& dump{dumped_modules.emplace(el->m_pszModule, unique_module_dump{}).first->second}; + dump.enums.emplace(el->m_pszName, el); + } + + auto current_classes = current_scope->GetClassBindings(); + for (auto el : current_classes.GetElements()) { + auto& dump{dumped_modules.emplace(el->m_pszModule, unique_module_dump{}).first->second}; + dump.classes.emplace(el->m_pszName, el); + } + } + + std::unordered_map result{}; + + for (const auto& [module_name, unique_dump] : dumped_modules) { + constexpr auto to_set = [](const auto& pair) { + return pair.second; + }; + + module_dump dump{}; + + std::ranges::transform(unique_dump.enums, std::inserter(dump.enums, dump.enums.end()), to_set); + + std::ranges::transform(unique_dump.classes, std::inserter(dump.classes, dump.classes.end()), to_set); + + result.emplace(module_name, dump); + } + + return result; + } + bool Dump() try { // set up the allocator before anything else. we can't use allocating // C++ functions without it. @@ -114,12 +171,12 @@ namespace source2_gen { // @note: @es3n1n: Obtaining type scopes and generating sdk const auto type_scopes = sdk::g_schema->GetTypeScopes(); assert(type_scopes.Count() > 0 && "sdk is outdated"); - for (auto i = 0; i < type_scopes.Count(); ++i) - sdk::GenerateTypeScopeSdk(type_scopes.m_pElements[i]); - // @note: @es3n1n: Generating sdk for global type scope - // - sdk::GenerateTypeScopeSdk(sdk::g_schema->GlobalTypeScope()); + const std::unordered_map all_modules = collect_modules(std::span{type_scopes.m_pElements, static_cast(type_scopes.m_Size)}); + + for (const auto& [module_name, dump] : all_modules) { + sdk::GenerateTypeScopeSdk(module_name, dump.enums, dump.classes); + } std::cout << std::format("Schema stats: {} registrations; {} were redundant; {} were ignored ({} bytes of ignored data)", util::PrettifyNum(sdk::g_schema->GetRegistration()), util::PrettifyNum(sdk::g_schema->GetRedundant()), diff --git a/source2gen/src/tools/field_parser.cpp b/source2gen/src/tools/field_parser.cpp index 2b89a79..93f917e 100644 --- a/source2gen/src/tools/field_parser.cpp +++ b/source2gen/src/tools/field_parser.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2024 neverlosecc // See end of file for extended copyright information. -#include +#include "tools/field_parser.h" +#include "tools/codegen.h" #include namespace field_parser { @@ -123,7 +124,7 @@ namespace field_parser { if (result.m_field_type == fieldtype_t::FIELD_UNUSED) result.m_field_type = type_name; - // @note: @es3n1n: applying kTypeNameToCpp rules + // @note: @es3n1n: applying kDatamapToCpp rules for (auto& rule : kDatamapToCpp) { if (result.m_field_type != rule.first) continue; @@ -134,6 +135,15 @@ namespace field_parser { } } // namespace detail + std::optional type_name_to_cpp(std::string_view type_name) { + if (const auto found = std::ranges::find(detail::kTypeNameToCpp, type_name, &decltype(detail::kTypeNameToCpp)::value_type::first); + found != detail::kTypeNameToCpp.end()) { + return found->second; + } else { + return std::nullopt; + } + } + field_info_t parse(const std::string& type_name, const std::string& name, const std::vector& array_sizes) { field_info_t result = {}; result.m_name = name;