diff --git a/README.md b/README.md index 1c02d43..87e45bb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ source2gen-loader.exe ```sh ./scripts/run.sh "$HOME/.steam/steam/steamapps/cs2/" +cp -r ./sdk-static/* ./sdk # view generated sdk ls ./sdk ``` @@ -37,7 +38,32 @@ errors, bugs, and wrong output. Please only file issues if you want to work on them. This note will be removed once we have thoroughly tested Source2Gen on Linux. -## Getting Started +### Using the generated SDK + +The sdk depends on a file/module called `source2gen.hpp`. This file has +to be provided by the user and expose all types listed in +[source2gen.hpp](sdk-static/include/source2sdk/source2gen.hpp). If you don't +intend to access any of these types, you can use the dummy file +[source2gen.hpp](sdk-static/include/source2sdk/source2gen.hpp). + +## Limitations + +### Disabled entities + +Under the following conditions, entities are either entirely omitted, or emitted +as a comment and replaced with a dummy: + +- Overlapping fields: Fields that share memory with another field +- Misaligned fields: Fields that cannot be placed at the correct in-class offset + because of their type's alignment requirements +- Misaligned types: Class types that would exceed their correct size because + padding bytes would have to be inserted to meet alignment requirements +- Fields with template types + +Some of these disabled entities can be made to work by using compiler-specific +attributes. + +## Getting Started with Development These instructions will help you set up the project on your local machine for development and testing purposes. diff --git a/scripts/run.sh b/scripts/run.sh index ff8ae1b..8876532 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -7,6 +7,19 @@ SCRIPT_DIRECTORY="$(dirname "$(readlink -f "$0")")" PROJECT_ROOT="${SCRIPT_DIRECTORY}/../" BINARY="" +find_second_bin_directory() { + local game_path="$1" + + local found;found="$(find "${game_path}" -name libclient.so)" + + if [ -z "${found}" ]; then + echo "Error: unable to find second bin directory" >&2 + exit 1 + else + dirname "${found}" + fi +} + if [ -z "${GAME_DIRECTORY}" ]; then echo "usage: run.sh " echo "eg : run.sh $HOME/.steam/steam/steamapps/cs2/" @@ -28,23 +41,14 @@ done if [ -z "${BINARY}" ]; then echo "source2gen binary not found. set LD_PRELOAD_PATH and run source2gen by hand." exit 1 +else + FIRST_BIN_DIRECTORY="${GAME_DIRECTORY}/game/bin/linuxsteamrt64/" + SECOND_BIN_DIRECTORY=$(find_second_bin_directory "$GAME_DIRECTORY") + export LD_LIBRARY_PATH="${FIRST_BIN_DIRECTORY}:${SECOND_BIN_DIRECTORY}:${LD_LIBRARY_PATH:-}" + set -x + if [ -z "${DEBUGGER:-}" ]; then + "${BINARY}" + else + "${DEBUGGER}" -- "${BINARY}" + fi fi - -find_second_bin_directory() { - local game_path="$1" - - local found;found="$(find "${game_path}" -name libclient.so)" - - if [ -z "${found}" ]; then - echo "Error: unable to find second bin directory" >&2 - exit 1 - else - dirname "${found}" - fi -} - -FIRST_BIN_DIRECTORY="${GAME_DIRECTORY}/game/bin/linuxsteamrt64/" -SECOND_BIN_DIRECTORY=$(find_second_bin_directory "$GAME_DIRECTORY") - -set -x -LD_LIBRARY_PATH="${FIRST_BIN_DIRECTORY}:${SECOND_BIN_DIRECTORY}:${LD_LIBRARY_PATH:-}" "${BINARY}" diff --git a/sdk-static/CMakeLists.txt b/sdk-static/CMakeLists.txt new file mode 100644 index 0000000..174b823 --- /dev/null +++ b/sdk-static/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.30) + +set(CMAKE_EXPORT_COMPILE_COMMANDS On) + +project(source2sdk + LANGUAGES CXX +) + +file(GLOB_RECURSE source2sdk_headers "./**.hpp") + +add_library(${PROJECT_NAME} INTERFACE) +target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${source2sdk_headers}") + +set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX) + +install( + DIRECTORY "${CMAKE_SOURCE_DIR}/include" + DESTINATION . +) + +# Add a target that includes all headers of the generated library to check for compile-time errors + +foreach(el ${source2sdk_headers}) + string(APPEND generated_cpp_contents "#include \"${el}\"\n") +endforeach() + +set(generated_cpp_file "${CMAKE_BINARY_DIR}/all_headers.cpp") + +file(WRITE ${generated_cpp_file} ${generated_cpp_contents}) + +add_library(${PROJECT_NAME}-compile-test ${generated_cpp_file}) +set_target_properties(${PROJECT_NAME}-compile-test PROPERTIES LINKER_LANGUAGE CXX) +target_link_libraries(${PROJECT_NAME}-compile-test ${PROJECT_NAME}) + +target_compile_options(${PROJECT_NAME}-compile-test PRIVATE + "-Wfatal-errors" + "-pedantic-errors" +) diff --git a/sdk-static/conanfile.py b/sdk-static/conanfile.py new file mode 100644 index 0000000..60d309a --- /dev/null +++ b/sdk-static/conanfile.py @@ -0,0 +1,51 @@ +from conan import ConanFile +from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps + + +# TODO: We should set `version` and `name` to reflect what game this sdk is for +class source2sdkRecipe(ConanFile): + name = "source2sdk" + version = "0.0.0" + package_type = "library" + + author = "source2gen" + url = "https://github.com/neverlosecc/source2gen" + description = "Source2 SDK" + topics = ("source2",) + + settings = "os", "compiler", "build_type", "arch" + options = {"shared": [True, False], "fPIC": [True, False]} + default_options = {"shared": False, "fPIC": True} + + exports_sources = "CMakeLists.txt", "include/*" + + def config_options(self): + if self.settings.os == "Windows": + self.options.rm_safe("fPIC") + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + + def layout(self): + cmake_layout(self) + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.bindirs = [] + self.cpp_info.libdirs = [] + diff --git a/sdk-static/include/source2sdk/source2gen.hpp b/sdk-static/include/source2sdk/source2gen.hpp new file mode 100644 index 0000000..539bf9b --- /dev/null +++ b/sdk-static/include/source2sdk/source2gen.hpp @@ -0,0 +1,149 @@ +#pragma once +#include +#include + +template +using CAnimValue = char[0x08]; +using CAnimVariant = char[0x11]; +// size is a guess +template +using CAnimScriptParam = char[0x08]; +using CBufferString = char[0x10]; +using CColorGradient = char[0x18]; +// size doesn't matter. only used as a pointer +template +using CCompressor = char[0x01]; +using CEntityHandle = char[0x04]; +using CEntityIndex = char[0x04]; +using CGlobalSymbol = char[0x08]; +using CKV3MemberNameWithStorage = char[0x38]; +using CNetworkedQuantizedFloat = char[0x08]; +using CParticleNamedValueRef = char[0x40]; +using CPiecewiseCurve = char[0x40]; +using CPlayerSlot = char[0x04]; +using CPulseValueFullType = char[0x10]; +using CResourceName = char[0xe0]; +using CSplitScreenSlot = char[0x04]; +using CTransform = char[0x20]; +using CUtlBinaryBlock = char[0x18]; +template +using CUtlHashtable = char[0x20]; +using CUtlStringTokenWithStorage = char[0x18]; +using CUtlStringToken = char[0x04]; +using CUtlString = char[0x08]; +using CUtlSymbolLarge = char[0x08]; +using CUtlSymbol = char[0x02]; +template +using CAnimGraphParamOptionalRef = char[0x20]; +template +using CAnimGraphParamRef = char[0x20]; +template +using CBitVec = char[(N + 7) / 8]; +template +using CEntityOutputTemplate = char[0x28]; +template +using CHandle = char[0x04]; +template +using C_NetworkUtlVectorBase = char[0x18]; +template +using CNetworkUtlVectorBase = char[0x18]; +// size unknown. only used in dynamic containers. +using CSoundEventName = char[0x01]; +template +using CUtlLeanVector = char[0x10]; +template +using CUtlOrderedMap = char[0x28]; +// size doesn't matter. only used as a pointer +template +using CUtlPair = char[0x01]; +template +using CUtlVector = char[0x18]; +// size is a guess that fits both occurences of this type in CS2 +template +using CUtlVectorFixedGrowable = char[0x18 + ((sizeof(Ty) < 4) ? 4 : sizeof(Ty))]; +template +using CUtlLeanVectorFixedGrowable = char[0x10 + ((sizeof(Ty) < 4) ? 4 : sizeof(Ty))]; +template +using C_UtlVectorEmbeddedNetworkVar = char[0x50]; +template +using CUtlVectorEmbeddedNetworkVar = char[0x50]; +using CUtlVectorSIMDPaddedVector = char[0x18]; +template +using CSmartPtr = char[0x08]; +template +using CResourceArray = char[0x08]; +// size unknown +using CResourceString = char[0x08]; +template +using CResourcePointer = char[0x08]; +template +using CResourceNameTyped = char[0xe0]; +template +using CStrongHandle = char[0x08]; +template +using CStrongHandleCopyable = char[0x08]; +// size doesn't matter. only used as a pointer +using CStrongHandleVoid = char[0x08]; +template +using CVariantBase = char[0x10]; +template +using CWeakHandle = char[0x18]; +using CSmartPropAttributeVector = char[0x40]; +using CSmartPropAttributeFloat = char[0x40]; +using CSmartPropAttributeBool = char[0x40]; +using CSmartPropAttributeColor = char[0x40]; +using CSmartPropAttributeInt = char[0x40]; +using CSmartPropAttributeModelName = char[0x40]; +using CSmartPropAttributeMaterialGroup = char[0x40]; +using CSmartPropAttributeVector2D = char[0x40]; +using CSmartPropVariableComparison = char[0x20]; +using CSmartPropAttributeAngles = char[0x40]; +using CSmartPropAttributeStateName = char[0x40]; +using CSmartPropAttributeVariableValue = char[0x40]; +using Color = char[0x04]; +using DegreeEuler = char[0x0c]; +using FourVectors = char[0x30]; +using HSCRIPT = char[0x08]; +using KeyValues3 = char[0x10]; +// size doesn't matter. only used as a pointer +using KeyValues = char[0x01]; +using QAngle = char[0x0c]; +using QuaternionStorage = char[0x10]; +using Quaternion = char[0x10]; +using RadianEuler = char[0x0c]; +using RenderInputLayoutField_t = char[0x04]; +// we don't have a field size for this type. uses the fallback of 1. +using RenderPrimitiveType_t = char[0x01]; +using RotationVector = char[0x0c]; +template +using SphereBase_t = char[0x10]; +using Vector2D = char[0x08]; +using Vector4D = char[0x10]; +using VectorAligned = char[0x10]; +using Vector = char[0x0c]; +using WorldGroupId_t = char[0x04]; +using float32 = char[0x04]; +using fltx4 = char[0x10]; +using matrix3x4_t = char[0x30]; +using matrix3x4a_t = char[0x30]; + +// intentionally left undefined. if you want to access static fields, add your own sdk. +namespace interfaces { + struct SchemaStaticFieldData_t { + void* m_pInstance{}; + }; + + struct CSchemaClassInfo { + auto GetStaticFields() -> SchemaStaticFieldData_t**; + }; + + struct CSchemaSystemTypeScope { + auto FindDeclaredClass(std::string_view) -> CSchemaClassInfo*; + }; + + struct schema_t { + auto FindTypeScopeForModule(std::string_view) -> CSchemaSystemTypeScope*; + }; + + extern schema_t* g_schema; +} // namespace interfaces diff --git a/source2gen/include/sdk/interfaces/common/CUtlTSHash.h b/source2gen/include/sdk/interfaces/common/CUtlTSHash.h index 2b77396..647cd3b 100644 --- a/source2gen/include/sdk/interfaces/common/CUtlTSHash.h +++ b/source2gen/include/sdk/interfaces/common/CUtlTSHash.h @@ -2,10 +2,13 @@ // See end of file for extended copyright information. #pragma once #include "sdk/interfaces/common/CThreadSpinMutex.h" +#include "sdk/interfaces/common/CThreadSpinRWLock.h" #include "sdk/interfaces/common/CUtlMemory.h" #include "sdk/interfaces/common/CUtlMemoryPoolBase.h" +#include #include #include +#include #if defined(CS2) || defined(DOTA2) || defined(DEADLOCK) constexpr auto kUtlTsHashVersion = 2; @@ -273,7 +276,7 @@ inline std::vector CUtlTSHashV2::merge_wi std::vector merged_list = allocated_list; for (const auto& item : un_allocated_list) { - if (std::find_if(allocated_list.begin(), allocated_list.end(), [&](const T& elem) { return pred(elem, item); }) == allocated_list.end()) { + if (std::ranges::find_if(allocated_list, [&](const T& elem) { return pred(elem, item); }) == allocated_list.end()) { merged_list.push_back(item); } } diff --git a/source2gen/include/sdk/interfaces/schemasystem/schema.h b/source2gen/include/sdk/interfaces/schemasystem/schema.h index d46a052..14773a8 100644 --- a/source2gen/include/sdk/interfaces/schemasystem/schema.h +++ b/source2gen/include/sdk/interfaces/schemasystem/schema.h @@ -2,14 +2,20 @@ // See end of file for extended copyright information. #pragma once +#include "sdk/interfaceregs.h" #include "tools/platform.h" +#include #include #include #include +#include +#include #include #include #include #include +#include +#include #include #include @@ -134,8 +140,8 @@ enum { kSchemaType_GetSizeWithAlignOf = 3, kSchemaSystem_ValidateClasses = 35, kSchemaSystem_GetClassInfoBinaryName = 22, - kSchemaSystem_GetClassProjectName = kSchemaSystem_GetClassInfoBinaryName + 1, - kSchemaSystem_GetEnumBinaryName = kSchemaSystem_GetClassProjectName + 1, + kSchemaSystem_GetClassModuleName = kSchemaSystem_GetClassInfoBinaryName + 1, + kSchemaSystem_GetEnumBinaryName = kSchemaSystem_GetClassModuleName + 1, kSchemaSystem_GetEnumProjectName = kSchemaSystem_GetEnumBinaryName + 1, kSchemaSystemTypeScope_DeclaredClass = 14, kSchemaSystemTypeScope_DeclaredEnum = kSchemaSystemTypeScope_DeclaredClass + 1, @@ -166,7 +172,7 @@ enum { }; #else - #error No implementation defined, please re-generate project with premake5 + #error No implementation defined, please set SOURCE2GEN_GAME and re-generate the project #endif class ISaveRestoreOps; @@ -344,6 +350,9 @@ enum class SchemaBuiltinType_t : std::uint32_t { constexpr auto kSchemaBuiltinTypeCount = static_cast(SchemaBuiltinType_t::Schema_Builtin_count); +class CSchemaType_DeclaredClass; +class CSchemaType_DeclaredEnum; + class CSchemaType { public: [[nodiscard]] bool IsValid() { @@ -361,8 +370,8 @@ class CSchemaType { } // @note: @og: gets size with align - [[nodiscard]] bool GetSizeWithAlignOf(int* nOutSize, std::uint8_t* unOutAlign) { - return reinterpret_cast(vftable[kSchemaType_GetSizeWithAlignOf])(this, nOutSize, unOutAlign); + [[nodiscard]] bool GetSizeWithAlignOf(int* nOutSize, std::uint8_t* unOutAlign) const { + return reinterpret_cast(vftable[kSchemaType_GetSizeWithAlignOf])(this, nOutSize, unOutAlign); } [[nodiscard]] bool CanReinterpretAs(CSchemaType* pType) { @@ -375,13 +384,25 @@ class CSchemaType { } public: - // @note: @og: wrapper around GetSizes, this one gets CSchemaClassInfo->m_nSizeOf + // @note: @og: wrapper around GetSizeWithAlignOf, this one gets CSchemaClassInfo->m_nSizeOf [[nodiscard]] std::optional GetSize() { - std::uint8_t align_of = 0; - int result = 0; - return GetSizeWithAlignOf(&result, &align_of) ? std::make_optional(result) : std::nullopt; + return GetSizeAndAlignment().transform([](auto e) { return std::get<0>(e); }); } + /// @return {size, alignment} + [[nodiscard]] std::optional>> GetSizeAndAlignment() const { + std::uint8_t alignment = 0; + int size = 0; + + return GetSizeWithAlignOf(&size, &alignment) ? + std::make_optional(std::make_pair(size, (alignment == 0xff) ? std::nullopt : std::make_optional(static_cast(alignment)))) : + std::nullopt; + } + + /// @return @ref nullptr if this @ref GetTypeCategory() is not @ref ETypeCategory::Schema_DeclaredClass + const CSchemaType_DeclaredClass* GetAsDeclaredClass() const; + const CSchemaType_DeclaredEnum* GetAsDeclaredEnum() const; + // @todo: @og: find out to what class pointer points. [[nodiscard]] CSchemaType* GetRefClass() const; @@ -448,10 +469,14 @@ static_assert(sizeof(CSchemaType_Builtin) == 0x28); class CSchemaType_DeclaredClass : public CSchemaType { public: + /// never @ref nullptr CSchemaClassInfo* m_pClassInfo; bool m_bGlobalPromotionRequired; }; +static_assert(offsetof(CSchemaType_DeclaredClass, m_pClassInfo) == 0x20); +static_assert(sizeof(CSchemaType_DeclaredClass) == 0x30); + class CSchemaType_DeclaredEnum : public CSchemaType { public: CSchemaEnumBinding* m_pClassInfo; @@ -499,6 +524,8 @@ class CSchemaType_Atomic_CollectionOfT : public CSchemaType_Atomic_T { std::uint16_t m_unElementSize; }; +static_assert(offsetof(CSchemaType_Atomic_CollectionOfT, m_pFn) == 0x38); + class CSchemaType_Atomic_TF : public CSchemaType_Atomic_T { public: int m_nFuncPtrSize; @@ -532,6 +559,22 @@ class CSchemaType_FixedArray : public CSchemaType { CSchemaType* m_pElementType; }; +inline const CSchemaType_DeclaredClass* CSchemaType::GetAsDeclaredClass() const { + if (GetTypeCategory() == ETypeCategory::Schema_DeclaredClass) { + return static_cast(this); + } else { + return nullptr; + } +} + +inline const CSchemaType_DeclaredEnum* CSchemaType::GetAsDeclaredEnum() const { + if (GetTypeCategory() == ETypeCategory::Schema_DeclaredEnum) { + return static_cast(this); + } else { + return nullptr; + } +} + struct AtomicTypeInfo_T_t { int m_nAtomicID; CSchemaType* m_pTemplateType; @@ -570,6 +613,8 @@ struct SchemaClassFieldData_t { SchemaMetadataEntryData_t* m_pMetadata; // 0x0018 }; +static_assert(sizeof(SchemaClassFieldData_t) == 0x20); + struct SchemaStaticFieldData_t { const char* m_pszName; // 0x0000 CSchemaType* m_pSchemaType; // 0x0008 @@ -612,8 +657,9 @@ struct SchemaClassInfoData_t { std::uint8_t m_unAlignOf; // 0x0022 std::int8_t m_nBaseClassSize; // 0x0023 - std::int16_t - m_nMultipleInheritanceDepth; // 0x0024 // @note: @og: if there is no derived or base class, then it will be 1 otherwise derived class size + 1. + + // @note: @og: if there is no derived or base class, then it will be 1 otherwise derived class size + 1. + std::int16_t m_nMultipleInheritanceDepth; // 0x0024 std::int16_t m_nSingleInheritanceDepth; // 0x0026 SchemaClassFieldData_t* m_pFields; // 0x0028 @@ -707,8 +753,9 @@ class CSchemaClassInfo : public SchemaClassInfoData_t { return m_nSizeOf; } - [[nodiscard]] std::uint8_t GetAlignment() const { - return m_unAlignOf == std::numeric_limits::max() ? 8 : m_unAlignOf; + /// @return Alignment as registered in the game + [[nodiscard]] std::optional GetRegisteredAlignment() const { + return m_unAlignOf == std::numeric_limits::max() ? std::nullopt : std::make_optional(static_cast(m_unAlignOf)); } // @note: @og: Copy instance from original to new created with all data from original, returns new_instance @@ -953,9 +1000,9 @@ class CSchemaSystem { } } - [[nodiscard]] std::string ScopedNameForClass(CSchemaClassBinding* pBinding) { + [[nodiscard]] std::string ScopedNameForClass(const CSchemaClassBinding* pBinding) { static CBufferStringGrowable<1024> szBuf; - Virtual::Get(this, 17)(this, pBinding, &szBuf); + Virtual::Get(this, 17)(this, pBinding, &szBuf); return szBuf.Get(); } @@ -971,7 +1018,7 @@ class CSchemaSystem { } } - [[nodiscard]] std::string GetScopedNameForEnum(CSchemaEnumBinding* pBinding) { + [[nodiscard]] std::string ScopedNameForEnum(CSchemaEnumBinding* pBinding) { static CBufferStringGrowable<1024> szBuf; Virtual::Get(this, 19)(this, pBinding, &szBuf); return szBuf.Get(); @@ -981,16 +1028,17 @@ class CSchemaSystem { return Virtual::Get(this, kSchemaSystem_GetClassInfoBinaryName)(this, pBinding); } - [[nodiscard]] const char* GetClassProjectName(CSchemaClassBinding* pBinding) { - return Virtual::Get(this, kSchemaSystem_GetClassProjectName)(this, pBinding); + [[nodiscard]] const char* GetClassModuleName(CSchemaClassBinding* pBinding) { + // Returns pBinding->m_pszModule + return Virtual::Get(this, kSchemaSystem_GetClassModuleName)(this, pBinding); } [[nodiscard]] const char* GetEnumBinaryName(CSchemaEnumBinding* pBinding) { return Virtual::Get(this, kSchemaSystem_GetEnumBinaryName)(this, pBinding); } - [[nodiscard]] const char* GetEnumProjectName(CSchemaEnumBinding* pBinding) { - return Virtual::Get(this, kSchemaSystem_GetEnumProjectName)(this, pBinding); + [[nodiscard]] const char* GetEnumProjectName(const CSchemaEnumBinding* pBinding) { + return Virtual::Get(this, kSchemaSystem_GetEnumProjectName)(this, pBinding); } CSchemaClassBinding* ValidateClasses(CSchemaClassBinding** ppBinding) { @@ -1028,8 +1076,8 @@ class CSchemaSystem { private: char pad_0x0000[kSchemaSystem_PAD0] = {}; // 0x0000 - CUtlVector m_TypeScopes = {}; // SCHEMASYSTEM_TYPE_SCOPES_OFFSET - char pad_01A0[kSchemaSystem_PAD1] = {}; // 0x01A0 + CUtlVector m_TypeScopes = {}; // linux: 0x01F0 + char pad_0x01A0[kSchemaSystem_PAD1] = {}; // 0x01A0 std::int32_t m_nRegistrations = 0; // 0x02C0 std::int32_t m_nIgnored = 0; // 0x02C4 std::int32_t m_nRedundant = 0; // 0x02C8 diff --git a/source2gen/include/sdk/sdk.h b/source2gen/include/sdk/sdk.h index f06b92c..e3057d1 100644 --- a/source2gen/include/sdk/sdk.h +++ b/source2gen/include/sdk/sdk.h @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -28,7 +29,24 @@ namespace sdk { inline CSchemaSystem* g_schema = nullptr; - void GenerateTypeScopeSdk(std::string_view module_name, const std::unordered_set& enums, + /// Unique identifier for a type in the source2 engine + struct TypeIdentifier { + std::string module{}; + std::string name{}; + + auto operator<=>(const TypeIdentifier&) const = default; + }; + + /// Stores results of expensive function calls, like those that recurse through classes. + struct GeneratorCache { + /// Key is {module,class} + /// If an entry exists for a class, but its value is @ref std::nullopt, we have already tried finding its alignment but couldn't figure it out. + std::map> class_alignment{}; + /// Key is {module,class} + std::map class_has_standard_layout{}; + }; + + void GenerateTypeScopeSdk(GeneratorCache& cache, std::string_view module_name, const std::unordered_set& enums, const std::unordered_set& classes); } // namespace sdk diff --git a/source2gen/include/tools/codegen.h b/source2gen/include/tools/codegen.h index c9627f0..9dbb5fa 100644 --- a/source2gen/include/tools/codegen.h +++ b/source2gen/include/tools/codegen.h @@ -2,6 +2,7 @@ // See end of file for extended copyright information. #pragma once #include +#include #include #include #include @@ -11,37 +12,11 @@ #include "tools/fnv.h" namespace codegen { - constexpr char kTabSym = '\t'; - constexpr std::size_t kTabsPerBlock = 1; // @note: @es3n1n: how many \t characters shall we place per each block + constexpr char kSpaceSym = ' '; + constexpr char kIndentWidth = 4; + constexpr std::size_t kTabsPerBlock = 1; // @note: @es3n1n: how many (kSpaceSym * kIndentWidth) characters shall we place per each block constexpr std::array kBlacklistedCharacters = {':', ';', '\\', '/'}; - // @note: @es3n1n: a list of possible integral types for bitfields (would be used in `guess_bitfield_type`) - // - // clang-format off - constexpr auto kBitfieldIntegralTypes = std::to_array>({ - {8, "uint8_t"}, - {16, "uint16_t"}, - {32, "uint32_t"}, - {64, "uint64_t"}, - - // @todo: @es3n1n: define uint128_t/uint256_t/... as custom structs in the very beginning of the file - {128, "uint128_t"}, - {256, "uint256_t"}, - {512, "uint512_t"}, - }); - // clang-format on - - inline std::string guess_bitfield_type(const std::size_t bits_count) { - for (auto p : kBitfieldIntegralTypes) { - if (bits_count > p.first) - continue; - - return p.second.data(); - } - - throw std::runtime_error(std::format("{} : Unable to guess bitfield type with size {}", __FUNCTION__, bits_count)); - } - struct generator_t { using self_ref = std::add_lvalue_reference_t; @@ -82,6 +57,14 @@ namespace codegen { return pragma("warning(pop)"); } + self_ref pack_push(const std::size_t alignment = 1) { + return pragma(std::format("pack(push, {})", alignment)); + } + + self_ref pack_pop() { + return pragma("pack(pop)"); + } + self_ref next_line() { return push_line(""); } @@ -103,8 +86,9 @@ namespace codegen { // @note: @es3n1n: we should reset tabs count if we aren't moving cursor to // the next line const auto backup_tabs_count = _tabs_count; - if (!move_cursor_to_next_line) + if (!move_cursor_to_next_line) { _tabs_count = 0; + } push_line("{", move_cursor_to_next_line); @@ -130,21 +114,21 @@ namespace codegen { } self_ref begin_class(const std::string& class_name, const std::string& access_modifier = "public") { - return begin_block(std::format("class {}", class_name), access_modifier); + return begin_block(std::format("class {}", escape_name(class_name)), access_modifier); } self_ref begin_class_with_base_type(const std::string& class_name, const std::string& base_type, const std::string& access_modifier = "public") { if (base_type.empty()) - return begin_class(std::cref(class_name), access_modifier); - - return begin_block(std::format("class {} : public {}", class_name, base_type), access_modifier); + return begin_class(class_name, access_modifier); + else + return begin_block(std::format("class {} : public {}", escape_name(class_name), base_type), access_modifier); } self_ref end_class() { return end_block(); } - self_ref begin_namespace(const std::string& namespace_name) { + self_ref begin_namespace(const std::string_view namespace_name) { return begin_block(std::format("namespace {}", namespace_name)); } @@ -165,13 +149,13 @@ namespace codegen { return push_line(std::vformat(sizeof(T) >= 2 ? "{} = {:#x}," : "{} = {},", std::make_format_args(name, value))); } - self_ref begin_struct(const std::string& name, const std::string& access_modifier = "public") { + self_ref begin_struct(const std::string_view name, const std::string& access_modifier = "public") { return begin_block(std::format("struct {}", escape_name(name)), access_modifier); } - self_ref begin_struct_with_base_type(const std::string& name, const std::string& base_type, const std::string& access_modifier = "public") { + self_ref begin_struct_with_base_type(const std::string_view name, const std::string& base_type, const std::string& access_modifier = "public") { if (base_type.empty()) - return begin_struct(std::cref(name), access_modifier); + return begin_struct(name, access_modifier); return begin_block(std::format("struct {} : public {}", escape_name(name), base_type), access_modifier); } @@ -183,7 +167,7 @@ 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) { - return begin_block(std::format("{}{} {}()", prefix, type_name, escape_name(func_name)), "", increment_tabs_count, move_cursor_to_next_line); + return begin_block(std::format("{}{} {}() ", prefix, type_name, escape_name(func_name)), "", increment_tabs_count, move_cursor_to_next_line); } self_ref end_function(const bool decrement_tabs_count = true, const bool move_cursor_to_next_line = true) { @@ -215,6 +199,28 @@ namespace codegen { return *this; } + self_ref static_assert_size(std::string_view type_name, const std::size_t expected_size) { + assert(expected_size > 0); + + return push_line(std::format("static_assert(sizeof({}) == {:#x});", escape_name(type_name), expected_size)); + } + + self_ref static_assert_offset(std::string_view class_name, std::string_view prop_name, const std::size_t expected_offset) { + assert(expected_offset >= 0); + + return push_line(std::format("static_assert(offsetof({}, {}) == {:#x});", escape_name(class_name), prop_name, expected_offset)); + } + + /// Not to be used for inline comments. Doing so could break support for other laguages. + self_ref begin_multi_line_comment(const bool move_cursor_to_next_line = true) { + return push_line("/*", move_cursor_to_next_line); + } + + /// Not to be used for inline comments. Doing so could break support for other laguages. + self_ref end_multi_line_comment(const bool move_cursor_to_next_line = true) { + return push_line("*/", move_cursor_to_next_line); + } + self_ref comment(const std::string& text, const bool move_cursor_to_next_line = true) { return push_line(std::format("// {}", text), move_cursor_to_next_line); } @@ -224,9 +230,9 @@ namespace codegen { return push_line(line, move_cursor_to_next_line); } - self_ref forward_declaration(const std::string& text) { + self_ref forward_declaration(const std::string& type_name) { // @note: @es3n1n: forward decl only once - const auto fwd_decl_hash = fnv32::hash_runtime(text.data()); + const auto fwd_decl_hash = fnv32::hash_runtime(type_name.data()); if (_forward_decls.contains(fwd_decl_hash)) return *this; @@ -234,21 +240,34 @@ namespace codegen { // @fixme: split method to class_forward_declaration & struct_forward_declaration // one for `struct uwu_t` and the other one for `class c_uwu` - return push_line(std::format("struct {};", text)); + return push_line(std::format("struct {};", type_name)); } self_ref struct_padding(const std::optional pad_offset, const std::size_t padding_size, const bool move_cursor_to_next_line = true, - const bool is_private_field = false, const std::size_t bitfield_size = 0ull) { + const bool is_private_field = false, const int bitfield_size = 0) { + assert(bitfield_size >= 0); + + const auto bytes = (bitfield_size == 0) ? padding_size : bitfield_size / 8; + const auto remaining_bits = bitfield_size % 8; + // @note: @es3n1n: mark private fields as maybe_unused to silence -Wunused-private-field - std::string type_name = bitfield_size ? guess_bitfield_type(bitfield_size) : "uint8_t"; + std::string type_name = "std::uint8_t"; if (is_private_field) type_name = "[[maybe_unused]] " + type_name; - auto pad_name = pad_offset.has_value() ? std::format("__pad{:04x}", pad_offset.value()) : std::format("__pad{:d}", _pads_count++); - if (!bitfield_size) - pad_name = pad_name + std::format("[{:#x}]", padding_size); + auto pad_name = pad_offset.has_value() ? std::format("pad_{:#04x}", pad_offset.value()) : std::format("pad_{:d}", _pads_count++); - return prop(type_name, bitfield_size ? std::format("{}: {}", pad_name, bitfield_size) : pad_name, move_cursor_to_next_line); + if (bytes != 0) { + prop(type_name, std::format("{}[{:#x}]", pad_name, bytes), move_cursor_to_next_line); + } + + if (remaining_bits != 0) { + auto remainder_pad_name = + pad_offset.has_value() ? std::format("pad_{:#04x}", pad_offset.value() + bytes) : std::format("pad_{:d}", _pads_count++); + prop(type_name, std::format("{}: {}", remainder_pad_name, remaining_bits), move_cursor_to_next_line); + } + + return *this; } self_ref begin_union(std::string name = "") { @@ -262,13 +281,12 @@ namespace codegen { return push_line(move_cursor_to_next_line ? "};" : "}; ", move_cursor_to_next_line); } - self_ref begin_bitfield_block() { - return begin_struct("", ""); + self_ref begin_bitfield_block(std::ptrdiff_t offset) { + return comment(std::format("start of bitfield block at {:#x}", offset)); } self_ref end_bitfield_block(const bool move_cursor_to_next_line = true) { - dec_tabs_count(1); - return push_line(move_cursor_to_next_line ? "};" : "}; ", move_cursor_to_next_line); + return comment(std::format("end of bitfield block{}", move_cursor_to_next_line ? "" : " "), move_cursor_to_next_line); } public: @@ -278,15 +296,16 @@ namespace codegen { private: self_ref push_line(const std::string& line, bool move_cursor_to_next_line = true) { - for (std::size_t i = 0; i < _tabs_count; i++) - _stream << kTabSym; - _stream << line; - if (move_cursor_to_next_line) + _stream << std::string(_tabs_count * kIndentWidth, kSpaceSym) // insert spaces + << line; + + if (move_cursor_to_next_line) { _stream << std::endl; + } return *this; } - static std::string escape_name(const std::string& name) { + static std::string escape_name(const std::string_view name) { std::string result; result.resize(name.size()); diff --git a/source2gen/include/tools/field_parser.h b/source2gen/include/tools/field_parser.h index 27cad56..b5cda24 100644 --- a/source2gen/include/tools/field_parser.h +++ b/source2gen/include/tools/field_parser.h @@ -8,14 +8,13 @@ #include #include #include -#include #include namespace field_parser { class field_info_t { public: std::string m_type; // var type - fieldtype_t m_field_type = static_cast(24); // var type + fieldtype_t m_field_type = fieldtype_t::FIELD_UNUSED; // var type std::string m_name; // var name // array sizes, for example {13, 37} for multi demensional array "[13][37]" @@ -69,6 +68,9 @@ namespace field_parser { } }; + [[nodiscard]] + std::string guess_bitfield_type(std::size_t bits_count); + /// @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); diff --git a/source2gen/include/tools/util.h b/source2gen/include/tools/util.h index aa7d322..51d9f3c 100644 --- a/source2gen/include/tools/util.h +++ b/source2gen/include/tools/util.h @@ -2,6 +2,8 @@ // See end of file for extended copyright information. #pragma once +#include +#include #include #include #include @@ -22,6 +24,15 @@ namespace util { return std::to_string(num); } + /// Useful for optional integers, e.g. + /// ```cpp + /// const std::optional offset = try_get_offset(); + /// std::cout << std::format("offset: {}\n", offset.transform(to_hex_string).value_or("unknown")); + /// ``` + inline std::string to_hex_string(const std::uintptr_t i) { + return std::format("{:#x}", i); + } + [[nodiscard]] inline std::string EscapePath(std::string_view path) { std::string result(path); std::ranges::replace(result, ':', '_'); diff --git a/source2gen/src/sdk/sdk.cpp b/source2gen/src/sdk/sdk.cpp index 29c58f2..6954fb0 100644 --- a/source2gen/src/sdk/sdk.cpp +++ b/source2gen/src/sdk/sdk.cpp @@ -3,6 +3,8 @@ // ReSharper disable CppClangTidyClangDiagnosticLanguageExtensionToken #include "sdk/sdk.h" + +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include @@ -31,58 +34,81 @@ namespace { auto operator<=>(const NameLookup&) const = default; }; - using namespace std::string_view_literals; + struct BitfieldEntry { + std::string name{}; + std::size_t size{}; + /// Lifetime of fields' pointers bound to the source2's @ref CSchemaClassInfo + std::vector metadata{}; + }; + + struct ClassAssemblyState { + std::optional last_field_size = std::nullopt; + std::optional last_field_offset = std::nullopt; + bool assembling_bitfield = false; + std::vector bitfield = {}; + std::int32_t bitfield_start = 0; - constexpr std::string_view kOutDirName = "sdk"sv; + std::ptrdiff_t collision_end_offset = 0ull; // @fixme: @es3n1n: todo proper collision fix and remove this var + }; - constexpr uint32_t kMaxReferencesForClassEmbed = 2; - constexpr std::size_t kMinFieldCountForClassEmbed = 2; - constexpr std::size_t kMaxFieldCountForClassEmbed = 12; + /** + * Project structure is + * + * - CMakeLists.txt + * - ... + * - include/ + * - + * - some_module + * - some_header.hpp + */ + constexpr std::string_view kOutDirName = "sdk"; + constexpr std::string_view kIncludeDirName = "source2sdk"; constinit std::array string_metadata_entries = { - FNV32("MNetworkChangeCallback"), - FNV32("MPropertyFriendlyName"), - FNV32("MPropertyDescription"), - FNV32("MPropertyAttributeRange"), - FNV32("MPropertyStartGroup"), - FNV32("MPropertyAttributeChoiceName"), - FNV32("MPropertyGroupName"), - FNV32("MNetworkUserGroup"), - FNV32("MNetworkAlias"), - FNV32("MNetworkTypeAlias"), - FNV32("MNetworkSerializer"), - FNV32("MPropertyAttributeEditor"), - FNV32("MPropertySuppressExpr"), - FNV32("MKV3TransferName"), + FNV32("MCellForDomain"), + FNV32("MCustomFGDMetadata"), FNV32("MFieldVerificationName"), - FNV32("MVectorIsSometimesCoordinate"), + FNV32("MKV3TransferName"), + FNV32("MNetworkAlias"), + FNV32("MNetworkChangeCallback"), FNV32("MNetworkEncoder"), - FNV32("MPropertyCustomFGDType"), - FNV32("MPropertyCustomEditor"), - FNV32("MVDataUniqueMonotonicInt"), - FNV32("MScriptDescription"), - FNV32("MPropertyAttributeSuggestionName"), - FNV32("MPropertyIconName"), - FNV32("MVDataOutlinerIcon"), - FNV32("MPropertyExtendedEditor"), - FNV32("MParticleReplacementOp"), - FNV32("MCustomFGDMetadata"), - FNV32("MCellForDomain"), - FNV32("MSrc1ImportDmElementType"), - FNV32("MSrc1ImportAttributeName"), - FNV32("MResourceBlockType"), - FNV32("MVDataOutlinerIconExpr"), - FNV32("MPropertyArrayElementNameKey"), - FNV32("MPropertyFriendlyName"), - FNV32("MPropertyDescription"), FNV32("MNetworkExcludeByName"), FNV32("MNetworkExcludeByUserGroup"), FNV32("MNetworkIncludeByName"), FNV32("MNetworkIncludeByUserGroup"), - FNV32("MNetworkUserGroupProxy"), FNV32("MNetworkReplayCompatField"), - FNV32("MPulseProvideFeatureTag"), + FNV32("MNetworkSerializer"), + FNV32("MNetworkTypeAlias"), + FNV32("MNetworkUserGroup"), + FNV32("MNetworkUserGroupProxy"), + FNV32("MParticleReplacementOp"), + FNV32("MPropertyArrayElementNameKey"), + FNV32("MPropertyAttributeChoiceName"), + FNV32("MPropertyAttributeEditor"), + FNV32("MPropertyAttributeRange"), + FNV32("MPropertyAttributeSuggestionName"), + FNV32("MPropertyCustomEditor"), + FNV32("MPropertyCustomFGDType"), + FNV32("MPropertyDescription"), + FNV32("MPropertyDescription"), + FNV32("MPropertyExtendedEditor"), + FNV32("MPropertyFriendlyName"), + FNV32("MPropertyFriendlyName"), + FNV32("MPropertyGroupName"), + FNV32("MPropertyIconName"), + FNV32("MPropertyStartGroup"), + FNV32("MPropertySuppressExpr"), + FNV32("MPulseCellOutflowHookInfo"), FNV32("MPulseEditorHeaderIcon"), + FNV32("MPulseProvideFeatureTag"), + FNV32("MResourceBlockType"), + FNV32("MScriptDescription"), + FNV32("MSrc1ImportAttributeName"), + FNV32("MSrc1ImportDmElementType"), + FNV32("MVDataOutlinerIcon"), + FNV32("MVDataOutlinerIconExpr"), + FNV32("MVDataUniqueMonotonicInt"), + FNV32("MVectorIsSometimesCoordinate"), }; constinit std::array string_class_metadata_entries = { @@ -91,8 +117,7 @@ namespace { }; constinit std::array var_name_string_class_metadata_entries = { - FNV32("MNetworkVarNames"), FNV32("MNetworkOverride"), FNV32("MNetworkVarTypeOverride"), - FNV32("MPulseCellOutflowHookInfo"), FNV32("MScriptDescription"), FNV32("MParticleDomainTag"), + FNV32("MNetworkVarNames"), FNV32("MNetworkOverride"), FNV32("MNetworkVarTypeOverride"), FNV32("MScriptDescription"), FNV32("MParticleDomainTag"), }; constinit std::array integer_metadata_entries = { @@ -116,6 +141,10 @@ namespace { FNV32("MNetworkMaxValue"), }; + void warn(std::string_view message) { + std::cerr << "warning: " << message << '\n'; + } + // @note: @es3n1n: some more utils // std::string GetMetadataValue(const SchemaMetadataEntryData_t& metadata_entry) { @@ -123,22 +152,18 @@ namespace { const auto value_hash_name = fnv32::hash_runtime(metadata_entry.m_szName); - // clang-format off - if (std::ranges::find(var_name_string_class_metadata_entries, value_hash_name) != var_name_string_class_metadata_entries.end()) - { - const auto &var_value = metadata_entry.m_pNetworkValue->m_VarValue; + if (std::ranges::find(var_name_string_class_metadata_entries, value_hash_name) != var_name_string_class_metadata_entries.end()) { + const auto& var_value = metadata_entry.m_pNetworkValue->m_VarValue; if (var_value.m_pszType && var_value.m_pszName) value = std::format("{} {}", var_value.m_pszType, var_value.m_pszName); else if (var_value.m_pszName && !var_value.m_pszType) value = var_value.m_pszName; else if (!var_value.m_pszName && var_value.m_pszType) value = var_value.m_pszType; - } - else if (std::ranges::find(string_class_metadata_entries, value_hash_name) != string_class_metadata_entries.end()) - { + } else if (std::ranges::find(string_class_metadata_entries, value_hash_name) != string_class_metadata_entries.end()) { auto clean_string = [](const std::string_view& input) { std::string result; - for (const char &ch : input) { + for (const char& ch : input) { if (std::isalpha(static_cast(ch))) { result += ch; } else { @@ -149,59 +174,165 @@ namespace { }; value = clean_string(metadata_entry.m_pNetworkValue->m_szValue.data()); - } - else if (std::ranges::find(string_metadata_entries, value_hash_name) != string_metadata_entries.end()) + } else if (std::ranges::find(string_metadata_entries, value_hash_name) != string_metadata_entries.end()) { value = metadata_entry.m_pNetworkValue->m_pszValue; - else if (std::ranges::find(integer_metadata_entries, value_hash_name) != integer_metadata_entries.end()) + } else if (std::ranges::find(integer_metadata_entries, value_hash_name) != integer_metadata_entries.end()) { value = std::to_string(metadata_entry.m_pNetworkValue->m_nValue); - else if (std::ranges::find(float_metadata_entries, value_hash_name) != float_metadata_entries.end()) + } else if (std::ranges::find(float_metadata_entries, value_hash_name) != float_metadata_entries.end()) { value = std::to_string(metadata_entry.m_pNetworkValue->m_fValue); - // clang-format on + } return value; - }; + } - 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)); + /// https://en.cppreference.com/w/cpp/language/classes#Standard-layout_class + /// Doesn't check for all requirements, but is strict enough for what we are doing. + [[nodiscard]] bool IsStandardLayoutClass(std::map& cache, const CSchemaClassInfo& class_) { + const auto id = sdk::TypeIdentifier{.module = std::string{class_.GetModule()}, .name = std::string{class_.GetName()}}; + + if (const auto found = cache.find(id); found != cache.end()) { + return found->second; + } + + // only one class in the hierarchy has non-static data members. + // assumes that source2 only has single inheritance. + { + const auto* pClass = &class_; + int classes_with_fields = 0; + do { + // also check size because not all members are registered with + // the schema system. + classes_with_fields += ((pClass->m_nSizeOf > 1) || (pClass->m_nFieldSize != 0)) ? 1 : 0; + + if (classes_with_fields > 1) { + return cache.emplace(id, false).first->second; + } + + pClass = (pClass->m_pBaseClasses == nullptr) ? nullptr : pClass->m_pBaseClasses->m_pClass; + } while (pClass != nullptr); + } - if ((class_info.m_nClassFlags & SCHEMA_CF1_HAS_VIRTUAL_MEMBERS) != 0) // @note: @og: its means that class probably does have vtable + const auto has_non_standard_layout_field = std::ranges::any_of( + class_.GetFields() | std::ranges::views::transform([&](const SchemaClassFieldData_t& e) { + if (const auto* e_class = e.m_pSchemaType->GetAsDeclaredClass(); e_class != nullptr && e_class->m_pClassInfo != nullptr) { + return !IsStandardLayoutClass(cache, *e_class->m_pClassInfo); + } else { + // Everything that is not a class has no effect + return false; + } + }), + std::identity{}); + + if (has_non_standard_layout_field) { + return cache.emplace(id, false).first->second; + } + + return cache.emplace(id, true).first->second; + } + + /// Gets the alignment of a class by recursing through all of its fields. + /// Does not guess, the returned value is correct if set. + /// @param cache Used to look up and store alignment of fields + /// @return @ref GetRegisteredAlignment() if set. Otherwise tries to determine the alignment by recursing through all fields. + /// Returns @ref std::nullopt if one or more fields have unknown alignment. + [[nodiscard]] std::optional GetClassAlignmentRecursive(std::map>& cache, const CSchemaClassInfo& class_) { + const auto id = sdk::TypeIdentifier{.module = std::string{class_.GetModule()}, .name = std::string{class_.GetName()}}; + + if (const auto found = cache.find(id); found != cache.end()) { + return found->second; + } + + return class_.GetRegisteredAlignment().or_else([&]() { + int base_alignment = 0; + + if (class_.m_pBaseClasses != nullptr) { + if (const auto maybe_base_alignment = GetClassAlignmentRecursive(cache, *class_.m_pBaseClasses->m_pClass)) { + base_alignment = maybe_base_alignment.value(); + } else { + // we have a base class, but it has unknown alignment + return cache.emplace(id, std::nullopt).first->second; + } + } + + auto field_alignments = class_.GetFields() | std::ranges::views::transform([&](const SchemaClassFieldData_t& e) { + if (const auto* e_class = e.m_pSchemaType->GetAsDeclaredClass(); e_class != nullptr) { + return GetClassAlignmentRecursive(cache, *e_class->m_pClassInfo); + } else { + return e.m_pSchemaType->GetSizeAndAlignment().and_then([](const auto& e) { return std::get<1>(e); }); + } + }); + + if (field_alignments.empty()) { + // This is an empty class. The generator will add a single pad with alignment 1. + return cache.emplace(id, std::make_optional((base_alignment == 0) ? 1 : base_alignment)).first->second; + } else if (std::ranges::all_of(field_alignments, &std::optional::has_value)) { + int max_alignment = base_alignment; + for (const auto& e : field_alignments) { + max_alignment = std::max(max_alignment, e.value()); + } + return cache.emplace(id, std::make_optional(max_alignment)).first->second; + } else { + // there are fields with unknown alignment + return cache.emplace(id, std::nullopt).first->second; + } + }); + } + + /// @return For class types, returns @ref GetClassAlignmentRecursive(). Otherwise returns the immediately available size. + [[nodiscard]] + std::optional GetAlignmentOfTypeRecursive(std::map>& cache, const CSchemaType& type) { + if (const auto* class_ = type.GetAsDeclaredClass(); class_ != nullptr && class_->m_pClassInfo != nullptr) { + return GetClassAlignmentRecursive(cache, *class_->m_pClassInfo); + } else { + return type.GetSizeAndAlignment().and_then([](const auto& e) { return std::get<1>(e); }); + } + } + + void PrintClassInfo(sdk::GeneratorCache& cache, codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { + builder.comment(std::format("Registered alignment: {}", class_.GetRegisteredAlignment().transform(&util::to_hex_string).value_or("unknown"))); + builder.comment( + std::format("Alignment: {}", GetClassAlignmentRecursive(cache.class_alignment, class_).transform(&util::to_hex_string).value_or("unknown"))); + builder.comment(std::format("Standard-layout class: {}", IsStandardLayoutClass(cache.class_has_standard_layout, class_))); + builder.comment(std::format("Size: {:#x}", class_.m_nSizeOf)); + + if ((class_.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_.m_nClassFlags & SCHEMA_CF1_IS_ABSTRACT) != 0) builder.comment("Is Abstract"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_HAS_TRIVIAL_CONSTRUCTOR) != 0) + if ((class_.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_.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_.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_ALLOWED) != 0) builder.comment("Construct allowed"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_DISALLOWED) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_CONSTRUCT_DISALLOWED) != 0) builder.comment("Construct disallowed"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MConstructibleClassBase) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MConstructibleClassBase) != 0) builder.comment("MConstructibleClassBase"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasCustomAlignedNewDelete) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasCustomAlignedNewDelete) != 0) builder.comment("MClassHasCustomAlignedNewDelete"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasEntityLimitedDataDesc) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MClassHasEntityLimitedDataDesc) != 0) builder.comment("MClassHasEntityLimitedDataDesc"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MDisableDataDescValidation) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MDisableDataDescValidation) != 0) builder.comment("MDisableDataDescValidation"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MIgnoreTypeScopeMetaChecks) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MIgnoreTypeScopeMetaChecks) != 0) builder.comment("MIgnoreTypeScopeMetaChecks"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkNoBase) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkNoBase) != 0) builder.comment("MNetworkNoBase"); - if ((class_info.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkAssumeNotNetworkable) != 0) + if ((class_.m_nClassFlags & SCHEMA_CF1_INFO_TAG_MNetworkAssumeNotNetworkable) != 0) builder.comment("MNetworkAssumeNotNetworkable"); #endif - if (class_info.m_nStaticMetadataSize > 0) + if (class_.m_nStaticMetadataSize > 0) builder.comment(""); - for (const auto& metadata : class_info.GetStaticMetadata()) { + for (const auto& metadata : class_.GetStaticMetadata()) { if (const auto value = GetMetadataValue(metadata); !value.empty()) - builder.comment(std::format("{} \"{}\"", metadata.m_szName, value)); + builder.comment(std::format("static metadata: {} \"{}\"", metadata.m_szName, value)); else - builder.comment(metadata.m_szName); + builder.comment(std::format("static metadata: {}", metadata.m_szName)); } } @@ -214,7 +345,7 @@ namespace { builder.comment(""); for (const auto& metadata : enum_binding.GetStaticMetadata()) { - builder.comment(metadata.m_szName); + builder.comment(std::format("metadata: {}", metadata.m_szName)); } } @@ -318,7 +449,7 @@ namespace { } return {base_type, sizes}; - }; + } /// @return Lifetime is bound to string viewed by @p type_name [[nodiscard]] @@ -357,7 +488,7 @@ namespace { } else { return std::nullopt; } - }; + } [[nodiscard]] std::string EscapeTypeName(const std::string_view type_name) { // TODO: when we have a package manager: use a library @@ -384,7 +515,7 @@ namespace { return GetModuleOfTypeInScope(scope, type_name) .transform([&](const auto module_name) { return std::format("{}::{}", module_name, escaped_type_name); }) .value_or(escaped_type_name); - }; + } /// Decomposes a templated type into its components, keeping template /// syntax for later reassembly by @ref ReassembleRetypedTemplate(). @@ -394,13 +525,13 @@ namespace { 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) { + 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) { + if (const auto found = str.find_last_not_of(' '); found != std::string_view::npos) { str.remove_suffix(str.size() - (found + 1)); } @@ -442,7 +573,7 @@ namespace { // 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_back(std::get(el)); } } @@ -475,6 +606,7 @@ namespace { 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); @@ -488,13 +620,13 @@ namespace { return {type_name_with_modules, array_sizes}; return {type_name_with_modules, {}}; - }; + } // 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) { + constexpr bool IsOdrUse(std::string_view type_name) { return !type_name.contains('*'); } @@ -567,7 +699,64 @@ namespace { return result; } - void AssembleClass(codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { + [[nodiscard]] + ClassAssemblyState AssembleBitfield(codegen::generator_t& builder, ClassAssemblyState&& state, std::ptrdiff_t expected_offset) { + state.assembling_bitfield = false; + + std::size_t exact_bitfield_size_bits = 0; + for (const auto& entry : state.bitfield) { + exact_bitfield_size_bits += entry.size; + } + const auto type_name = field_parser::guess_bitfield_type(exact_bitfield_size_bits); + + builder.begin_bitfield_block(expected_offset); + + for (const auto& entry : state.bitfield) { + for (const auto& field_metadata : entry.metadata) { + if (auto data = GetMetadataValue(field_metadata); data.empty()) + builder.comment(std::format("metadata: {}", field_metadata.m_szName)); + else + builder.comment(std::format("metadata: {} \"{}\"", field_metadata.m_szName, data)); + } + + builder.prop(type_name, entry.name, true); + } + + builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", exact_bitfield_size_bits)).restore_tabs_count(); + + state.bitfield.clear(); + state.last_field_offset = state.last_field_offset.value_or(0) + state.last_field_size.value_or(0); + // call to bit_ceil() relies on guess_bitfield_type() returning the next highest power of 2 + state.last_field_size = std::bit_ceil(std::max(std::size_t{8}, exact_bitfield_size_bits)) / 8; + + return state; + } + + /// Does not insert a pad if it would have size 0 + void InsertPadUntil(codegen::generator_t::self_ref builder, const ClassAssemblyState& state, std::int32_t offset, bool verbose) { + if (verbose) { + builder.comment(std::format("last_field_offset={} last_field_size={}", + state.last_field_offset.transform(&util::to_hex_string).value_or("none"), + state.last_field_size.transform(&util::to_hex_string).value_or("none"))); + } + + const auto expected_offset = state.last_field_offset.value_or(0) + state.last_field_size.value_or(0); + + // insert padding only if needed + if (expected_offset < static_cast(offset) && !state.assembling_bitfield) { + builder.struct_padding(expected_offset, offset - expected_offset, false, true) + .reset_tabs_count() + .comment(std::format("{:#x}", expected_offset)) + .restore_tabs_count(); + } + } + + void AssembleClass(sdk::GeneratorCache& cache, codegen::generator_t::self_ref builder, const CSchemaClassBinding& class_) { + static constexpr std::size_t source2_max_align = 8; + + // TODO: when we have a CLI parser: pass this property in from the outside + constexpr bool verbose = false; + struct cached_datamap_t { std::string type_; std::string name_; @@ -577,205 +766,243 @@ namespace { // @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"); + const auto class_size = class_.GetSize(); + const auto class_alignment = GetClassAlignmentRecursive(cache.class_alignment, class_); + // Source2 has alignof(max_align_t)=8, i.e. every class whose size is a multiple of 8 is aligned. + const auto class_is_aligned = (class_size % class_alignment.value_or(source2_max_align)) == 0; + const auto is_struct = std::string_view{class_.m_pszName}.ends_with("_t"); + + if (!class_is_aligned) { + const auto warning = [&]() { + if (class_alignment.has_value()) { + // ceil size to next possible aligned size + const auto aligned_size = class_size + (class_alignment.value() - (class_size % class_alignment.value())) % class_alignment.value(); + + return std::format("Type {} is misaligned. Its size should be {:#x}, but with proper alignment it has size {:#x}.", class_.GetName(), + class_size, aligned_size); + } else { + return std::format("Type {} appears to be misaligned. Its alignment is unknown and it is not aligned to max_align_t ({}).", + class_.GetName(), source2_max_align); + } + }(); + warn(warning); + builder.comment(warning); + builder.comment("It has been replaced by a dummy. You can try uncommenting the struct below."); + builder.begin_struct(class_.GetName()); + builder.struct_padding(0, class_size); + builder.end_struct(); + } - PrintClassInfo(builder, class_info); + PrintClassInfo(cache, builder, class_); // @note: @es3n1n: get parent name // const std::string parent_class_name = (class_parent != nullptr) ? MaybeWithModuleName(*class_parent->m_pTypeScope, class_parent->m_pszName) : ""; + const std::optional parent_class_size = class_parent ? std::make_optional(class_parent->m_nSizeOf) : std::nullopt; - // @note: @es3n1n: start class - // - if (is_struct) - builder.begin_struct_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); - else - builder.begin_class_with_base_type(EscapeTypeName(class_info.m_pszName), parent_class_name, ""); + if (!class_is_aligned) { + builder.begin_multi_line_comment(); + } // @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{}; + ClassAssemblyState state = {.last_field_size = parent_class_size}; // @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(); + const auto* first_field = (class_.m_pFields == nullptr) ? nullptr : &class_.m_pFields[0]; + const std::optional first_field_offset = + (first_field != nullptr) ? std::make_optional(first_field->m_nSingleInheritanceOffset) : std::nullopt; // @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 ((class_parent != nullptr) && first_field_offset.has_value() && first_field_offset.value() < parent_class_size.value()) { + const auto warning = std::format("Collision detected: {} and its base {} have {:#x} overlapping byte(s)", class_.GetName(), parent_class_name, + parent_class_size.value() - first_field_offset.value()); + warn(warning); + builder.comment(warning); + state.collision_end_offset = parent_class_size.value(); } - // @note: @es3n1n: begin public members - // - builder.access_modifier("public"); + /// Start the class + builder.pack_push(1); // we are aligning stuff ourselves + if (is_struct) + builder.begin_struct_with_base_type(class_.m_pszName, parent_class_name); + else + builder.begin_class_with_base_type(class_.m_pszName, parent_class_name); - for (const auto& field : class_info.GetFields()) { - // @fixme: @es3n1n: todo proper collision fix and remove this block - if (state.collision_end_offset && field.m_nSingleInheritanceOffset < state.collision_end_offset) { - builder.comment( - std::format("Skipped field \"{}\" @ {:#x} because of the struct collision", field.m_pszName, field.m_nSingleInheritanceOffset)); - continue; - } + /// If fields cannot be emitted, e.g. because of collisions, they're added to + /// this set so we can ignore them when asserting offsets. + std::unordered_set skipped_fields{}; + std::list> cached_fields{}; + std::list cached_datamap_fields{}; - // @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); + for (const auto& field : class_.GetFields()) { + // Fall back to size=1 because there are no 0-sized types. + // `RenderPrimitiveType_t` is the only type (in CS2 9035763) without size information. + const auto field_size = field.m_pSchemaType->GetSize().value_or(1); + const auto field_alignment = GetAlignmentOfTypeRecursive(cache.class_alignment, *field.m_pSchemaType); // @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); + 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: begin union if we're assembling bitfields - // - if (!state.assembling_bitfield && var_info.is_bitfield()) { - builder.begin_bitfield_block(); - state.assembling_bitfield = true; + // @fixme: @es3n1n: todo proper collision fix and remove this block + if (state.collision_end_offset && field.m_nSingleInheritanceOffset < state.collision_end_offset) { + skipped_fields.emplace(field.m_pszName); + // A warning has already been logged at the start of the class + builder.comment( + std::format("Skipped field \"{}\" @ {:#x} because of the struct collision", field.m_pszName, field.m_nSingleInheritanceOffset)); + builder.comment("", false).reset_tabs_count().prop(var_info.m_type, var_info.formatted_name()).restore_tabs_count(); + continue; } - // @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; - - const auto actual_union_size_bits = state.total_bits_count_in_union; - - 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)); + // Collect all bitfield entries and emit them later. We need to know + // how large the bitfield is in order to choose the right type. We + // only know how large the bitfield is once we've reached its end. + if (var_info.is_bitfield()) { + if (!state.assembling_bitfield) { + state.assembling_bitfield = true; + state.bitfield_start = field.m_nSingleInheritanceOffset; + } - 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.bitfield.emplace_back(BitfieldEntry{ + .name = var_info.formatted_name(), + .size = var_info.m_bitfield_size, + .metadata = std::vector(field.m_pMetadata, field.m_pMetadata + field.m_nMetadataSize), + }); + continue; + } - state.last_field_offset += expected_union_size_bytes; - state.last_field_size = expected_union_size_bytes; + // At this point, we're never still inside a bitfield. If `assembling_bitfield` is set, that means we're at the first field following a + // bitfield, but the bitfield has not been emitted yet. + // note: in CS2, there are no types with padding before a bitfield + InsertPadUntil(builder, state, state.assembling_bitfield ? state.bitfield_start : field.m_nSingleInheritanceOffset, verbose); - builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + // This is the first field after a bitfield, i.e. the active bitfield has ended. Emit the bitfield we have collected. + if (state.assembling_bitfield) { + state = AssembleBitfield(builder, std::move(state), state.last_field_offset.value_or(0) + state.last_field_size.value_or(0)); - state.total_bits_count_in_union = 0ull; - state.assembling_bitfield = false; + // We need another pad here because the current loop iteration is already on a non-bitfield field which will get emitted right away. + InsertPadUntil(builder, state, field.m_nSingleInheritanceOffset, verbose); } // @note: @es3n1n: dump metadata // for (auto j = 0; j < field.m_nMetadataSize; j++) { - auto field_metadata = field.m_pMetadata[j]; + const auto field_metadata = field.m_pMetadata[j]; if (auto data = GetMetadataValue(field_metadata); data.empty()) - builder.comment(field_metadata.m_szName); + builder.comment(std::format("metadata: {}", field_metadata.m_szName)); else - builder.comment(std::format("{} \"{}\"", field_metadata.m_szName, data)); + builder.comment(std::format("metadata: {} \"{}\"", field_metadata.m_szName, data)); } // @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); + state.last_field_offset = field.m_nSingleInheritanceOffset; + state.last_field_size = static_cast(field_size); + + /// @note: @es3n1n: game bug: + /// There are some classes that have literally no info about them in schema, + /// for these fields we'll just insert a pad. + if (const auto e_class = field.m_pSchemaType->GetAsDeclaredClass(); e_class != nullptr && e_class->m_pClassInfo == nullptr) { + var_info.m_type = "std::uint8_t"; + var_info.m_array_sizes.clear(); + var_info.m_array_sizes.emplace_back(field_size); + builder.comment(std::format("game bug: prop with no declared class info ({})", e_class->m_pszName)); } - 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 ((field.m_nSingleInheritanceOffset % field_alignment.value_or(source2_max_align)) == 0) { + if (std::string{field.m_pSchemaType->m_pszName}.contains('<')) { + // This is a workaround to get the size of template types right. + // There are types that have non-type template parameters, e.g. + // `CUtlLeanVectorFixedGrowable`. The non-type template parameter affects the size of the template type, but the schema system + // doesn't store information about non-type template parameters. The schema system just says `CUtlLeanVectorFixedGrowable`, which + // is insufficient to generate a `CUtlLeanVectorFixedGrowable` with correct size.` + // To still keep the rest of the class in order, we replace all template fields with char arrays. + // We're applying this workaround to all template type, even those that don't have non-type template parameters, because we can't tell + // them apart. So we're certainly commenting out more than is necessary. + builder.comment( + std::format("{} has a template type with potentially unknown template parameters. You can try uncommenting the field below.", + var_info.m_name)); + builder.comment("", false); + builder.reset_tabs_count().prop(var_info.m_type, var_info.formatted_name(), true).restore_tabs_count(); + builder.prop("char", std::format("{}[{:#x}]", var_info.m_name, field_size), false); + } else { + builder.prop(var_info.m_type, var_info.formatted_name(), false); + } + } else { + const auto warning = + field_alignment.has_value() ? + std::format("Property {}::{} is misaligned.", class_.GetName(), field.m_pszName) : + std::format("Property {}::{} appears to be misaligned. Its alignment is unknown and it is not aligned to max_align_t ({}).", + class_.GetName(), field.m_pszName, source2_max_align); + warn(warning); + builder.comment(warning); + builder.prop("char", std::format("{}[{:#x}]", var_info.m_name, field_size), true); + builder.comment("", false).reset_tabs_count().prop(var_info.m_type, var_info.formatted_name(), false).restore_tabs_count(); + } - if (!var_info.is_bitfield()) { + if constexpr (verbose) { + builder.reset_tabs_count() + .comment(std::format("type.name=\"{}\" offset={:#x} size={:#x} alignment={}", std::string_view{field.m_pSchemaType->m_pszName}, + field.m_nSingleInheritanceOffset, field_size, + field_alignment.transform(&util::to_hex_string).value_or("unknown")), + false) + .restore_tabs_count(); + } else { 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); } + cached_fields.emplace_back(var_info.formatted_name(), field.m_nSingleInheritanceOffset); + builder.next_line(); } - // @note: @es3n1n: if struct ends with union we should end union before ending the class + // @note: @es3n1n: if struct ends with bitfield we should end bitfield before ending the class // if (state.assembling_bitfield) { - 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 > actual_union_size_bits) - builder.struct_padding(std::nullopt, 0, true, false, expected_union_size_bits - actual_union_size_bits); - - builder.end_bitfield_block(false).reset_tabs_count().comment(std::format("{:d} bits", expected_union_size_bits)).restore_tabs_count(); + state = AssembleBitfield(builder, std::move(state), state.last_field_offset.value_or(0) + state.last_field_size.value_or(0)); + } - state.total_bits_count_in_union = 0; - state.assembling_bitfield = false; + // pad the class end. + const auto last_field_end = state.last_field_offset.value_or(0) + state.last_field_size.value_or(0); + const auto end_pad = class_size - last_field_end; + + // The `(class_size != 1)` check is here because of empty classes. If + // we generated a pad for empty classes, they'd no longer have standard-layout. + // The pad isn't necessary for such classes, because the compiler will make them have size=1. + if ((end_pad != 0) && (class_size != 1)) { + builder.struct_padding(last_field_end, end_pad, true, true); + } else if (end_pad < 0) [[unlikely]] { + throw std::runtime_error{std::format("{} overflows by {:#x} byte(s). Its last field ends at {:#x}, but {} ends at {:#x}", class_.GetName(), + -end_pad, last_field_end, class_.GetName(), class_size)}; } // @note: @es3n1n: dump static fields // - if (class_info.m_nStaticFieldsSize) { - if (class_info.m_nFieldSize) + if (class_.m_nStaticFieldsSize) { + if (class_.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()}; + const std::string scope_name{class_.m_pTypeScope->BGetScopeName()}; - for (auto s = 0; s < class_info.m_nStaticFieldsSize; s++) { - auto static_field = &class_info.m_pStaticFields[s]; + for (auto s = 0; s < class_.m_nStaticFieldsSize; s++) { + auto static_field = &class_.m_pStaticFields[s]; 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); + builder.static_field_getter(var_info.m_type, var_info.m_name, scope_name, class_.m_pszName, s); } - if (class_info.m_pFieldMetadataOverrides && class_info.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { - const auto& dm = class_info.m_pFieldMetadataOverrides; + if (class_.m_pFieldMetadataOverrides && class_.m_pFieldMetadataOverrides->m_iTypeDescriptionCount > 1) { + const auto& dm = class_.m_pFieldMetadataOverrides; for (std::uint64_t s = 0; s < dm->m_iTypeDescriptionCount; s++) { auto* t = &dm->m_pTypeDescription[s]; @@ -785,7 +1012,7 @@ namespace { if (t->GetFieldName().empty()) continue; - const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName().data(), t->m_nFieldSize); + const auto var_info = field_parser::parse(t->m_iFieldType, t->GetFieldName(), t->m_nFieldSize); std::string field_type = var_info.m_type; if (t->m_iFieldType == fieldtype_t::FIELD_EMBEDDED) { @@ -795,6 +1022,7 @@ namespace { std::string field_name = var_info.formatted_name(); // @note: @og: if schema dump already has this field, then just skip it + if (const auto it = std::ranges::find_if(cached_fields, [t, field_name](const auto& f) { return f.first == field_name && f.second == t->m_iOffset; }); it != cached_fields.end()) @@ -804,7 +1032,7 @@ namespace { } if (!cached_datamap_fields.empty()) { - if (class_info.m_nFieldSize) + if (class_.m_nFieldSize) builder.next_line(); builder.comment("Datamap fields:"); @@ -814,14 +1042,47 @@ namespace { } } - if (!class_info.m_nFieldSize && !class_info.m_nStaticMetadataSize) + if (!class_.m_nFieldSize && !class_.m_nStaticMetadataSize) builder.comment("No schema binary for binding"); builder.end_block(); + builder.pack_pop(); + builder.next_line(); + + const bool is_standard_layout_class = IsStandardLayoutClass(cache.class_has_standard_layout, class_); + + // TODO: when we have a CLI parser: allow users to generate assertions in non-standard-layout classes. Those assertions are + // conditionally-supported by compilers. + if (is_standard_layout_class) { + for (const auto& field : + class_.GetFields() | std::ranges::views::filter([&](const auto& e) { return !skipped_fields.contains(e.m_pszName); })) { + if (field.m_pSchemaType->m_unTypeCategory == ETypeCategory::Schema_Bitfield) { + builder.comment(std::format("Cannot assert offset of bitfield {}::{}", class_.m_pszName, field.m_pszName)); + } else { + builder.static_assert_offset(class_.m_pszName, field.m_pszName, field.m_nSingleInheritanceOffset); + } + } + } else { + if (class_.m_nFieldSize != 0) { + builder.comment(std::format("Cannot assert offsets of fields in {} because it is not a standard-layout class", class_.m_pszName)); + } + } + + if (!class_is_aligned) { + builder.end_multi_line_comment(); + } + + builder.next_line(); + builder.static_assert_size(class_.m_pszName, class_size); + } + + [[nodiscard]] + std::filesystem::path GetFilePathForType(std::string_view module_name, std::string_view type_name) { + return util::EscapePath(std::format("{}/include/{}/{}/{}.hpp", kOutDirName, kIncludeDirName, module_name, DecayTypeName(type_name))); } void GenerateEnumSdk(std::string_view module_name, const CSchemaEnumBinding& enum_) { - const std::string out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, enum_.m_pszName)); + const auto out_file_path = GetFilePathForType(module_name, enum_.m_pszName).string(); // @note: @es3n1n: init codegen // @@ -851,7 +1112,7 @@ namespace { // std::ofstream f(out_file_path, std::ios::out); f << builder.str(); - if (f.bad()) { + if (!f.good()) { 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 @@ -861,8 +1122,8 @@ namespace { } } - void GenerateClassSdk(std::string_view module_name, const CSchemaClassBinding& class_) { - const std::filesystem::path out_file_path = util::EscapePath(std::format("{}/{}/{}.hpp", kOutDirName, module_name, class_.m_pszName)); + void GenerateClassSdk(sdk::GeneratorCache& cache, std::string_view module_name, const CSchemaClassBinding& class_) { + const auto out_file_path = GetFilePathForType(module_name, class_.m_pszName).string(); // @note: @es3n1n: init codegen // @@ -872,19 +1133,14 @@ 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, util::EscapePath(include.type_name))); + builder.include(util::EscapePath(std::format("\"{}/{}/{}.hpp\"", kIncludeDirName, include.module, include.type_name))); } + builder.include(std::format("\"{}/source2gen.hpp\"", kIncludeDirName)); + builder.include(""); // for offsetof() builder.include(""); - for (const auto& forward_declaration : names | std::views::filter([](const auto& el) { return el.source == NameSource::forward_declaration; })) { - builder.begin_namespace(std::format("source2sdk::{}", forward_declaration.module)); - builder.forward_declaration(forward_declaration.type_name); - builder.end_namespace(); - } - - // @note: @es3n1n: print banner - // + /// print banner builder.next_line() .comment("/////////////////////////////////////////////////////////////") .comment(std::format("Module: {}", module_name)) @@ -892,11 +1148,19 @@ namespace { .comment("/////////////////////////////////////////////////////////////") .next_line(); + /// Insert forward declarations + 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(); + builder.next_line(); + } + builder.begin_namespace(std::format("source2sdk::{}", module_name)); // @note: @es3n1n: assemble props // - AssembleClass(builder, class_); + AssembleClass(cache, builder, class_); builder.end_namespace(); @@ -904,8 +1168,8 @@ namespace { // 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.string(), std::strerror(errno)) << std::endl; + if (!f.good()) { + 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 @@ -916,20 +1180,20 @@ namespace { } // namespace namespace sdk { - void GenerateTypeScopeSdk(std::string_view module_name, const std::unordered_set& enums, + void GenerateTypeScopeSdk(GeneratorCache& cache, 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); + const std::filesystem::path out_directory_path = std::format("{}/include/{}/{}", kOutDirName, kIncludeDirName, 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); }); + std::ranges::for_each(classes, [&](const auto* el) { GenerateClassSdk(cache, module_name, *el); }); } } // namespace sdk diff --git a/source2gen/src/startup/startup.cpp b/source2gen/src/startup/startup.cpp index 4113d53..dcb7977 100644 --- a/source2gen/src/startup/startup.cpp +++ b/source2gen/src/startup/startup.cpp @@ -174,8 +174,10 @@ namespace source2_gen { const std::unordered_map all_modules = collect_modules(std::span{type_scopes.m_pElements, static_cast(type_scopes.m_Size)}); + sdk::GeneratorCache cache{}; + for (const auto& [module_name, dump] : all_modules) { - sdk::GenerateTypeScopeSdk(module_name, dump.enums, dump.classes); + sdk::GenerateTypeScopeSdk(cache, module_name, dump.enums, dump.classes); } std::cout << std::format("Schema stats: {} registrations; {} were redundant; {} were ignored ({} bytes of ignored data)", diff --git a/source2gen/src/tools/field_parser.cpp b/source2gen/src/tools/field_parser.cpp index 93f917e..ec7530f 100644 --- a/source2gen/src/tools/field_parser.cpp +++ b/source2gen/src/tools/field_parser.cpp @@ -11,19 +11,28 @@ namespace field_parser { constexpr std::string_view kBitfieldTypePrefix = "bitfield:"sv; + // @note: @es3n1n: a list of possible integral types for bitfields (would be used in `guess_bitfield_type`) + // + constexpr auto kBitfieldIntegralTypes = std::to_array>({ + {8, "uint8_t"}, + {16, "uint16_t"}, + {32, "uint32_t"}, + {64, "uint64_t"}, + }); + // clang-format off constexpr auto kTypeNameToCpp = std::to_array>({ - {"float32"sv, "float"sv}, + {"float32"sv, "float"sv}, {"float64"sv, "double"sv}, - - {"int8"sv, "int8_t"sv}, - {"int16"sv, "int16_t"sv}, - {"int32"sv, "int32_t"sv}, + + {"int8"sv, "int8_t"sv}, + {"int16"sv, "int16_t"sv}, + {"int32"sv, "int32_t"sv}, {"int64"sv, "int64_t"sv}, - - {"uint8"sv, "uint8_t"sv}, - {"uint16"sv, "uint16_t"sv}, - {"uint32"sv, "uint32_t"sv}, + + {"uint8"sv, "uint8_t"sv}, + {"uint16"sv, "uint16_t"sv}, + {"uint32"sv, "uint32_t"sv}, {"uint64"sv, "uint64_t"sv} }); @@ -98,7 +107,7 @@ namespace field_parser { // @note: @es3n1n: saving parsed value result.m_bitfield_size = bitfield_size; - result.m_type = codegen::guess_bitfield_type(bitfield_size); + result.m_type = guess_bitfield_type(bitfield_size); } // @note: @es3n1n: we are assuming that this function would be executed right after @@ -135,6 +144,17 @@ namespace field_parser { } } // namespace detail + std::string guess_bitfield_type(const std::size_t bits_count) { + for (auto p : detail::kBitfieldIntegralTypes) { + if (bits_count > p.first) + continue; + + return p.second.data(); + } + + throw std::runtime_error(std::format("{} : Unable to guess bitfield type with size {}", __FUNCTION__, bits_count)); + } + 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()) {