From b2ed2eea184bcaf4611f3f63986165180c229c14 Mon Sep 17 00:00:00 2001 From: Cre3per Date: Fri, 26 Jul 2024 16:32:46 +0200 Subject: [PATCH 1/9] generate types per module --- .gitignore | 4 +- CMakeLists.txt | 1 + include/sdk/sdk.h | 4 +- src/sdk/sdk.cpp | 214 ++++++++++++++++++++++++---------------- src/startup/startup.cpp | 66 ++++++++++++- 5 files changed, 194 insertions(+), 95 deletions(-) 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/CMakeLists.txt b/CMakeLists.txt index 5e35aae..55e9c84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,4 +110,5 @@ target_compile_definitions(source2gen PRIVATE "NOMINMAX" "WIN32_LEAN_AND_MEAN" "_WIN32_WINNT=0x601" + "__cpp_lib_expected" # Because clangd-18 + GLIBCXX_3.4.33 doesn't know std::expected. This is a hack to make the language server work. ) \ No newline at end of file diff --git a/include/sdk/sdk.h b/include/sdk/sdk.h index 8ddb796..f06b92c 100644 --- a/include/sdk/sdk.h +++ b/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/src/sdk/sdk.cpp b/src/sdk/sdk.cpp index c9ff89f..c0b8fd4 100644 --- a/src/sdk/sdk.cpp +++ b/src/sdk/sdk.cpp @@ -5,14 +5,14 @@ #include "sdk/sdk.h" #include #include -#include #include +#include namespace { using namespace std::string_view_literals; constexpr std::string_view kOutDirName = "sdk"sv; - constinit std::array include_paths = {""sv, R"("!GlobalTypes.hpp")"sv}; + constinit std::array include_paths = {""sv}; constexpr uint32_t kMaxReferencesForClassEmbed = 2; constexpr std::size_t kMinFieldCountForClassEmbed = 2; @@ -144,11 +144,7 @@ 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)); + builder.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"); @@ -191,11 +187,8 @@ namespace sdk { } } - 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)) + 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)); @@ -207,8 +200,8 @@ namespace sdk { } } - void AssembleEnums(codegen::generator_t::self_ref builder, CUtlTSHash enums) { - for (auto schema_enum_binding : enums.GetElements()) { + void AssembleEnums(codegen::generator_t::self_ref builder, const std::unordered_set& enums) { + for (auto schema_enum_binding : enums) { // @note: @es3n1n: get type name by align size // const auto get_type_name = [schema_enum_binding]() -> std::string { @@ -286,7 +279,62 @@ namespace sdk { } } - void AssembleClasses(CSchemaSystemTypeScope* current, codegen::generator_t::self_ref builder, CUtlTSHash classes) { + /// @return {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 {base_type, sizes}; + }; + + /// @return @ref std::nullopt if the type is not contained in a module, e.g. because it is a built-in type + auto get_module(CSchemaType* type) -> std::optional { + if (type->m_pTypeScope != nullptr) { + if (const auto* class_ = type->m_pTypeScope->FindDeclaredClass(type->m_pszName)) { + return class_->m_pszModule; + } else if (const auto* enum_ = type->m_pTypeScope->FindDeclaredEnum(type->m_pszName)) { + return enum_->m_pszModule; + } + } + + return std::nullopt; + }; + + /// @return {type_name, array_sizes} where type_name is a fully qualified name + auto get_type(CSchemaType* type) -> std::pair> { + const auto maybe_with_module_name = [type](const auto& type_name) { + return get_module(type) + .transform([&](const auto module_name) { return std::format("{}::{}", module_name, type_name); }) + .value_or(type_name); + }; + const auto [type_name, array_sizes] = parse_array(type); + + assert(type_name.empty() == array_sizes.empty()); + + if (!type_name.empty() && !array_sizes.empty()) + return {maybe_with_module_name(type_name), array_sizes}; + + return {maybe_with_module_name(type->m_pszName), {}}; + }; + + void AssembleClasses(codegen::generator_t::self_ref builder, const std::unordered_set& classes) { struct class_t { CSchemaClassInfo* target_{}; std::set refs_; @@ -355,10 +403,10 @@ namespace sdk { std::list classes_to_dump; bool did_forward_decls = false; - for (const auto* schema_class_binding : classes.GetElements()) { + for (const auto* schema_class_binding : classes) { assert(schema_class_binding != nullptr); - const auto class_info = current->FindDeclaredClass(schema_class_binding->m_pszName); + const auto class_info = schema_class_binding->m_pTypeScope->FindDeclaredClass(schema_class_binding->m_pszName); auto& class_dump = classes_to_dump.emplace_back(); class_dump.target_ = class_info; @@ -429,43 +477,6 @@ namespace sdk { } 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 {base_type, sizes}; - }; - - // returns {type_name, array_sizes} - auto get_type = [&](CSchemaType* type) -> std::pair> { - auto [type_name, mods] = parse_array(type); - - assert((!type_name.empty() && !mods.empty()) || (type_name.empty() && mods.empty())); - - if (!type_name.empty() && !mods.empty()) - return {type_name, mods}; - - return {std::string{type->m_pszName}, {}}; - }; - for (auto& class_dump : classes_to_dump) { // @note: @es3n1n: get class info, assemble it // @@ -546,8 +557,8 @@ namespace sdk { // @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); + const auto [type_name, array_sizes] = get_type(field.m_pSchemaType); + const auto var_info = field_parser::parse(type_name, field.m_pszName, array_sizes); // @note: @es3n1n: insert padding if needed // @@ -610,7 +621,7 @@ namespace sdk { // 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; }); + std::ranges::find_if(classes_to_dump, [type_name](const class_t& cls) { return cls.target_->GetName().compare(type_name) == 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 && @@ -670,12 +681,16 @@ namespace sdk { builder.comment("Static fields:"); } + // 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()}; + for (auto s = 0; s < class_info->m_nStaticFieldsSize; s++) { auto static_field = &class_info->m_pStaticFields[s]; 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); + builder.static_field_getter(var_info.m_type, var_info.m_name, scope_name, class_info->m_pszName, s); } if (class_info->m_pFieldMetadataOverrides && class_info->m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { @@ -725,35 +740,58 @@ namespace sdk { } } - template - std::string StringifyUtlTsHashCount(const CUtlTSHashV1& item) { - return util::PrettifyNum(item.PeakAlloc()); + } // namespace + + /// @param exclude_module_name will not be contained in the return value + /// @return All modules that are required by properties of @p classes + std::set find_required_modules_of_class(std::string_view exclude_module_name, const CSchemaClassBinding& class_) { + std::set modules{}; + + for (const auto& field : std::span{class_.m_pFields, static_cast(class_.m_nFieldSize)}) { + if (const auto module = get_module(field.m_pSchemaType)) { + modules.emplace(module.value()); + } } - template - std::string StringifyUtlTsHashCount(const CUtlTSHashV2& item) { - return std::format("{} (Allocated) | {} (Unallocated)", util::PrettifyNum(item.BlocksAllocated()), util::PrettifyNum(item.PeakAlloc())); + for (const auto& field : std::span{class_.m_pStaticFields, static_cast(class_.m_nStaticFieldsSize)}) { + if (const auto module = get_module(field.m_pSchemaType)) { + modules.emplace(module.value()); + } } - } // namespace - void GenerateTypeScopeSdk(CSchemaSystemTypeScope* current) { - // @note: @es3n1n: getting current scope name & formatting it - // - 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())); + if (const auto found = modules.find(std::string{exclude_module_name}); found != modules.end()) { + modules.erase(found); + } + return modules; + } + + /// @param exclude_module_name will not be contained in the return value + /// @return All modules that are required by properties of @p classes + std::set find_required_modules(std::string_view exclude_module_name, const std::unordered_set& classes) { + std::set modules{}; + + for (const auto& class_ : classes) { + assert(class_ != nullptr); + + const auto partial = find_required_modules_of_class(exclude_module_name, *class_); + modules.insert(partial.begin(), partial.end()); + } + + return modules; + } + + 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 {}", __FUNCTION__, scope_name) << std::endl; + std::cout << std::format("{}: Assembling {}", __FUNCTION__, module_name) << std::endl; // @note: @es3n1n: build file path // if (!std::filesystem::exists(kOutDirName)) std::filesystem::create_directories(kOutDirName); - const std::string out_file_path = std::format("{}/{}.hpp", kOutDirName, scope_name); + const std::string out_file_path = std::format("{}/{}.hpp", kOutDirName, module_name); // @note: @es3n1n: init codegen // @@ -762,30 +800,32 @@ namespace sdk { // @note: @es3n1n: include files // + for (const auto& required_module : find_required_modules(module_name, classes)) { + builder.include(std::format("\"{}.hpp\"", required_module)); + } + for (auto&& include_path : include_paths) builder.include(include_path.data()); - // @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(); - // @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::format("Classes count: {}", classes.size())) + .comment(std::format("Enums count: {}", enums.size())) + .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); + AssembleEnums(builder, enums); + AssembleClasses(builder, classes); + + builder.end_namespace(); // @note: @es3n1n: write generated data to output file // diff --git a/src/startup/startup.cpp b/src/startup/startup.cpp index 03314ac..035878e 100644 --- a/src/startup/startup.cpp +++ b/src/startup/startup.cpp @@ -5,9 +5,13 @@ #include #include +#include #include #include #include +#include +#include +#include namespace { [[nodiscard]] auto get_required_modules() { @@ -50,6 +54,58 @@ 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()) { + // TOOD: this is an expensive get_or_default() + 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()) { + // TOOD: this is an expensive get_or_default() + 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 +170,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()), From 4ad98fc8b69ef36395a8375519c89af2fa8e51e9 Mon Sep 17 00:00:00 2001 From: Cre3per Date: Fri, 26 Jul 2024 16:38:17 +0200 Subject: [PATCH 2/9] remove TODO, it's ok to be slow in this part of the code --- src/startup/startup.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/startup/startup.cpp b/src/startup/startup.cpp index 035878e..bb77f41 100644 --- a/src/startup/startup.cpp +++ b/src/startup/startup.cpp @@ -74,14 +74,12 @@ namespace source2_gen { for (const auto* current_scope : type_scopes) { auto current_enums = current_scope->GetEnumBindings(); for (auto el : current_enums.GetElements()) { - // TOOD: this is an expensive get_or_default() 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()) { - // TOOD: this is an expensive get_or_default() auto& dump{dumped_modules.emplace(el->m_pszModule, unique_module_dump{}).first->second}; dump.classes.emplace(el->m_pszName, el); } From db3f74b6430c5be4aca7acdca625c1147a35b1eb Mon Sep 17 00:00:00 2001 From: Cre3per Date: Sun, 4 Aug 2024 17:22:08 +0200 Subject: [PATCH 3/9] dump one type per file --- include/sdk/interfaces/common/CUtlVector.h | 1 + include/sdk/interfaces/schemasystem/schema.h | 59 +- include/sdk/interfaces/tier0/IMemAlloc.h | 2 + include/tools/field_parser.h | 13 +- include/tools/virtual.h | 9 +- src/sdk/sdk.cpp | 1070 ++++++++++-------- src/tools/field_parser.cpp | 14 +- 7 files changed, 653 insertions(+), 515 deletions(-) diff --git a/include/sdk/interfaces/common/CUtlVector.h b/include/sdk/interfaces/common/CUtlVector.h index d29172f..27834fc 100644 --- a/include/sdk/interfaces/common/CUtlVector.h +++ b/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/include/sdk/interfaces/schemasystem/schema.h b/include/sdk/interfaces/schemasystem/schema.h index a8ca40d..2cef438 100644 --- a/include/sdk/interfaces/schemasystem/schema.h +++ b/include/sdk/interfaces/schemasystem/schema.h @@ -382,7 +382,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) @@ -421,13 +421,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 +617,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 +638,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,19 +687,19 @@ 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 { @@ -765,36 +766,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/include/sdk/interfaces/tier0/IMemAlloc.h b/include/sdk/interfaces/tier0/IMemAlloc.h index 306cb36..75c30bb 100644 --- a/include/sdk/interfaces/tier0/IMemAlloc.h +++ b/include/sdk/interfaces/tier0/IMemAlloc.h @@ -4,6 +4,8 @@ #pragma warning(push) #pragma warning(disable:4191) + +#include "tools/platform.h" #include "tools/virtual.h" class IMemAlloc { diff --git a/include/tools/field_parser.h b/include/tools/field_parser.h index 9463feb..67de5dc 100644 --- a/include/tools/field_parser.h +++ b/include/tools/field_parser.h @@ -2,10 +2,13 @@ // See end of file for extended copyright information. #pragma once -#include +#include +#include +#include #include - -enum class fieldtype_t : uint8_t; +#include +#include +#include namespace field_parser { class field_info_t { @@ -65,6 +68,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/include/tools/virtual.h b/include/tools/virtual.h index 3109960..76e70ae 100644 --- a/include/tools/virtual.h +++ b/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/src/sdk/sdk.cpp b/src/sdk/sdk.cpp index c0b8fd4..f7d3933 100644 --- a/src/sdk/sdk.cpp +++ b/src/sdk/sdk.cpp @@ -4,9 +4,15 @@ // ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken #include "sdk/sdk.h" #include +#include #include +#include +#include +#include #include #include +#include +#include namespace { using namespace std::string_view_literals; @@ -97,7 +103,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); @@ -142,44 +148,50 @@ namespace { } // namespace namespace sdk { + struct cached_datamap_t { + std::string type_; + std::string name_; + std::ptrdiff_t offset_; + }; + namespace { - void PrintClassInfo(codegen::generator_t::self_ref builder, CSchemaClassBinding* class_info) { - builder.comment(std::format("Alignment: {}", class_info->GetAligment())).comment(std::format("Size: {:#x}", class_info->m_nSizeOf)); + void PrintClassInfo(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_info) { + builder.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 + 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) + 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) + 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) + 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) + if ((class_info.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_ALLOWED) != 0) builder.comment("Construct allowed"); - if ((class_info->m_nClassFlags & SCHEMA_CF1_CONSTRUCT_DISALLOWED) != 0) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkAssumeNotNetworkable) != 0) builder.comment("MNetworkAssumeNotNetworkable"); #endif - if (class_info->m_nStaticMetadataSize > 0) + if (class_info.m_nStaticMetadataSize > 0) builder.comment(""); - for (const auto& metadata : class_info->GetStaticMetadata()) { + for (const auto& metadata : class_info.GetStaticMetadata()) { if (const auto value = GetMetadataValue(metadata); !value.empty()) builder.comment(std::format("{} \"{}\"", metadata.m_szName, value)); else @@ -187,112 +199,110 @@ namespace sdk { } } - 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)); + 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) + if (enum_binding.m_nStaticMetadataSize > 0) builder.comment(""); - for (const auto& metadata : enum_binding->GetStaticMetadata()) { + for (const auto& metadata : enum_binding.GetStaticMetadata()) { builder.comment(metadata.m_szName); } } - void AssembleEnums(codegen::generator_t::self_ref builder, const std::unordered_set& enums) { - for (auto schema_enum_binding : enums) { - // @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); + 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"; + } - // @note: @es3n1n: begin enum class - // - builder.begin_enum_class(schema_enum_binding->m_pszName, get_type_name()); + return type_storage; + }; - // @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: 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()); + + // @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 + // @note: @es3n1n: assemble enum items + // + for (const auto& field : schema_enum_binding.GetEnumeratorValues()) { + // @note: @og: dump enum metadata // - 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)); - } + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; - print_enum_item(field); + if (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(field_metadata.m_szName); + else + builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); } - // @note: @es3n1n: we are done with this enum - // - builder.end_enum_class(); + print_enum_item(field); } + + // @note: @es3n1n: we are done with this enum + // + builder.end_enum_class(); } /// @return {type_name, array_sizes} - auto parse_array(CSchemaType* type) -> std::pair> { - const auto ptr = type->GetRefClass(); - const auto actual_type = ptr ? ptr : type; + std::pair> ParseArray(const CSchemaType& type) { + 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) { + if (actual_type.GetTypeCategory() == ETypeCategory::Schema_FixedArray) { // dump all sizes. - auto schema = reinterpret_cast(actual_type); + auto* schema = reinterpret_cast(&actual_type); while (true) { sizes.emplace_back(schema->m_nElementCount); - schema = reinterpret_cast(schema->m_pElementType); + schema = reinterpret_cast(schema->m_pElementType); if (schema->GetTypeCategory() != ETypeCategory::Schema_FixedArray) { base_type = schema->m_pszName; @@ -304,516 +314,593 @@ namespace sdk { return {base_type, sizes}; }; - /// @return @ref std::nullopt if the type is not contained in a module, e.g. because it is a built-in type - auto get_module(CSchemaType* type) -> std::optional { - if (type->m_pTypeScope != nullptr) { - if (const auto* class_ = type->m_pTypeScope->FindDeclaredClass(type->m_pszName)) { - return class_->m_pszModule; - } else if (const auto* enum_ = type->m_pTypeScope->FindDeclaredEnum(type->m_pszName)) { - return enum_->m_pszModule; - } + /// @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 std::nullopt; - }; + return type_name; + } - /// @return {type_name, array_sizes} where type_name is a fully qualified name - auto get_type(CSchemaType* type) -> std::pair> { - const auto maybe_with_module_name = [type](const auto& type_name) { - return get_module(type) - .transform([&](const auto module_name) { return std::format("{}::{}", module_name, type_name); }) - .value_or(type_name); - }; - const auto [type_name, array_sizes] = parse_array(type); + /// @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; + } + } - assert(type_name.empty() == array_sizes.empty()); + /// @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; + } + }; - if (!type_name.empty() && !array_sizes.empty()) - return {maybe_with_module_name(type_name), array_sizes}; + // TODO: when we have a package manager: use a library + [[nodiscard]] + std::string 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; + } - return {maybe_with_module_name(type->m_pszName), {}}; + /// 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) { + // 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. + const auto escaped_type_name = StringReplace(std::string{type_name}, "::", "__"); + + return GetModuleOfTypeInScope(scope, type_name) + .transform([&](const auto module_name) { return std::format("{}::{}", module_name, escaped_type_name); }) + .value_or(std::string{escaped_type_name}); }; - void AssembleClasses(codegen::generator_t::self_ref builder, const std::unordered_set& 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; + /// Decomposes a templated type into its components, keeping template syntax for later reassembly. + /// 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 AddRefToClass(CSchemaType* type) { - if (type->GetTypeCategory() == ETypeCategory::Schema_DeclaredClass) { - refs_.insert(reinterpret_cast(type)->m_pClassInfo); - } - - // auto ptr = type->GetRefClass(); - // if (ptr && ptr->m_nTypeCategory == Schema_DeclaredClass) - // { - // refs_.insert(ptr->m_pClassInfo); - // return; - // } + if (const auto found = str.find_last_not_of(" "); found != std::string_view::npos) { + str.remove_suffix(str.size() - (found + 1)); } - [[nodiscard]] bool IsDependsOn(const class_t& other) const { - // if current class inherit other. - const auto parent = this->GetParent(); - if (parent == other.target_) - return true; - - // if current class contains ref to other. - if (this->refs_.contains(other.target_)) - return true; + return str; + }; - // otherwise, order doesn`t matter. - return false; - } + /// 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; - [[nodiscard]] SchemaClassFieldData_t* GetFirstField() const { - if (target_->m_nFieldSize) - return &target_->m_pFields[0]; - return nullptr; + 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; + } } - // @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; }; - // @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 split_trim(type_name, "<,>"); + } - for (const auto* schema_class_binding : classes) { - assert(schema_class_binding != nullptr); + /// 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); + } - const auto class_info = schema_class_binding->m_pTypeScope->FindDeclaredClass(schema_class_binding->m_pszName); + // std::cout << "-> " << result << '\n'; - auto& class_dump = classes_to_dump.emplace_back(); - class_dump.target_ = class_info; - } + 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 (auto& class_dump : classes_to_dump) { - const auto class_info = class_dump.target_; + assert(type_name.empty() == array_sizes.empty()); - for (auto k = 0; k < class_info->m_nFieldSize; k++) { - const auto field = &class_info->m_pFields[k]; - if (!field) - continue; + const auto type_name_with_modules = + ReassembleRetypedTemplate(*type.m_pTypeScope, DecomposeTemplate(type_name.empty() ? type.m_pszName : type_name)); - // forward declare all classes. - // @todo: maybe we need to forward declare only pointers to classes? - auto ptr = field->m_pSchemaType->GetRefClass(); + if (!type_name.empty() && !array_sizes.empty()) + return {type_name_with_modules, array_sizes}; - 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; - } + return {type_name_with_modules, {}}; + }; - class_dump.AddRefToClass(field->m_pSchemaType); + /// 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{}; - 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_++; + // 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))); } } - if (did_forward_decls) - builder.next_line(); - - bool did_change = false; - do { - did_change = false; - - // swap until we done. - for (auto first = classes_to_dump.begin(); first != classes_to_dump.end(); ++first) { - bool second_below_first = false; + return result; + } - for (auto second = classes_to_dump.begin(); second != classes_to_dump.end(); ++second) { - if (second == first) { - second_below_first = true; - continue; - } + // TOOD: need to sort these fuctions, they're all over the place. + // TOOD: move local functions into an anonymous namespace - // swap if second class below first, and first depends on second. - bool first_depend = first->IsDependsOn(*second); + // We assume that everything that is not a pointer 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('*'); + } - // swap if first class below second, and second depends on first. - bool second_depend = second->IsDependsOn(*first); + void AssembleClass(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { + // @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"); + + PrintClassInfo(builder, class_info); + + // @note: @es3n1n: get parent name + // + const std::string parent_class_name = + (class_parent != nullptr) ? MaybeWithModuleName(*class_parent->m_pTypeScope, class_parent->m_pszName) : ""; + + // @note: @es3n1n: start class + // + if (is_struct) + builder.begin_struct_with_base_type(class_info.m_pszName, parent_class_name, ""); + else + builder.begin_class_with_base_type(class_info.m_pszName, parent_class_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; + + std::list> cached_fields{}; + std::list cached_datamap_fields{}; + + // @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; + + 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; + + 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; + + // @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 (first_depend && second_depend) { - // classes depends on each other, forward declare them. - // @todo: verify that cyclic dependencies is a pointers. - continue; - } + // @note: @es3n1n: begin public members + // + builder.access_modifier("public"); - if (second_below_first ? first_depend : second_depend) { - std::iter_swap(first, second); - did_change = true; - } - } + 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; } - } while (did_change); - // ================== - for (auto& class_dump : classes_to_dump) { - // @note: @es3n1n: get class info, assemble it + // @note: @es3n1n: obtaining size + // fall back to 1 because there are no 0-sized types // - 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); + const int field_size = field.m_pSchemaType->GetSize().value_or(1); - // @note: @es3n1n: get parent name + // @note: @es3n1n: parsing type // - 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; + 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: start class + // @note: @es3n1n: insert padding if needed // - 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, ""); + 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) { - // @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; + 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"); + } - std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var - } state; + // @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; + } - // @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 + // @note: @es3n1n: if we are done with bitfields we should insert a pad and finish union // - std::optional first_field_offset = std::nullopt; - if (const auto first_field = class_dump.GetFirstField(); first_field) - first_field_offset = first_field->m_nSingleInheritanceOffset; + 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; + + const auto actual_union_size_bits = state.total_bits_count_in_union; - const std::ptrdiff_t parent_class_size = class_parent ? class_parent->m_nSizeOf : 0; + 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)); - 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; + 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); + + state.last_field_offset += expected_union_size_bytes; + state.last_field_size = expected_union_size_bytes; - // @note: @es3n1n: and finally insert a pad + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + + state.total_bits_count_in_union = 0ull; + state.assembling_bitfield = false; + } + + // @note: @es3n1n: dump metadata // - 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(); + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; - // @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 (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(field_metadata.m_szName); + else + builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); } - // @note: @es3n1n: begin public members + // @note: @es3n1n: update state // - builder.access_modifier("public"); + 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; - 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; - } + // @note: @es3n1n: push prop + // + builder.prop(var_info.m_type, var_info.formatted_name(), false); - // @note: @es3n1n: obtaining size - // - const int field_size = field.m_pSchemaType->GetSize().value_or(0); - - // @note: @es3n1n: parsing type - // - const auto [type_name, array_sizes] = get_type(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"); - } + 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: begin union if we're assembling bitfields - // - if (!state.assembling_bitfield && var_info.is_bitfield()) { - builder.begin_bitfield_block(); - state.assembling_bitfield = true; - } + // @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; - // @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; + // @note: @es3n1n: apply 8 bytes align + // + const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); - const auto actual_union_size_bits = state.total_bits_count_in_union; + 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 (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)); + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); - 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); + state.total_bits_count_in_union = 0; + state.assembling_bitfield = false; + } - state.last_field_offset += expected_union_size_bytes; - state.last_field_size = expected_union_size_bytes; + // @note: @es3n1n: dump static fields + // + if (class_info.m_nStaticFieldsSize) { + if (class_info.m_nFieldSize) + builder.next_line(); + builder.comment("Static fields:"); + } - builder.end_bitfield_block(false) - .reset_tabs_count() - .comment(std::format("{:d} bits", expected_union_size_bits)) - .restore_tabs_count(); + // 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()}; - state.total_bits_count_in_union = 0ull; - state.assembling_bitfield = false; - } + for (auto s = 0; s < class_info.m_nStaticFieldsSize; s++) { + auto static_field = &class_info.m_pStaticFields[s]; - // @note: @es3n1n: dump metadata - // - for (auto j = 0; j < field.m_nMetadataSize; j++) { - auto field_metadata = field.m_pMetadata[j]; + 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); + } - 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 (class_info.m_pFieldMetadataOverrides && class_info.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { + const auto& dm = class_info.m_pFieldMetadataOverrides; - // 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_name](const class_t& cls) { return cls.target_->GetName().compare(type_name) == 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)); - } - } - } - } + for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { + auto* t = &dm->m_pTypeDescription[s]; + if (!t) + continue; - // @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(); - } + if (t->GetFieldName().empty()) + continue; - // @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; + const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); - // @note: @es3n1n: apply 8 bytes align - // - const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); + std::string field_type = var_info.m_type; + if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { + field_type = t->m_pDataMap->m_pszClassName; + } - 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); + std::string field_name = var_info.formatted_name(); - builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + // @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; - state.total_bits_count_in_union = 0; - state.assembling_bitfield = false; + cached_datamap_fields.emplace_back(field_type, field_name, t->m_iOffset); } - // @note: @es3n1n: dump static fields - // - if (class_info->m_nStaticFieldsSize) { - if (class_info->m_nFieldSize) + if (!cached_datamap_fields.empty()) { + if (class_info.m_nFieldSize) builder.next_line(); - builder.comment("Static fields:"); + + 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)); + } } + } - // 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()}; + if (!class_info.m_nFieldSize && !class_info.m_nStaticMetadataSize) + builder.comment("No schema binary for binding"); - for (auto s = 0; s < class_info->m_nStaticFieldsSize; s++) { - auto static_field = &class_info->m_pStaticFields[s]; + builder.end_block(); + } + } // namespace - 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, scope_name, class_info->m_pszName, s); - } + enum class TypeSource { + include, + forward_declaration, + }; - if (class_info->m_pFieldMetadataOverrides && class_info->m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { - const auto& dm = class_info->m_pFieldMetadataOverrides; + // TOOD: "dependency" is a misleading term here. repeats for functions. "includes" and "forward declarations" were good terms. + struct TypeDependency { + std::string module{}; - for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { - auto* t = &dm->m_pTypeDescription[s]; - if (!t) - continue; + /// Decayed + std::string type_name{}; - if (t->GetFieldName().empty()) - continue; + TypeSource source{}; - const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); + auto operator<=>(const TypeDependency&) const = default; + }; - std::string field_type = var_info.m_type; - if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { - field_type = t->m_pDataMap->m_pszClassName; - } + /// @return All dependencies to types used by @p type. Returns multiple + /// modules for template types. + [[nodiscard]] + std::set GetRequiredDependenciesForType(const CSchemaType& type) { + // m_pTypeScope can be nullptr for built-in types + if (type.m_pTypeScope != nullptr) { + std::set result{}; - std::string field_name = var_info.formatted_name(); + const auto destructured = ParseTemplateRecursive(type.m_pszName); - // @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; + // 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.cached_datamap_fields_.emplace_back(field_type, field_name, t->m_iOffset); - } + for (const auto& dirty_type_name : destructured) { + const auto type_name = DecayTypeName(dirty_type_name); - if (!class_dump.cached_datamap_fields_.empty()) { - if (class_info->m_nFieldSize) - builder.next_line(); + if (auto module{GetModuleOfTypeInScope(*type.m_pTypeScope, type_name)}) { + const auto source = + (!is_used_in_non_odr_container && IsOdrUse(dirty_type_name)) ? TypeSource::include : TypeSource::forward_declaration; - 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)); - } - } + result.emplace(TypeDependency{.module = std::move(module.value()), .type_name = std::string{type_name}, .source = source}); } - if (!class_info->m_nFieldSize && !class_info->m_nStaticMetadataSize) - builder.comment("No schema binary for binding"); - - builder.end_block(); + is_used_in_non_odr_container = non_odr_containers.contains(type_name); } - } - } // namespace + return result; + } else { + return {}; + } + } - /// @param exclude_module_name will not be contained in the return value - /// @return All modules that are required by properties of @p classes - std::set find_required_modules_of_class(std::string_view exclude_module_name, const CSchemaClassBinding& class_) { - std::set modules{}; + /// @return All modules+types that are required to define @p classes + std::set FindRequiredDependenciesForClass(const CSchemaClassBinding& class_) { + std::set result{}; for (const auto& field : std::span{class_.m_pFields, static_cast(class_.m_nFieldSize)}) { - if (const auto module = get_module(field.m_pSchemaType)) { - modules.emplace(module.value()); - } + const auto includes = GetRequiredDependenciesForType(*field.m_pSchemaType); + result.insert(includes.begin(), includes.end()); } for (const auto& field : std::span{class_.m_pStaticFields, static_cast(class_.m_nStaticFieldsSize)}) { - if (const auto module = get_module(field.m_pSchemaType)) { - modules.emplace(module.value()); - } + const auto includes = GetRequiredDependenciesForType(*field.m_pSchemaType); + result.insert(includes.begin(), includes.end()); } - if (const auto found = modules.find(std::string{exclude_module_name}); found != modules.end()) { - modules.erase(found); + 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 = GetRequiredDependenciesForType(*base_classes[0].m_pClass->m_pSchemaType); + result.insert(includes.begin(), includes.end()); + } + + 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); + }; + + // 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); } - return modules; + return result; } - /// @param exclude_module_name will not be contained in the return value - /// @return All modules that are required by properties of @p classes - std::set find_required_modules(std::string_view exclude_module_name, const std::unordered_set& classes) { - std::set modules{}; + void GenerateEnumSdk(std::string_view module_name, const CSchemaEnumBinding& enum_) { + const std::string out_file_path = std::format("{}/{}/{}.hpp", kOutDirName, module_name, enum_.m_pszName); - for (const auto& class_ : classes) { - assert(class_ != nullptr); + // @note: @es3n1n: init codegen + // + auto builder = codegen::get(); + builder.pragma("once"); - const auto partial = find_required_modules_of_class(exclude_module_name, *class_); - modules.insert(partial.begin(), partial.end()); - } + builder.include(""); - return modules; - } + // @note: @es3n1n: print banner + // + builder.next_line() + .comment("/////////////////////////////////////////////////////////////") + .comment(std::format("Module: {}", module_name)) + .comment(std::string{kCreatedBySource2genMessage}) + .comment("/////////////////////////////////////////////////////////////") + .next_line(); - void GenerateTypeScopeSdk(std::string_view module_name, const std::unordered_set& enums, - const std::unordered_set& classes) { - // @note: @es3n1n: print debug info + builder.begin_namespace(std::format("source2sdk::{}", module_name)); + + // @note: @es3n1n: assemble props // - std::cout << std::format("{}: Assembling {}", __FUNCTION__, module_name) << std::endl; + AssembleEnum(builder, enum_); + + builder.end_namespace(); - // @note: @es3n1n: build file path + // @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, module_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 = 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 (const auto& required_module : find_required_modules(module_name, classes)) { - builder.include(std::format("\"{}.hpp\"", required_module)); + const auto dependencies = FindRequiredDependenciesForClass(class_); + + for (const auto& include : dependencies | std::views::filter([](const auto& el) { return el.source == TypeSource::include; })) { + builder.include(std::format("\"{}/{}.hpp\"", include.module, include.type_name)); } - for (auto&& include_path : include_paths) + // TOOD: make include consistent with GenerateEnumSdk + for (const auto& include_path : include_paths) builder.include(include_path.data()); + for (const auto& forward_declaration : + dependencies | std::views::filter([](const auto& el) { return el.source == TypeSource::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("Module: {}", module_name)) - .comment(std::format("Classes count: {}", classes.size())) - .comment(std::format("Enums count: {}", enums.size())) .comment(std::string{kCreatedBySource2genMessage}) .comment("/////////////////////////////////////////////////////////////") .next_line(); @@ -822,8 +909,7 @@ namespace sdk { // @note: @es3n1n: assemble props // - AssembleEnums(builder, enums); - AssembleClasses(builder, classes); + AssembleClass(builder, class_); builder.end_namespace(); @@ -832,7 +918,7 @@ namespace sdk { 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 @@ -840,6 +926,22 @@ namespace sdk { std::exit(1); } } + + 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/src/tools/field_parser.cpp b/src/tools/field_parser.cpp index 2b89a79..93f917e 100644 --- a/src/tools/field_parser.cpp +++ b/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; From d5971624eba0fb3de70fc55ab4ac834a0654e131 Mon Sep 17 00:00:00 2001 From: Cre3per Date: Sun, 4 Aug 2024 17:37:09 +0200 Subject: [PATCH 4/9] code style and naming --- include/sdk/interfaces/schemasystem/schema.h | 3 +- src/sdk/sdk.cpp | 1081 +++++++++--------- 2 files changed, 536 insertions(+), 548 deletions(-) diff --git a/include/sdk/interfaces/schemasystem/schema.h b/include/sdk/interfaces/schemasystem/schema.h index 2cef438..cb62683 100644 --- a/include/sdk/interfaces/schemasystem/schema.h +++ b/include/sdk/interfaces/schemasystem/schema.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -706,7 +707,7 @@ class CSchemaClassInfo : public SchemaClassInfoData_t { 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; } diff --git a/src/sdk/sdk.cpp b/src/sdk/sdk.cpp index f7d3933..8caf711 100644 --- a/src/sdk/sdk.cpp +++ b/src/sdk/sdk.cpp @@ -15,10 +15,25 @@ #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}; constexpr uint32_t kMaxReferencesForClassEmbed = 2; constexpr std::size_t kMinFieldCountForClassEmbed = 2; @@ -145,689 +160,662 @@ namespace { return value; }; -} // namespace -namespace sdk { - struct cached_datamap_t { - std::string type_; - std::string name_; - std::ptrdiff_t offset_; - }; + 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)); - namespace { - void PrintClassInfo(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_info) { - builder.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"); + 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); - } + 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)); + 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(""); + if (enum_binding.m_nStaticMetadataSize > 0) + builder.comment(""); - for (const auto& metadata : enum_binding.GetStaticMetadata()) { - builder.comment(metadata.m_szName); - } + 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"; - } + 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; - }; + return type_storage; + }; - // @note: @es3n1n: print meta info - // - PrintEnumInfo(builder, schema_enum_binding); + // @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()); + // @note: @es3n1n: begin enum class + // + builder.begin_enum_class(schema_enum_binding.m_pszName, get_type_name()); - // @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: @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 + // @note: @es3n1n: assemble enum items + // + for (const auto& field : schema_enum_binding.GetEnumeratorValues()) { + // @note: @og: dump enum metadata // - 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)); - } + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; - print_enum_item(field); + if (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(field_metadata.m_szName); + else + builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); } - // @note: @es3n1n: we are done with this enum - // - builder.end_enum_class(); + print_enum_item(field); } - /// @return {type_name, array_sizes} - std::pair> ParseArray(const CSchemaType& type) { - const auto* ptr = type.GetRefClass(); - const auto& actual_type = ptr ? *ptr : type; + // @note: @es3n1n: we are done with this enum + // + builder.end_enum_class(); + } - std::string base_type; - std::vector sizes; + /// @return {type_name, array_sizes} + std::pair> ParseArray(const CSchemaType& type) { + const auto* ptr = type.GetRefClass(); + const auto& actual_type = ptr ? *ptr : type; - 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); + std::string base_type; + std::vector sizes; - if (schema->GetTypeCategory() != ETypeCategory::Schema_FixedArray) { - base_type = schema->m_pszName; - break; - } + 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 {base_type, sizes}; - }; - - /// @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 {base_type, sizes}; + }; - return type_name; + /// @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 @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."); + return type_name; + } - 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; - } + /// @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; } + } - /// @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; - } - }; + /// @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; + } + }; + /// 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) { // TODO: when we have a package manager: use a library [[nodiscard]] - std::string StringReplace(std::string str, std::string_view search, std::string_view replace) { + 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. + const auto escaped_type_name = StringReplace(std::string{type_name}, "::", "__"); + + return GetModuleOfTypeInScope(scope, type_name) + .transform([&](const auto module_name) { return std::format("{}::{}", module_name, escaped_type_name); }) + .value_or(std::string{escaped_type_name}); + }; - /// 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) { - // 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. - const auto escaped_type_name = StringReplace(std::string{type_name}, "::", "__"); - - return GetModuleOfTypeInScope(scope, type_name) - .transform([&](const auto module_name) { return std::format("{}::{}", module_name, escaped_type_name); }) - .value_or(std::string{escaped_type_name}); + /// 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{}; + } + + if (const auto found = str.find_last_not_of(" "); found != std::string_view::npos) { + str.remove_suffix(str.size() - (found + 1)); + } + + return str; }; - /// Decomposes a templated type into its components, keeping template syntax for later reassembly. - /// 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); + /// 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; + + 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 { - return std::string_view{}; + if (const auto part = trim(remainder); !part.empty()) { + result.emplace_back(std::string{part}); + } + break; } + } - if (const auto found = str.find_last_not_of(" "); found != std::string_view::npos) { - str.remove_suffix(str.size() - (found + 1)); - } + return result; + }; - return str; - }; + return split_trim(type_name, "<,>"); + } - /// 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; + /// 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{}; - 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); + // 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))); + } + } + + 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 part = trim(remainder); !part.empty()) { - result.emplace_back(std::string{part}); + 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; } - break; } - } + }, + el); + } - return result; - }; + 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); - return split_trim(type_name, "<,>"); - } + assert(type_name.empty() == array_sizes.empty()); - /// Adds module qualifiers and resolves built-in types - std::string ReassembleRetypedTemplate(const CSchemaSystemTypeScope& scope, const std::vector>& decomposed) { - std::string result{}; + const auto type_name_with_modules = + ReassembleRetypedTemplate(*type.m_pTypeScope, DecomposeTemplate(type_name.empty() ? type.m_pszName : type_name)); - 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); - } + if (!type_name.empty() && !array_sizes.empty()) + return {type_name_with_modules, array_sizes}; - // std::cout << "-> " << result << '\n'; + return {type_name_with_modules, {}}; + }; - 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); + // 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('*'); + } - assert(type_name.empty() == array_sizes.empty()); + /// @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{}; - const auto type_name_with_modules = - ReassembleRetypedTemplate(*type.m_pTypeScope, DecomposeTemplate(type_name.empty() ? type.m_pszName : type_name)); + const auto destructured = ParseTemplateRecursive(type.m_pszName); - if (!type_name.empty() && !array_sizes.empty()) - return {type_name_with_modules, array_sizes}; + // 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; - return {type_name_with_modules, {}}; - }; + for (const auto& dirty_type_name : destructured) { + const auto type_name = DecayTypeName(dirty_type_name); - /// 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{}; + 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; - // 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))); + 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); } return result; + } else { + return {}; } + } - // TOOD: need to sort these fuctions, they're all over the place. - // TOOD: move local functions into an anonymous namespace + /// @return All names that are required to define @p classes + std::set GetRequiredNamesForClass(const CSchemaClassBinding& class_) { + std::set result{}; - // We assume that everything that is not a pointer 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('*'); + 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()); } - void AssembleClass(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { - // @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"); - - PrintClassInfo(builder, class_info); - - // @note: @es3n1n: get parent name - // - const std::string parent_class_name = - (class_parent != nullptr) ? MaybeWithModuleName(*class_parent->m_pTypeScope, class_parent->m_pszName) : ""; - - // @note: @es3n1n: start class - // - if (is_struct) - builder.begin_struct_with_base_type(class_info.m_pszName, parent_class_name, ""); - else - builder.begin_class_with_base_type(class_info.m_pszName, parent_class_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; + 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()); + } - std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var - } state; + 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()); + } - std::list> cached_fields{}; - std::list cached_datamap_fields{}; + 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); + }; - // @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; + // 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); + } - 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; + return result; + } - 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; + 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_; + }; - // @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(); + // @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"); - // @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; - } + PrintClassInfo(builder, class_info); - // @note: @es3n1n: begin public members - // - builder.access_modifier("public"); + // @note: @es3n1n: get parent name + // + const std::string parent_class_name = (class_parent != nullptr) ? MaybeWithModuleName(*class_parent->m_pTypeScope, class_parent->m_pszName) : ""; - 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; - } + // @note: @es3n1n: start class + // + if (is_struct) + builder.begin_struct_with_base_type(class_info.m_pszName, parent_class_name, ""); + else + builder.begin_class_with_base_type(class_info.m_pszName, parent_class_name, ""); - // @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"); - } + // @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 union if we're assembling bitfields - // - if (!state.assembling_bitfield && var_info.is_bitfield()) { - builder.begin_bitfield_block(); - state.assembling_bitfield = true; - } + std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var + } state; - // @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::list> cached_fields{}; + std::list cached_datamap_fields{}; - const auto actual_union_size_bits = state.total_bits_count_in_union; + // @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; - 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)); + 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; - 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); + 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; - state.last_field_offset += expected_union_size_bytes; - state.last_field_size = expected_union_size_bytes; + // @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; + } - builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + // @note: @es3n1n: begin public members + // + builder.access_modifier("public"); - state.total_bits_count_in_union = 0ull; - state.assembling_bitfield = false; - } + 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; + } - // @note: @es3n1n: dump metadata - // - for (auto j = 0; j < field.m_nMetadataSize; j++) { - auto field_metadata = field.m_pMetadata[j]; + // @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); - if (auto data = GetMetadataValue(field_metadata); data.empty()) - builder.comment(field_metadata.m_szName); - else - builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); - } + // @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: 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: 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) { - // @note: @es3n1n: push prop - // - builder.prop(var_info.m_type, var_info.formatted_name(), false); + 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"); + } - 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: begin union if we're assembling bitfields + // + if (!state.assembling_bitfield && var_info.is_bitfield()) { + builder.begin_bitfield_block(); + state.assembling_bitfield = true; } - // @note: @es3n1n: if struct ends with union we should end union before ending the class + // @note: @es3n1n: if we are done with bitfields we should insert a pad and finish union // - if (state.assembling_bitfield) { + 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; + const auto actual_union_size_bits = state.total_bits_count_in_union; - // @note: @es3n1n: apply 8 bytes align - // - const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); + 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 (expected_union_size_bits > actual_union_size_bits) + 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); + state.last_field_offset += expected_union_size_bytes; + state.last_field_size = expected_union_size_bytes; + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); - state.total_bits_count_in_union = 0; + state.total_bits_count_in_union = 0ull; state.assembling_bitfield = false; } - // @note: @es3n1n: dump static fields + // @note: @es3n1n: dump metadata // - if (class_info.m_nStaticFieldsSize) { - if (class_info.m_nFieldSize) - builder.next_line(); - builder.comment("Static fields:"); - } - - // 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()}; - - for (auto s = 0; s < class_info.m_nStaticFieldsSize; s++) { - auto static_field = &class_info.m_pStaticFields[s]; + for (auto j = 0; j < field.m_nMetadataSize; j++) { + auto field_metadata = field.m_pMetadata[j]; - 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); + 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 (class_info.m_pFieldMetadataOverrides && class_info.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { - const auto& dm = class_info.m_pFieldMetadataOverrides; + // @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; - for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { - auto* t = &dm->m_pTypeDescription[s]; - if (!t) - continue; + // @note: @es3n1n: push prop + // + builder.prop(var_info.m_type, var_info.formatted_name(), false); - if (t->GetFieldName().empty()) - continue; + 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(); + } - const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); + // @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; - std::string field_type = var_info.m_type; - if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { - field_type = t->m_pDataMap->m_pszClassName; - } + // @note: @es3n1n: apply 8 bytes align + // + const auto expected_union_size_bits = actual_union_size_bits + (actual_union_size_bits % 8); - std::string field_name = var_info.formatted_name(); + 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); - // @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_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); - cached_datamap_fields.emplace_back(field_type, field_name, t->m_iOffset); - } + state.total_bits_count_in_union = 0; + state.assembling_bitfield = false; + } - if (!cached_datamap_fields.empty()) { - if (class_info.m_nFieldSize) - builder.next_line(); + // @note: @es3n1n: dump static fields + // + if (class_info.m_nStaticFieldsSize) { + if (class_info.m_nFieldSize) + builder.next_line(); + builder.comment("Static fields:"); + } - 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)); - } - } - } + // 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()}; - if (!class_info.m_nFieldSize && !class_info.m_nStaticMetadataSize) - builder.comment("No schema binary for binding"); + for (auto s = 0; s < class_info.m_nStaticFieldsSize; s++) { + auto static_field = &class_info.m_pStaticFields[s]; - builder.end_block(); + 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); } - } // namespace - enum class TypeSource { - include, - forward_declaration, - }; + if (class_info.m_pFieldMetadataOverrides && class_info.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { + const auto& dm = class_info.m_pFieldMetadataOverrides; - // TOOD: "dependency" is a misleading term here. repeats for functions. "includes" and "forward declarations" were good terms. - struct TypeDependency { - std::string module{}; - - /// Decayed - std::string type_name{}; + for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { + auto* t = &dm->m_pTypeDescription[s]; + if (!t) + continue; - TypeSource source{}; + if (t->GetFieldName().empty()) + continue; - auto operator<=>(const TypeDependency&) const = default; - }; + const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); - /// @return All dependencies to types used by @p type. Returns multiple - /// modules for template types. - [[nodiscard]] - std::set GetRequiredDependenciesForType(const CSchemaType& type) { - // m_pTypeScope can be nullptr for built-in types - if (type.m_pTypeScope != nullptr) { - std::set result{}; + std::string field_type = var_info.m_type; + if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { + field_type = t->m_pDataMap->m_pszClassName; + } - const auto destructured = ParseTemplateRecursive(type.m_pszName); + std::string field_name = var_info.formatted_name(); - // 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; + // @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; - for (const auto& dirty_type_name : destructured) { - const auto type_name = DecayTypeName(dirty_type_name); + cached_datamap_fields.emplace_back(field_type, field_name, t->m_iOffset); + } - if (auto module{GetModuleOfTypeInScope(*type.m_pTypeScope, type_name)}) { - const auto source = - (!is_used_in_non_odr_container && IsOdrUse(dirty_type_name)) ? TypeSource::include : TypeSource::forward_declaration; + if (!cached_datamap_fields.empty()) { + if (class_info.m_nFieldSize) + builder.next_line(); - result.emplace(TypeDependency{.module = std::move(module.value()), .type_name = std::string{type_name}, .source = source}); + 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)); } - - is_used_in_non_odr_container = non_odr_containers.contains(type_name); } - - return result; - } else { - return {}; } - } - /// @return All modules+types that are required to define @p classes - std::set FindRequiredDependenciesForClass(const CSchemaClassBinding& class_) { - std::set result{}; + if (!class_info.m_nFieldSize && !class_info.m_nStaticMetadataSize) + builder.comment("No schema binary for binding"); - for (const auto& field : std::span{class_.m_pFields, static_cast(class_.m_nFieldSize)}) { - const auto includes = GetRequiredDependenciesForType(*field.m_pSchemaType); - result.insert(includes.begin(), includes.end()); - } - - for (const auto& field : std::span{class_.m_pStaticFields, static_cast(class_.m_nStaticFieldsSize)}) { - const auto includes = GetRequiredDependenciesForType(*field.m_pSchemaType); - result.insert(includes.begin(), includes.end()); - } - - 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 = GetRequiredDependenciesForType(*base_classes[0].m_pClass->m_pSchemaType); - result.insert(includes.begin(), includes.end()); - } - - 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); - }; - - // 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); - } - - return result; + builder.end_block(); } void GenerateEnumSdk(std::string_view module_name, const CSchemaEnumBinding& enum_) { @@ -879,18 +867,15 @@ namespace sdk { auto builder = codegen::get(); builder.pragma("once"); - const auto dependencies = FindRequiredDependenciesForClass(class_); + const auto names = GetRequiredNamesForClass(class_); - for (const auto& include : dependencies | std::views::filter([](const auto& el) { return el.source == TypeSource::include; })) { + for (const auto& include : names | std::views::filter([](const auto& el) { return el.source == NameSource::include; })) { builder.include(std::format("\"{}/{}.hpp\"", include.module, include.type_name)); } - // TOOD: make include consistent with GenerateEnumSdk - for (const auto& include_path : include_paths) - builder.include(include_path.data()); + builder.include(""); - for (const auto& forward_declaration : - dependencies | std::views::filter([](const auto& el) { return el.source == TypeSource::forward_declaration; })) { + 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(); @@ -926,7 +911,9 @@ 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 From eb65386544a612a7398e8232d9aa3f1c165151a7 Mon Sep 17 00:00:00 2001 From: Arsenii es3n1n Date: Tue, 27 Aug 2024 21:42:48 +0200 Subject: [PATCH 5/9] build: fix compilation on windows --- source2gen/include/sdk/interfaces/tier0/IMemAlloc.h | 1 + source2gen/include/tools/codegen.h | 8 ++++---- source2gen/include/tools/field_parser.h | 1 + source2gen/src/startup/startup.cpp | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h b/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h index 75c30bb..b6f4a28 100644 --- a/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h +++ b/source2gen/include/sdk/interfaces/tier0/IMemAlloc.h @@ -7,6 +7,7 @@ #include "tools/platform.h" #include "tools/virtual.h" +#include class IMemAlloc { public: diff --git a/source2gen/include/tools/codegen.h b/source2gen/include/tools/codegen.h index 5800532..adc7d2a 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 @@ -183,8 +184,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); } @@ -293,8 +294,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 67de5dc..27cad56 100644 --- a/source2gen/include/tools/field_parser.h +++ b/source2gen/include/tools/field_parser.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include diff --git a/source2gen/src/startup/startup.cpp b/source2gen/src/startup/startup.cpp index a29ded3..4113d53 100644 --- a/source2gen/src/startup/startup.cpp +++ b/source2gen/src/startup/startup.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include From ad1ef4395f70a5919b91290601ec1fd3c520bd68 Mon Sep 17 00:00:00 2001 From: Arsenii es3n1n Date: Tue, 27 Aug 2024 21:49:29 +0200 Subject: [PATCH 6/9] fix: escape paths on windows --- source2gen/include/tools/util.h | 11 +++++++++++ source2gen/src/sdk/sdk.cpp | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/source2gen/include/tools/util.h b/source2gen/include/tools/util.h index 0bf3bd5..0cc265d 100644 --- a/source2gen/include/tools/util.h +++ b/source2gen/include/tools/util.h @@ -4,6 +4,7 @@ #include #include +#include namespace util { inline std::string PrettifyNum(int num) { @@ -19,6 +20,16 @@ namespace util { return std::to_string(num); } + + [[nodiscard]] inline std::string EscapePath(std::string_view path) { +#if TARGET_OS == WINDOWS + std::string result(path); + std::ranges::replace(result, ':', '_'); + return result; +#else + return std::string(path); +#endif + } } // namespace util // source2gen - Source2 games SDK generator diff --git a/source2gen/src/sdk/sdk.cpp b/source2gen/src/sdk/sdk.cpp index 8caf711..fbb56e2 100644 --- a/source2gen/src/sdk/sdk.cpp +++ b/source2gen/src/sdk/sdk.cpp @@ -819,7 +819,7 @@ namespace { } void GenerateEnumSdk(std::string_view module_name, const CSchemaEnumBinding& enum_) { - const std::string out_file_path = std::format("{}/{}/{}.hpp", kOutDirName, module_name, enum_.m_pszName); + const std::string out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, enum_.m_pszName)); // @note: @es3n1n: init codegen // @@ -860,7 +860,7 @@ namespace { } void GenerateClassSdk(std::string_view module_name, const CSchemaClassBinding& class_) { - const std::filesystem::path out_file_path = std::format("{}/{}/{}.hpp", kOutDirName, module_name, class_.m_pszName); + const std::filesystem::path out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, class_.m_pszName)); // @note: @es3n1n: init codegen // @@ -870,7 +870,7 @@ namespace { const auto names = GetRequiredNamesForClass(class_); for (const auto& include : names | std::views::filter([](const auto& el) { return el.source == NameSource::include; })) { - builder.include(std::format("\"{}/{}.hpp\"", include.module, include.type_name)); + builder.include(std::format("\"{}/{}.hpp\"", include.module, util::EscapePath(include.type_name))); } builder.include(""); From 7f3cb902d535defc3d64c212afc18efdd81a1c98 Mon Sep 17 00:00:00 2001 From: Arsenii es3n1n Date: Tue, 27 Aug 2024 22:12:46 +0200 Subject: [PATCH 7/9] refactor(codegen): adjust newlines --- source2gen/include/tools/codegen.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source2gen/include/tools/codegen.h b/source2gen/include/tools/codegen.h index adc7d2a..c9627f0 100644 --- a/source2gen/include/tools/codegen.h +++ b/source2gen/include/tools/codegen.h @@ -125,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; } @@ -210,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 // From f02cdebd0cc5461ca039132b6195afae61928300 Mon Sep 17 00:00:00 2001 From: Arsenii es3n1n Date: Tue, 27 Aug 2024 22:13:04 +0200 Subject: [PATCH 8/9] refactor(sdk): escape type names when starting a new struct --- source2gen/src/sdk/sdk.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/source2gen/src/sdk/sdk.cpp b/source2gen/src/sdk/sdk.cpp index fbb56e2..29c58f2 100644 --- a/source2gen/src/sdk/sdk.cpp +++ b/source2gen/src/sdk/sdk.cpp @@ -359,10 +359,8 @@ namespace { } }; - /// 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) { + [[nodiscard]] std::string EscapeTypeName(const std::string_view type_name) { // TODO: when we have a package manager: use a library - [[nodiscard]] 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) { @@ -377,11 +375,15 @@ namespace { // "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. - const auto escaped_type_name = StringReplace(std::string{type_name}, "::", "__"); + return StringReplace(std::string{type_name}, "::", "__"); + } + /// 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(std::string{escaped_type_name}); + .value_or(escaped_type_name); }; /// Decomposes a templated type into its components, keeping template @@ -587,9 +589,9 @@ namespace { // @note: @es3n1n: start class // if (is_struct) - builder.begin_struct_with_base_type(class_info.m_pszName, parent_class_name, ""); + builder.begin_struct_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); else - builder.begin_class_with_base_type(class_info.m_pszName, parent_class_name, ""); + builder.begin_class_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); // @note: @es3n1n: field assembling state // From aab95b630a01e99756d36ccd6e0a25ff3b5bc282 Mon Sep 17 00:00:00 2001 From: Cre3per Date: Fri, 30 Aug 2024 15:01:10 +0200 Subject: [PATCH 9/9] escape path on all platforms --- source2gen/include/tools/util.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source2gen/include/tools/util.h b/source2gen/include/tools/util.h index 0cc265d..aa7d322 100644 --- a/source2gen/include/tools/util.h +++ b/source2gen/include/tools/util.h @@ -2,9 +2,10 @@ // See end of file for extended copyright information. #pragma once +#include #include +#include #include -#include namespace util { inline std::string PrettifyNum(int num) { @@ -22,13 +23,9 @@ namespace util { } [[nodiscard]] inline std::string EscapePath(std::string_view path) { -#if TARGET_OS == WINDOWS std::string result(path); std::ranges::replace(result, ':', '_'); return result; -#else - return std::string(path); -#endif } } // namespace util