diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d38533..efd6b39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,14 +11,22 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/dllmain.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Loader/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Loader/Blueprint/PalBlueprintMod.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Loader/Spawner/PalListInfo.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Loader/Spawner/PalSpawnGroupListInfo.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Loader/Spawner/SpawnerInfo.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Classes/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Classes/Custom/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Classes/Custom/DataTable/TableSerializer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Helper/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Structs/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Structs/Reflected/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Structs/Custom/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Structs/Custom/DataTable/FTableSerializerRow.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Tools/EnumSchemaDefinitionGenerator.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Utility/Config.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Utility/JsonHelpers.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Utility/EnumHelpers.cpp" ) add_library(${TARGET} SHARED ${SRC_FILES}) diff --git a/assets/examples/NewSpawnsAtPlateau/spawns/new_spawns.jsonc b/assets/examples/NewSpawnsAtPlateau/spawns/new_spawns.jsonc new file mode 100644 index 0000000..1eeb6d0 --- /dev/null +++ b/assets/examples/NewSpawnsAtPlateau/spawns/new_spawns.jsonc @@ -0,0 +1,58 @@ +[ + { + "Type": "MonoNPC", // Type should be either MonoNPC or Sheet + "NPCID": "PalDealer", + "OtomoId": "PinkCat", // This is the Pal that the NPC summons when it is attacked + "Level": 2, + "Location": { "X": -342040.178, "Y": 264211.779, "Z": 4050.933 }, + "Rotation": { "Pitch": 0.0, "Yaw": 0.0, "Roll": 0.0 } + }, + { + "Type": "Sheet", + "Location": { "X": -342040.178, "Y": 263611.779, "Z": 4050.933 }, + "Rotation": { "Pitch": 0.0, "Yaw": 0.0, "Roll": 0.0 }, + "SpawnerName": "PalSchema_Custom_Spawn001", + "SpawnGroupList": [ + { + "Weight": 50, + "PalList": [ + { + "PalId": "Plesiosaur", + "Level": 25, + "Level_Max": 30, + "Num": 1, + "Num_Max": 1 + }, + { + "PalId": "SheepBall", + "Level": 1, + "Level_Max": 5, + "Num": 1, + "Num_Max": 2 + } + ] + } + ] + }, + { + "Type": "Sheet", + "Location": { "X": -342040.178, "Y": 262811.779, "Z": 4050.933 }, + "Rotation": { "Pitch": 0.0, "Yaw": 0.0, "Roll": 0.0 }, + "SpawnerName": "PalSchema_Custom_Boss_Spawn001", + "SpawnerType": "FieldBoss", // Should be FieldBoss if you want it to be a proper boss + "SpawnGroupList": [ + { + "Weight": 50, + "PalList": [ + { + "PalId": "BOSS_DarkMechaDragon", + "Level": 25, + "Level_Max": 25, + "Num": 1, + "Num_Max": 1 + } + ] + } + ] + } +] \ No newline at end of file diff --git a/include/Loader/PalHumanModLoader.h b/include/Loader/PalHumanModLoader.h index 358f219..51ad4aa 100644 --- a/include/Loader/PalHumanModLoader.h +++ b/include/Loader/PalHumanModLoader.h @@ -1,5 +1,6 @@ #pragma once +#include #include "Loader/PalModLoaderBase.h" #include "nlohmann/json.hpp" @@ -17,7 +18,7 @@ namespace Palworld { void Initialize(); virtual void Load(const nlohmann::json& json) override final; - + private: void Add(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); void Edit(uint8_t* TableRow, const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); @@ -34,16 +35,16 @@ namespace Palworld { void AddShop(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); - RC::Unreal::UDataTable* n_dataTable; - RC::Unreal::UDataTable* n_iconDataTable; - RC::Unreal::UDataTable* n_palBpClassTable; - RC::Unreal::UDataTable* n_dropItemTable; - RC::Unreal::UDataTable* n_npcNameTable; - RC::Unreal::UDataTable* n_palShortDescTable; - RC::Unreal::UDataTable* n_palLongDescTable; - RC::Unreal::UDataTable* n_npcTalkFlowTable; - RC::Unreal::UDataTable* n_ItemShopLotteryDataTable; - RC::Unreal::UDataTable* n_ItemShopCreateDataTable; - RC::Unreal::UDataTable* n_ItemShopSettingDataTable; + RC::Unreal::UDataTable* n_dataTable = nullptr; + RC::Unreal::UDataTable* n_iconDataTable = nullptr; + RC::Unreal::UDataTable* n_palBpClassTable = nullptr; + RC::Unreal::UDataTable* n_dropItemTable = nullptr; + RC::Unreal::UDataTable* n_npcNameTable = nullptr; + RC::Unreal::UDataTable* n_palShortDescTable = nullptr; + RC::Unreal::UDataTable* n_palLongDescTable = nullptr; + RC::Unreal::UDataTable* n_npcTalkFlowTable = nullptr; + RC::Unreal::UDataTable* n_ItemShopLotteryDataTable = nullptr; + RC::Unreal::UDataTable* n_ItemShopCreateDataTable = nullptr; + RC::Unreal::UDataTable* n_ItemShopSettingDataTable = nullptr; }; } \ No newline at end of file diff --git a/include/Loader/PalMainLoader.h b/include/Loader/PalMainLoader.h index 5d777b3..39c84d5 100644 --- a/include/Loader/PalMainLoader.h +++ b/include/Loader/PalMainLoader.h @@ -14,6 +14,7 @@ #include "Loader/PalEnumLoader.h" #include "Loader/PalHelpGuideModLoader.h" #include "Loader/PalResourceLoader.h" +#include "Loader/PalSpawnLoader.h" #include "FileWatch.hpp" namespace RC::Unreal { @@ -23,6 +24,7 @@ namespace RC::Unreal { namespace UECustom { class UCompositeDataTable; + class UWorldPartitionRuntimeLevelStreamingCell; } namespace Palworld { @@ -53,9 +55,16 @@ namespace Palworld { PalEnumLoader EnumLoader; PalHelpGuideModLoader HelpGuideModLoader; PalResourceLoader ResourceLoader; + PalSpawnLoader SpawnLoader; std::unique_ptr> m_fileWatch; + void HookDatatableSerialize(); + + void HookStaticItemDataTable_Get(); + + void HookWorldCleanup(); + void SetupAutoReload(); // Makes PalSchema read paks from the 'PalSchema/mods' folder. Although the paks can be anywhere, prefer for them to be put inside 'YourModName/paks'. @@ -90,11 +99,14 @@ namespace Palworld { static RC::Unreal::UObject* StaticItemDataTable_Get(UPalStaticItemDataTable* This, RC::Unreal::FName ItemId); + static void UWorld_CleanupWorld(RC::Unreal::UWorld* This, bool bSessionEnded, bool bCleanupResources, RC::Unreal::UWorld* NewWorld); + bool m_hasInit = false; static inline std::vector> DatatableSerializeCallbacks; static inline std::vector> GameInstanceInitCallbacks; static inline std::vector> PostLoadCallbacks; + static inline std::vector> WorldCleanUp_Callbacks; static inline std::vector> GetPakFoldersCallback; static inline SafetyHookInline DatatableSerialize_Hook; @@ -102,5 +114,6 @@ namespace Palworld { static inline SafetyHookInline PostLoad_Hook; static inline SafetyHookInline GetPakFolders_Hook; static inline SafetyHookInline StaticItemDataTable_Get_Hook; + static inline SafetyHookInline WorldCleanUp_Hook; }; } \ No newline at end of file diff --git a/include/Loader/PalSpawnLoader.h b/include/Loader/PalSpawnLoader.h new file mode 100644 index 0000000..b1694ed --- /dev/null +++ b/include/Loader/PalSpawnLoader.h @@ -0,0 +1,64 @@ +#pragma once + +#include "Unreal/NameTypes.hpp" +#include "Unreal/UnrealCoreStructs.hpp" +#include "Unreal/Rotator.hpp" +#include "Loader/PalModLoaderBase.h" +#include "Loader/Spawner/SpawnerInfo.h" +#include "nlohmann/json.hpp" + +namespace RC::Unreal { + class UWorld; + class UDataTable; +} + +namespace UECustom { + class UWorldPartitionRuntimeLevelStreamingCell; +} + +namespace Palworld { + class AMonoNPCSpawner; + + class PalSpawnLoader : public PalModLoaderBase { + public: + PalSpawnLoader(); + + ~PalSpawnLoader(); + + void Initialize(); + + void Load(const std::filesystem::path::string_type& modName, const nlohmann::json& data); + + void Reload(const std::filesystem::path::string_type& modName, const nlohmann::json& data); + + void OnWorldCleanup(RC::Unreal::UWorld* World); + + // This is called whenever a world partition is loaded within the main world. + void OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + + // This is called whenever a world partition is unloaded within the main world. + void OnCellUnloaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + private: + RC::Unreal::UDataTable* m_bossSpawnerLocationData = nullptr; + + // Storing loaded cells here for mod reloading purposes. + RC::Unreal::TArray m_loadedCells; + std::vector m_spawns; + + void RegisterSpawn(const std::filesystem::path::string_type& modName, const nlohmann::json& value); + void RegisterSheet(const std::filesystem::path::string_type& modName, PS::SpawnerInfo& spawnerInfo, const nlohmann::json& value); + void RegisterMonoNPC(const std::filesystem::path::string_type& modName, PS::SpawnerInfo& spawnerInfo, const nlohmann::json& value); + + void ProcessCellSpawners(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + void CreateSpawner(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo); + void SpawnMonoNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo); + void SpawnSheet(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo); + + void AddBossSpawnLocationToMap(PS::SpawnerInfo& spawnerInfo); + void RemoveBossSpawnLocationFromMap(PS::SpawnerInfo& spawnerInfo); + + void DestroySpawnersInCell(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + + void UnloadMod(const std::filesystem::path::string_type& modName); + }; +} \ No newline at end of file diff --git a/include/Loader/Spawner/PalSpawnGroupListInfo.h b/include/Loader/Spawner/PalSpawnGroupListInfo.h new file mode 100644 index 0000000..a1a2880 --- /dev/null +++ b/include/Loader/Spawner/PalSpawnGroupListInfo.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "SDK/Structs/PalListInfo.h" +#include "nlohmann/json_fwd.hpp" + +namespace PS { + struct PalSpawnGroupListInfo { + int Weight = 10; + RC::Unreal::uint8 OnlyTime = 0; + RC::Unreal::uint8 OnlyWeather = 0; + std::vector PalList; + + void AddPalListInfo(const nlohmann::json& value); + }; +} \ No newline at end of file diff --git a/include/Loader/Spawner/SpawnerInfo.h b/include/Loader/Spawner/SpawnerInfo.h new file mode 100644 index 0000000..26ad253 --- /dev/null +++ b/include/Loader/Spawner/SpawnerInfo.h @@ -0,0 +1,63 @@ +#pragma once + +#include "Unreal/NameTypes.hpp" +#include "Unreal/UnrealCoreStructs.hpp" +#include "Unreal/Rotator.hpp" +#include "SDK/Structs/Guid.h" +#include "Loader/Spawner/PalSpawnGroupListInfo.h" +#include "nlohmann/json_fwd.hpp" + +namespace RC::Unreal { + class UWorld; +} + +namespace UECustom { + class UWorldPartitionRuntimeLevelStreamingCell; +} + +namespace PS { + class AMonoNPCSpawner; + + enum class SpawnerType : RC::Unreal::uint8 { + Undefined, + Sheet, + MonoNPC + }; + + struct SpawnerInfo { + bool bExistsInWorld = false; + RC::Unreal::AActor* SpawnerActor = nullptr; + UECustom::UWorldPartitionRuntimeLevelStreamingCell* Cell = nullptr; + RC::StringType ModName{}; + UECustom::FGuid Guid; + + // Generic + + SpawnerType Type = SpawnerType::Undefined; + RC::Unreal::FVector Location; + RC::Unreal::FRotator Rotation; + + // MonoNPCSpawner + + int Level = 1; + RC::Unreal::FName NPCID = RC::Unreal::NAME_None; + RC::Unreal::FName OtomoName = RC::Unreal::NAME_None; + + // Sheet + + RC::Unreal::FName SpawnerName = RC::Unreal::NAME_None; + RC::Unreal::uint8 SpawnerType = 0; + std::vector SpawnGroupList; + bool bHasMapIcon = false; + + void Unload(); + + void AddSpawnGroupList(const nlohmann::json& value); + + RC::StringType ToString(); + private: + RC::StringType CachedString = TEXT(""); + + RC::Unreal::uint8 GetOnlyTimeFromString(const std::string& str); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/AMonoNPCSpawner.h b/include/SDK/Classes/AMonoNPCSpawner.h new file mode 100644 index 0000000..525b3da --- /dev/null +++ b/include/SDK/Classes/AMonoNPCSpawner.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Unreal/AActor.hpp" + +namespace Palworld { + class AMonoNPCSpawner : public RC::Unreal::AActor { + public: + int& GetLevel(); + + RC::Unreal::FName& GetHumanName(); + + RC::Unreal::FName& GetCharaName(); + + RC::Unreal::FName& GetOtomoName(); + + void Spawn(); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/APalSpawnerStandard.h b/include/SDK/Classes/APalSpawnerStandard.h new file mode 100644 index 0000000..55ddb74 --- /dev/null +++ b/include/SDK/Classes/APalSpawnerStandard.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Unreal/AActor.hpp" +#include "SDK/Structs/PalSpawnerGroup.h" + +namespace Palworld { + class APalSpawnerStandard : public RC::Unreal::AActor { + public: + void AddSpawnerGroup(const PalSpawnerGroup& spawnerGroup); + + void SetSpawnerName(const RC::Unreal::FName& spawnerName); + + void SetSpawnerType(const RC::Unreal::uint8& spawnerType); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/Custom/DataTable/TableSerializer.h b/include/SDK/Classes/Custom/DataTable/TableSerializer.h new file mode 100644 index 0000000..4123340 --- /dev/null +++ b/include/SDK/Classes/Custom/DataTable/TableSerializer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Unreal/NameTypes.hpp" +#include "SDK/Structs/Custom/DataTable/FTableSerializerRow.h" + +namespace RC::Unreal { + class UDataTable; +} + +namespace Palworld { + class TableSerializer { + public: + TableSerializer(RC::Unreal::UDataTable* table); + + FTableSerializerRow* Add(const RC::Unreal::FName& rowId); + + FTableSerializerRow* Edit(const RC::Unreal::FName& rowId); + private: + RC::Unreal::UDataTable* m_table = nullptr; + std::vector> m_rows; + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/Custom/UObjectGlobals.h b/include/SDK/Classes/Custom/UObjectGlobals.h index ba58b4a..b9c0fcb 100644 --- a/include/SDK/Classes/Custom/UObjectGlobals.h +++ b/include/SDK/Classes/Custom/UObjectGlobals.h @@ -3,14 +3,15 @@ #include "Unreal/UObject.hpp" namespace UECustom { + namespace UObjectGlobals { RC::Unreal::UObject* StaticFindObject(RC::Unreal::UClass* ObjectClass, RC::Unreal::UObject* InObjectPackage, const RC::Unreal::TCHAR* OrigInName = nullptr, bool bExactClass = false); - template - T StaticFindObject(RC::Unreal::UClass* ObjectClass, RC::Unreal::UObject* InObjectPackage, const RC::Unreal::TCHAR* OrigInName = nullptr, bool bExactClass = false) + template + ObjectType StaticFindObject(RC::Unreal::UClass* ObjectClass, RC::Unreal::UObject* InObjectPackage, const RC::Unreal::TCHAR* OrigInName = nullptr, bool bExactClass = false) { auto Object = StaticFindObject(ObjectClass, InObjectPackage, OrigInName, bExactClass); - return reinterpret_cast(Object); + return static_cast(Object); } void GetObjectsOfClass(const RC::Unreal::UClass* ClassToLookFor, RC::Unreal::TArray& Results, diff --git a/include/SDK/Classes/KismetGuidLibrary.h b/include/SDK/Classes/KismetGuidLibrary.h new file mode 100644 index 0000000..9a17d6a --- /dev/null +++ b/include/SDK/Classes/KismetGuidLibrary.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Unreal/UObject.hpp" +#include "SDK/Structs/Guid.h" + +namespace UECustom { + class UKismetGuidLibrary : public RC::Unreal::UObject { + public: + static UECustom::FGuid NewGuid(); + + static RC::Unreal::FString Conv_GuidToString(const UECustom::FGuid& Guid); + private: + static UKismetGuidLibrary* GetDefaultObj(); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/ULevelStreaming.h b/include/SDK/Classes/ULevelStreaming.h new file mode 100644 index 0000000..f7b1551 --- /dev/null +++ b/include/SDK/Classes/ULevelStreaming.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Unreal/UObject.hpp" +#include "Unreal/FWeakObjectPtr.hpp" + +namespace UECustom { + class UWorldPartition; + + class ULevelStreaming : public RC::Unreal::UObject { + public: + UWorldPartition* GetOuterWorldPartition(); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/UWorldPartition.h b/include/SDK/Classes/UWorldPartition.h new file mode 100644 index 0000000..e51b25f --- /dev/null +++ b/include/SDK/Classes/UWorldPartition.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Unreal/UObject.hpp" + +namespace RC::Unreal { + class UWorld; +} + +namespace UECustom { + class UWorldPartition : public RC::Unreal::UObject { + public: + RC::Unreal::UWorld* GetWorld(); + }; +} \ No newline at end of file diff --git a/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h b/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h new file mode 100644 index 0000000..26c7d9c --- /dev/null +++ b/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Unreal/UObject.hpp" +#include "SDK/Structs/FBox.h" + +namespace UECustom { + class ULevelStreaming; + + class UWorldPartitionRuntimeLevelStreamingCell : public RC::Unreal::UObject { + public: + FBox& GetContentBounds(); + + ULevelStreaming* GetLevelStreaming(); + + bool& GetIsHLOD(); + + RC::Unreal::FVector& GetPosition(); + + float& GetExtent(); + + int& GetLevel(); + }; +} \ No newline at end of file diff --git a/include/SDK/PalSignatures.h b/include/SDK/PalSignatures.h index bb132a1..835c951 100644 --- a/include/SDK/PalSignatures.h +++ b/include/SDK/PalSignatures.h @@ -42,6 +42,7 @@ namespace Palworld { // Raw Tables { "UDataTable::Serialize", "E8 ?? ?? ?? ?? 41 F6 06 01 0F 84 1D 03 00 00 48 89 5C 24 50" }, { "UPalStaticItemDataTable::Get", "E8 ?? ?? ?? ?? 48 85 C0 74 0B 8B 40" }, + { "UWorld::CleanupWorld", "E8 ?? ?? ?? ?? 8B 55 A7 FF C2 49 83 C5 08 89 55 A7" }, }; }; } \ No newline at end of file diff --git a/include/SDK/Structs/Custom/DataTable/FTableSerializerRow.h b/include/SDK/Structs/Custom/DataTable/FTableSerializerRow.h new file mode 100644 index 0000000..4ca9500 --- /dev/null +++ b/include/SDK/Structs/Custom/DataTable/FTableSerializerRow.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Helpers/String.hpp" +#include "Unreal/UClass.hpp" +#include "Unreal/NameTypes.hpp" +#include "Unreal/UScriptStruct.hpp" +#include "Unreal/FProperty.hpp" +#include "SDK/Helper/PropertyHelper.h" + +namespace RC::Unreal { + class UDataTable; +} + +namespace Palworld { + // When adding a row, the row is added to the table once this object destructs. + // When editing a row, changes are applied to the table immediately. + struct FTableSerializerRow { + public: + enum class ETableSerializeMode { + None, + Add, + Edit + }; + + ~FTableSerializerRow(); + FTableSerializerRow(RC::Unreal::UDataTable* table, const RC::Unreal::FName& rowId, ETableSerializeMode mode); + + template + void SetValue(const RC::StringType& propertyName, const T& value) + { + auto property = PropertyHelper::GetPropertyByName(m_struct, propertyName); + if (property && m_data) + { + auto valuePtr = property->ContainerPtrToValuePtr(m_data); + RC::Unreal::FMemory::Memcpy(valuePtr, &value, sizeof(T)); + } + } + private: + RC::Unreal::UDataTable* m_table = nullptr; + RC::Unreal::UScriptStruct* m_struct = nullptr; + RC::Unreal::FName m_rowId = RC::Unreal::NAME_None; + void* m_data = nullptr; + ETableSerializeMode m_mode = ETableSerializeMode::None; + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/FBox.h b/include/SDK/Structs/FBox.h new file mode 100644 index 0000000..bd9c5eb --- /dev/null +++ b/include/SDK/Structs/FBox.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Unreal/UnrealCoreStructs.hpp" + +namespace UECustom { + struct FBox { + RC::Unreal::FVector Min; + RC::Unreal::FVector Max; + bool IsValid; + + inline bool IsInside(const RC::Unreal::FVector& In) const + { + return ((In.X() > Min.X()) && (In.X() < Max.X()) && (In.Y() > Min.Y()) && (In.Y() < Max.Y()) && (In.Z() > Min.Z()) && (In.Z() < Max.Z())); + } + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/FVector.h b/include/SDK/Structs/FVector.h new file mode 100644 index 0000000..e9b00b3 --- /dev/null +++ b/include/SDK/Structs/FVector.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Unreal/UnrealCoreStructs.hpp" + +namespace UECustom { + // Custom implementation that allows it to be used in std::set + // RC::Unreal::TSet would've been better, but it seems like UE4SS doesn't have a GetTypeHash override for RC::Unreal::FVector ? + struct FVector { + FVector(const double& x, const double& y, const double& z) : X(x), Y(y), Z(z) {}; + FVector(const RC::Unreal::FVector& vector) : X(vector.X()), Y(vector.Y()), Z(vector.Z()) {}; + + double X; + double Y; + double Z; + + bool operator<(const FVector& Other) const + { + if (X != Other.X) return X < Other.X; + if (Y != Other.Y) return Y < Other.Y; + return Z < Other.Z; + } + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/Guid.h b/include/SDK/Structs/Guid.h new file mode 100644 index 0000000..9ae42d7 --- /dev/null +++ b/include/SDK/Structs/Guid.h @@ -0,0 +1,41 @@ +#pragma once + +namespace UECustom { + struct FGuid { + int A = 0; + int B = 0; + int C = 0; + int D = 0; + + friend bool operator==(const UECustom::FGuid& X, const UECustom::FGuid& Y) + { + return ((X.A ^ Y.A) | (X.B ^ Y.B) | (X.C ^ Y.C) | (X.D ^ Y.D)) == 0; + } + + friend bool operator!=(const UECustom::FGuid& X, const UECustom::FGuid& Y) + { + return ((X.A ^ Y.A) | (X.B ^ Y.B) | (X.C ^ Y.C) | (X.D ^ Y.D)) != 0; + } + + friend bool operator<(const UECustom::FGuid& X, const UECustom::FGuid& Y) + { + return ((X.A < Y.A) ? true : ((X.A > Y.A) ? false : + ((X.B < Y.B) ? true : ((X.B > Y.B) ? false : + ((X.C < Y.C) ? true : ((X.C > Y.C) ? false : + ((X.D < Y.D) ? true : ((X.D > Y.D) ? false : false)))))))); + } + }; +} + +namespace std { + template <> + struct hash { + std::size_t operator()(const UECustom::FGuid& Guid) const noexcept { + std::size_t h1 = std::hash()(Guid.A); + std::size_t h2 = std::hash()(Guid.B); + std::size_t h3 = std::hash()(Guid.C); + std::size_t h4 = std::hash()(Guid.D); + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3); + } + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/PalListInfo.h b/include/SDK/Structs/PalListInfo.h new file mode 100644 index 0000000..a5f837f --- /dev/null +++ b/include/SDK/Structs/PalListInfo.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Unreal/NameTypes.hpp" + +namespace Palworld { + // Do not pass to UE internals, see FPalSpawnerOneTribeInfo and FPalSpawnerGroupInfo instead in /Reflected + struct PalListInfo { + RC::Unreal::FName PalId = RC::Unreal::NAME_None; + RC::Unreal::FName NPCID = RC::Unreal::NAME_None; + int Level = 1; + int LevelMax = 1; + int Num = 1; + int NumMax = 1; + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/PalSpawnerGroup.h b/include/SDK/Structs/PalSpawnerGroup.h new file mode 100644 index 0000000..98e86c6 --- /dev/null +++ b/include/SDK/Structs/PalSpawnerGroup.h @@ -0,0 +1,14 @@ +#pragma once + +#include "SDK/Structs/PalListInfo.h" + +namespace Palworld { + struct PalSpawnerGroup { + RC::Unreal::FName OriginalRowName = RC::Unreal::NAME_None; + RC::Unreal::FName OriginalSpawnerName = RC::Unreal::NAME_None; + int Weight = 10; + RC::Unreal::uint8 OnlyTime = 0; + RC::Unreal::uint8 OnlyWeather = 0; + std::vector PalList; + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/Reflected/BaseReflectedStruct.h b/include/SDK/Structs/Reflected/BaseReflectedStruct.h new file mode 100644 index 0000000..34c7129 --- /dev/null +++ b/include/SDK/Structs/Reflected/BaseReflectedStruct.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include "Unreal/FProperty.hpp" +#include "Unreal/UScriptStruct.hpp" +#include "Helpers/String.hpp" +#include "SDK/Helper/PropertyHelper.h" +#include "SDK/Structs/Custom/FScriptArrayHelper.h" + +namespace UECustom { + struct BaseReflectedStruct { + public: + BaseReflectedStruct(RC::Unreal::UScriptStruct* scriptStruct); + BaseReflectedStruct(RC::Unreal::UScriptStruct* scriptStruct, void* data); + virtual ~BaseReflectedStruct(); + + void DestroyStruct(); + + void* GetData(); + + virtual RC::Unreal::UScriptStruct* StaticStruct() = 0; + protected: + template + void SetPropertyValue(RC::Unreal::FProperty* property, const T& value) + { + auto valuePtr = property->ContainerPtrToValuePtr(m_data); + *valuePtr = value; + } + + template + T GetPropertyValue(RC::Unreal::FProperty* property) + { + auto valuePtr = property->ContainerPtrToValuePtr(m_data); + return *valuePtr; + } + + // This will return a nullptr if the property doesn't exist. + RC::Unreal::FProperty* GetProperty(const RC::StringType& propertyName) + { + auto property = Palworld::PropertyHelper::GetPropertyByName(m_scriptStruct, propertyName); + if (!property) + { + return nullptr; + } + + return property; + } + + // This will throw an error if the property doesn't exist or if the type didn't match what was supplied. + template + T* GetPropertyChecked(const RC::StringType& propertyName) + { + auto property = Palworld::PropertyHelper::GetPropertyByName(m_scriptStruct, propertyName); + if (!property) + { + throw std::runtime_error(RC::fmt("Property '%S' does not exist in struct '%S'.", propertyName.c_str(), + m_scriptStruct->GetNamePrivate().ToString().c_str())); + } + + T* returnValue = RC::Unreal::CastField(property); + if (!returnValue) + { + throw std::runtime_error(RC::fmt("Property '%S' has the wrong type, expected '%S'.", propertyName.c_str(), *property->GetCPPType())); + } + + return returnValue; + } + + std::unique_ptr GetArrayPropertyValue(RC::Unreal::FProperty* property); + private: + RC::Unreal::UScriptStruct* m_scriptStruct = nullptr; + void* m_data = nullptr; + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/Reflected/PalSpawnerGroupInfo.h b/include/SDK/Structs/Reflected/PalSpawnerGroupInfo.h new file mode 100644 index 0000000..75a4f68 --- /dev/null +++ b/include/SDK/Structs/Reflected/PalSpawnerGroupInfo.h @@ -0,0 +1,19 @@ +#pragma once + +#include "SDK/Structs/Reflected/BaseReflectedStruct.h" +#include "SDK/Structs/PalListInfo.h" + +namespace Palworld { + struct FPalSpawnerGroupInfo : UECustom::BaseReflectedStruct { + public: + FPalSpawnerGroupInfo(); + FPalSpawnerGroupInfo(void* Data); + + void SetWeight(int value); + void SetOnlyTime(RC::Unreal::uint8 value); + void SetOnlyWeather(RC::Unreal::uint8 value); + void AddPal(const PalListInfo& value); + + virtual RC::Unreal::UScriptStruct* StaticStruct() final; + }; +} \ No newline at end of file diff --git a/include/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h b/include/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h new file mode 100644 index 0000000..df07398 --- /dev/null +++ b/include/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h @@ -0,0 +1,20 @@ +#pragma once + +#include "SDK/Structs/Reflected/BaseReflectedStruct.h" + +namespace Palworld { + struct FPalSpawnerOneTribeInfo : UECustom::BaseReflectedStruct { + public: + FPalSpawnerOneTribeInfo(); + FPalSpawnerOneTribeInfo(void* Data); + + void SetPalId(const RC::Unreal::FName& value); + void SetNPCID(const RC::Unreal::FName& value); + void SetLevel(int value); + void SetLevelMax(int value); + void SetNum(int value); + void SetNumMax(int value); + + virtual RC::Unreal::UScriptStruct* StaticStruct() final; + }; +} \ No newline at end of file diff --git a/include/Utility/EnumHelpers.h b/include/Utility/EnumHelpers.h new file mode 100644 index 0000000..d2a94c5 --- /dev/null +++ b/include/Utility/EnumHelpers.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Unreal/Core/HAL/Platform.hpp" +#include "Unreal/NameTypes.hpp" + +namespace RC::Unreal { + class UEnum; +} + +namespace PS::EnumHelpers { + RC::Unreal::int64 GetEnumValueByName(RC::Unreal::UEnum* enum_, const std::string& enumString); +} \ No newline at end of file diff --git a/include/Utility/JsonHelpers.h b/include/Utility/JsonHelpers.h new file mode 100644 index 0000000..a3287a4 --- /dev/null +++ b/include/Utility/JsonHelpers.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "nlohmann/json_fwd.hpp" +#include "Unreal/Core/HAL/Platform.hpp" + +namespace RC::Unreal { + struct FRotator; + struct FVector; + class FName; +} + +namespace PS::JsonHelpers { + bool FieldExists(const nlohmann::json& data, const std::string& fieldName); + void ValidateFieldExists(const nlohmann::json& data, const std::string& fieldName); + + void ParseRotator(const nlohmann::json& value, const std::string& fieldName, RC::Unreal::FRotator& outValue); + void ParseVector(const nlohmann::json& value, const std::string& fieldName, RC::Unreal::FVector& outValue); + void ParseFName(const nlohmann::json& value, const std::string& fieldName, RC::Unreal::FName& outValue); + void ParseDouble(const nlohmann::json& value, const std::string& fieldName, double& outValue); + void ParseInteger(const nlohmann::json& value, const std::string& fieldName, int& outValue); + void ParseUInt8(const nlohmann::json& value, const std::string& fieldName, RC::Unreal::uint8& outValue); + void ParseString(const nlohmann::json& value, const std::string& fieldName, std::string& outValue); +} \ No newline at end of file diff --git a/src/Loader/PalEnumLoader.cpp b/src/Loader/PalEnumLoader.cpp index bddfd02..9529bbd 100644 --- a/src/Loader/PalEnumLoader.cpp +++ b/src/Loader/PalEnumLoader.cpp @@ -1,4 +1,5 @@ #include "Loader/PalEnumLoader.h" +#include "Unreal/UClass.hpp" #include "Unreal/UEnum.hpp" #include "Helpers/String.hpp" #include "Utility/Logging.h" diff --git a/src/Loader/PalHumanModLoader.cpp b/src/Loader/PalHumanModLoader.cpp index e4ce078..04bf04a 100644 --- a/src/Loader/PalHumanModLoader.cpp +++ b/src/Loader/PalHumanModLoader.cpp @@ -1,11 +1,15 @@ +#include "Unreal/UClass.hpp" +#include "Unreal/Engine/UDataTable.hpp" +#include "Unreal/FProperty.hpp" #include "Unreal/UObjectGlobals.hpp" #include "Unreal/UScriptStruct.hpp" -#include "Unreal/FProperty.hpp" -#include "Unreal/Engine/UDataTable.hpp" +#include "Unreal/World.hpp" +#include "Unreal/GameplayStatics.hpp" #include "SDK/Classes/KismetInternationalizationLibrary.h" #include "SDK/Helper/PropertyHelper.h" #include "Utility/Logging.h" #include "Helpers/String.hpp" +#include "SDK/Classes/AMonoNPCSpawner.h" #include "SDK/Structs/FPalCharacterIconDataRow.h" #include "SDK/Structs/FPalBPClassDataRow.h" #include "SDK/Structs/FPalNPCTalkFlowClassDataRow.h" @@ -531,5 +535,4 @@ namespace Palworld { PS::Log(STR("Failed to fully add shop for {} (ShopTableId: {}) - some parts were not added correctly.\n"), CharacterId.ToString(), ShopTableId); } } - } \ No newline at end of file diff --git a/src/Loader/PalMainLoader.cpp b/src/Loader/PalMainLoader.cpp index a28bf57..e5c4393 100644 --- a/src/Loader/PalMainLoader.cpp +++ b/src/Loader/PalMainLoader.cpp @@ -1,6 +1,7 @@ #include #include #include "Unreal/UClass.hpp" +#include "Unreal/UFunction.hpp" #include "Unreal/Hooks.hpp" #include "Utility/Config.h" #include "Utility/Logging.h" @@ -8,6 +9,7 @@ #include "SDK/Classes/Custom/UDataTableStore.h" #include "SDK/Classes/Custom/UObjectGlobals.h" #include "SDK/Classes/UCompositeDataTable.h" +#include "SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h" #include "SDK/Classes/PalUtility.h" #include "SDK/PalSignatures.h" #include "SDK/StaticClassStorage.h" @@ -34,6 +36,7 @@ namespace constants { constexpr std::string skinsFolder = "skins"; constexpr std::string translationsFolder = "translations"; constexpr std::string resourcesFolder = "resources"; + constexpr std::string spawnsFolder = "spawns"; } namespace Palworld { @@ -56,46 +59,21 @@ namespace Palworld { auto expected5 = StaticItemDataTable_Get_Hook.disable(); StaticItemDataTable_Get_Hook = {}; + auto expected6 = WorldCleanUp_Hook.disable(); + WorldCleanUp_Hook = {}; + DatatableSerializeCallbacks.clear(); GameInstanceInitCallbacks.clear(); PostLoadCallbacks.clear(); GetPakFoldersCallback.clear(); + WorldCleanUp_Callbacks.clear(); } void PalMainLoader::PreInitialize() { - auto DatatableSerializeFuncPtr = Palworld::SignatureManager::GetSignature("UDataTable::Serialize"); - if (DatatableSerializeFuncPtr) - { - DatatableSerialize_Hook = safetyhook::create_inline(reinterpret_cast(DatatableSerializeFuncPtr), - OnDataTableSerialized); - - DatatableSerializeCallbacks.push_back([&](RC::Unreal::UDataTable* Table) { - InitCore(); - UECustom::UDataTableStore::Store(Table); - RawTableLoader.OnDataTableChanged(Table); - }); - - PS::Log(STR("Core pre-initialized.\n")); - } - else - { - PS::Log(STR("Unable to initialize PalSchema core, signature for UDataTable::Serialize is outdated.\n")); - } - - auto StaticItemDataTable_GetFuncPtr = Palworld::SignatureManager::GetSignature("UPalStaticItemDataTable::Get"); - if (StaticItemDataTable_GetFuncPtr) - { - StaticItemDataTable_Get_Hook = safetyhook::create_inline(reinterpret_cast(StaticItemDataTable_GetFuncPtr), - StaticItemDataTable_Get); - - PS::Log(STR("Dummy item fix applied.\n")); - } - else - { - PS::Log(STR("Unable to apply dummy item fix, signature for UPalStaticItemDataTable::Get is outdated.\n")); - } - + HookDatatableSerialize(); + HookStaticItemDataTable_Get(); + HookWorldCleanup(); SetupAlternativePakPathReader(); } @@ -116,6 +94,59 @@ namespace Palworld { PS::Log(STR("Finished reloading mods.\n")); } + void PalMainLoader::HookDatatableSerialize() + { + auto DatatableSerializeFuncPtr = Palworld::SignatureManager::GetSignature("UDataTable::Serialize"); + if (!DatatableSerializeFuncPtr) + { + PS::Log(STR("Unable to initialize PalSchema core, signature for UDataTable::Serialize is outdated.\n")); + return; + } + + DatatableSerialize_Hook = safetyhook::create_inline(reinterpret_cast(DatatableSerializeFuncPtr), + OnDataTableSerialized); + + DatatableSerializeCallbacks.push_back([&](RC::Unreal::UDataTable* Table) { + InitCore(); + UECustom::UDataTableStore::Store(Table); + RawTableLoader.OnDataTableChanged(Table); + }); + + PS::Log(STR("Core pre-initialized.\n")); + } + + void PalMainLoader::HookStaticItemDataTable_Get() + { + auto StaticItemDataTable_GetFuncPtr = Palworld::SignatureManager::GetSignature("UPalStaticItemDataTable::Get"); + if (!StaticItemDataTable_GetFuncPtr) + { + PS::Log(STR("Unable to apply dummy item fix, signature for UPalStaticItemDataTable::Get is outdated.\n")); + return; + } + + StaticItemDataTable_Get_Hook = safetyhook::create_inline(reinterpret_cast(StaticItemDataTable_GetFuncPtr), + StaticItemDataTable_Get); + + PS::Log(STR("Dummy item fix applied.\n")); + } + + void PalMainLoader::HookWorldCleanup() + { + auto World_CleanupWorld_FuncPtr = Palworld::SignatureManager::GetSignature("UWorld::CleanupWorld"); + if (!World_CleanupWorld_FuncPtr) + { + PS::Log(STR("Unable to hook UWorld::CleanupWorld, signature is outdated. Custom spawns will not work.\n")); + return; + } + + WorldCleanUp_Hook = safetyhook::create_inline(reinterpret_cast(World_CleanupWorld_FuncPtr), + UWorld_CleanupWorld); + + WorldCleanUp_Callbacks.push_back([&](RC::Unreal::UWorld* WorldToCleanup) { + SpawnLoader.OnWorldCleanup(WorldToCleanup); + }); + } + void PalMainLoader::SetupAutoReload() { auto config = PS::PSConfig::Get(); @@ -197,6 +228,10 @@ namespace Palworld { { RawTableLoader.Reload(data); } + else if (folderType == constants::spawnsFolder) + { + SpawnLoader.Reload(modName, data); + } }); PS::Log(STR("Auto-reloaded mod {}\n"), modName); @@ -298,6 +333,18 @@ namespace Palworld { GameInstanceInit_Hook = safetyhook::create_inline(GameInstanceInitPtr, reinterpret_cast(OnGameInstanceInit)); + auto onLevelShownFunction = UECustom::UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Script/Engine.WorldPartitionRuntimeLevelStreamingCell:OnLevelShown")); + onLevelShownFunction->RegisterPostHook([&](UnrealScriptFunctionCallableContext& Context, void* CustomData) { + auto Cell = static_cast(Context.Context); + SpawnLoader.OnCellLoaded(Cell); + }); + + auto onLevelHiddenFunction = UECustom::UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Script/Engine.WorldPartitionRuntimeLevelStreamingCell:OnLevelHidden")); + onLevelHiddenFunction->RegisterPostHook([&](UnrealScriptFunctionCallableContext& Context, void* CustomData) { + auto Cell = static_cast(Context.Context); + SpawnLoader.OnCellUnloaded(Cell); + }); + PS::Log(STR("Initialized Core\n")); } @@ -315,6 +362,7 @@ namespace Palworld { ItemModLoader.Initialize(); SkinModLoader.Initialize(); HelpGuideModLoader.Initialize(); + SpawnLoader.Initialize(); PS::Log(STR("Initialized Loaders\n")); PS::Log(STR("Loading mods...\n")); @@ -383,6 +431,11 @@ namespace Palworld { ParseJsonFilesInPath(blueprintFolder, [&](const nlohmann::json& data) { BlueprintModLoader.Load(data); }); + + auto spawnsFolder = modPath / constants::spawnsFolder; + ParseJsonFilesInPath(spawnsFolder, [&](const nlohmann::json& data) { + SpawnLoader.Load(modName, data); + }); } catch (const std::exception& e) { @@ -582,4 +635,13 @@ namespace Palworld { // Subsequent calls to this hook with the same ItemId will just return in the first if block since it was added to StaticItemDataAsset. return PalItemModLoader::AddDummyItem(This, ItemId); } + + void PalMainLoader::UWorld_CleanupWorld(UWorld* This, bool bSessionEnded, bool bCleanupResources, UWorld* NewWorld) + { + WorldCleanUp_Hook.call(This, bSessionEnded, bCleanupResources, NewWorld); + for (auto& Callback : WorldCleanUp_Callbacks) + { + Callback(This); + } + } } diff --git a/src/Loader/PalSpawnLoader.cpp b/src/Loader/PalSpawnLoader.cpp new file mode 100644 index 0000000..eace6ef --- /dev/null +++ b/src/Loader/PalSpawnLoader.cpp @@ -0,0 +1,414 @@ +#include "Unreal/UClass.hpp" +#include "Unreal/UEnum.hpp" +#include "Unreal/Engine/UDataTable.hpp" +#include "Unreal/Transform.hpp" +#include "Unreal/World.hpp" +#include "SDK/Classes/AMonoNPCSpawner.h" +#include "SDK/Classes/APalSpawnerStandard.h" +#include "SDK/Classes/UWorldPartition.h" +#include "SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h" +#include "SDK/Classes/ULevelStreaming.h" +#include "SDK/Classes/KismetGuidLibrary.h" +#include "SDK/Classes/Custom/UObjectGlobals.h" +#include "SDK/Classes/Custom/DataTable/TableSerializer.h" +#include "Utility/Logging.h" +#include "Utility/EnumHelpers.h" +#include "Utility/JsonHelpers.h" +#include "Loader/PalSpawnLoader.h" + +using namespace RC; +using namespace RC::Unreal; + +namespace fs = std::filesystem; + +namespace Palworld { + PalSpawnLoader::PalSpawnLoader() : PalModLoaderBase("spawns") {} + + PalSpawnLoader::~PalSpawnLoader() {} + + void PalSpawnLoader::Initialize() + { + m_bossSpawnerLocationData = UObjectGlobals::StaticFindObject(nullptr, nullptr, + STR("/Game/Pal/DataTable/UI/DT_BossSpawnerLoactionData.DT_BossSpawnerLoactionData")); + } + + void PalSpawnLoader::Load(const std::filesystem::path::string_type& modName, const nlohmann::json& data) + { + if (!data.is_array()) + { + throw std::runtime_error("Spawn JSON must start as an array rather than as an object."); + } + + // The json file itself starts as an array, rather than as an object + for (auto& value : data) + { + RegisterSpawn(modName, value); + } + } + + void PalSpawnLoader::Reload(const std::filesystem::path::string_type& modName, const nlohmann::json& data) + { + UnloadMod(modName); + Load(modName, data); + + for (auto loadedCell : m_loadedCells) + { + ProcessCellSpawners(loadedCell); + } + } + + void PalSpawnLoader::OnWorldCleanup(RC::Unreal::UWorld* World) + { + static auto NAME_MainWorld5 = FName(STR("PL_MainWorld5"), FNAME_Add); + + if (NAME_MainWorld5 != World->GetNamePrivate()) + { + return; + } + + // Main world is unloading (returning to title). + // We want to reset all the containers so that our spawners can be spawned again when we re-enter the world. + for (auto& spawnInfo : m_spawns) + { + spawnInfo.Cell = nullptr; + spawnInfo.SpawnerActor = nullptr; + spawnInfo.bExistsInWorld = false; + } + + m_loadedCells.Empty(); + + PS::Log(STR("Session ending, spawners have been cleaned up.\n")); + } + + void PalSpawnLoader::OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell) + { + // This isn't perfect, but it should hopefully do the job for now until a better solution is found. + // Seems like cells can overlap each other (?) which means the spawner code can get called multiple times. + + if (cell->GetExtent() > 51200.0) + { + // Skip massive cells. Hopefully it doesn't cause issues with some locations not working for spawners. + return; + } + + if (cell->GetIsHLOD()) + { + // Skip HLOD. From what I noticed, if a spawner is created inside a HLOD, it more than likely will not despawn ever which is not ideal. + return; + } + + if (cell->GetLevel() == 0) + { + // Skip L0 grids, all the other spawners seem to be on L1 or above. + return; + } + + m_loadedCells.Add(cell); + ProcessCellSpawners(cell); + } + + void PalSpawnLoader::OnCellUnloaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell) + { + DestroySpawnersInCell(cell); + m_loadedCells.Remove(cell); + } + + void PalSpawnLoader::RegisterSpawn(const std::filesystem::path::string_type& modName, const nlohmann::json& value) + { + PS::JsonHelpers::ValidateFieldExists(value, "Type"); + PS::JsonHelpers::ValidateFieldExists(value, "Location"); + PS::JsonHelpers::ValidateFieldExists(value, "Rotation"); + + PS::SpawnerInfo spawnerInfo{}; + + auto Guid = UECustom::UKismetGuidLibrary::NewGuid(); + spawnerInfo.Guid = Guid; + spawnerInfo.ModName = modName; + + std::string type; + PS::JsonHelpers::ParseString(value, "Type", type); + + if (type == "MonoNPC") + { + spawnerInfo.Type = PS::SpawnerType::MonoNPC; + } + else if (type == "Sheet") + { + spawnerInfo.Type = PS::SpawnerType::Sheet; + } + else + { + throw std::runtime_error("Type must be 'MonoNPC' or 'Sheet'."); + } + + // Location is required for all spawner types + PS::JsonHelpers::ParseVector(value, "Location", spawnerInfo.Location); + + // Rotation is required for all spawner types + PS::JsonHelpers::ParseRotator(value, "Rotation", spawnerInfo.Rotation); + + if (spawnerInfo.Type == PS::SpawnerType::Sheet) + { + RegisterSheet(modName, spawnerInfo, value); + } + else if (spawnerInfo.Type == PS::SpawnerType::MonoNPC) + { + RegisterMonoNPC(modName, spawnerInfo, value); + } + + m_spawns.push_back(spawnerInfo); + + Output::send(STR("Added new spawn: {}\n"), spawnerInfo.ToString()); + } + + void PalSpawnLoader::RegisterSheet(const std::filesystem::path::string_type& modName, PS::SpawnerInfo& spawnerInfo, const nlohmann::json& value) + { + PS::JsonHelpers::ValidateFieldExists(value, "SpawnGroupList"); + auto& spawnGroupList = value.at("SpawnGroupList"); + + if (!spawnGroupList.is_array()) + { + throw std::runtime_error("SpawnGroupList must be an array of objects."); + } + + for (auto& spawnGroupListItem : spawnGroupList) + { + spawnerInfo.AddSpawnGroupList(spawnGroupListItem); + } + + if (PS::JsonHelpers::FieldExists(value, "SpawnerName")) + { + std::string spawnerName; + PS::JsonHelpers::ParseString(value, "SpawnerName", spawnerName); + + auto spawnerNameWide = RC::to_generic_string(spawnerName); + spawnerNameWide = std::format(TEXT("{}_{}"), spawnerInfo.ModName, spawnerNameWide); + + spawnerInfo.SpawnerName = FName(spawnerNameWide, FNAME_Add); + } + + if (PS::JsonHelpers::FieldExists(value, "SpawnerType")) + { + static auto ENUM_EPalSpawnedCharacterType = UECustom::UObjectGlobals::StaticFindObject(nullptr, nullptr, + STR("/Script/Pal.EPalSpawnedCharacterType")); + + std::string spawnerType; + PS::JsonHelpers::ParseString(value, "SpawnerType", spawnerType); + spawnerInfo.SpawnerType = PS::EnumHelpers::GetEnumValueByName(ENUM_EPalSpawnedCharacterType, spawnerType); + + if (spawnerType.ends_with("FieldBoss")) + { + AddBossSpawnLocationToMap(spawnerInfo); + } + } + } + + void PalSpawnLoader::RegisterMonoNPC(const std::filesystem::path::string_type& modName, PS::SpawnerInfo& spawnerInfo, const nlohmann::json& value) + { + PS::JsonHelpers::ValidateFieldExists(value, "NPCID"); + PS::JsonHelpers::ValidateFieldExists(value, "Level"); + + PS::JsonHelpers::ParseFName(value, "NPCID", spawnerInfo.NPCID); + PS::JsonHelpers::ParseInteger(value, "Level", spawnerInfo.Level); + + if (spawnerInfo.Type == PS::SpawnerType::MonoNPC && spawnerInfo.NPCID == NAME_None) + { + throw std::runtime_error("NPCID can not be 'None' when type is set to 'MonoNPC'"); + } + + // This will be the Pal that is summoned by the NPC when it is attacked. Optional field + if (PS::JsonHelpers::FieldExists(value, "OtomoId")) + { + PS::JsonHelpers::ParseFName(value, "OtomoId", spawnerInfo.OtomoName); + } + } + + void PalSpawnLoader::ProcessCellSpawners(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell) + { + auto& Box = cell->GetContentBounds(); + for (auto& spawn : m_spawns) + { + if (spawn.bExistsInWorld) continue; + + if (Box.IsInside(spawn.Location)) + { + CreateSpawner(cell, spawn); + } + } + } + + void PalSpawnLoader::CreateSpawner(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo) + { + switch (spawnerInfo.Type) + { + case PS::SpawnerType::MonoNPC: + SpawnMonoNPC(cell, spawnerInfo); + break; + case PS::SpawnerType::Sheet: + SpawnSheet(cell, spawnerInfo); + break; + } + } + + void PalSpawnLoader::SpawnMonoNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo) + { + static auto bpClass = UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Game/Pal/Blueprint/Spawner/BP_MonoNPCSpawner.BP_MonoNPCSpawner_C")); + if (!bpClass) + { + PS::Log(STR("Unable to get class BP_MonoNPCSpawner, failed to spawn {}\n"), spawnerInfo.NPCID.ToString()); + return; + } + + auto world = cell->GetWorld(); + if (!world) + { + PS::Log(STR("Unable to get world from cell, failed to spawn {}\n"), spawnerInfo.NPCID.ToString()); + return; + } + + auto transform = FTransform(spawnerInfo.Rotation, spawnerInfo.Location, { 1.0, 1.0, 1.0 }); + auto spawnedActor = world->SpawnActor(bpClass, &transform); + auto monoSpawner = static_cast(spawnedActor); + monoSpawner->GetHumanName() = spawnerInfo.NPCID; + monoSpawner->GetCharaName() = spawnerInfo.NPCID; + monoSpawner->GetLevel() = spawnerInfo.Level; + monoSpawner->GetOtomoName() = spawnerInfo.OtomoName; + + spawnerInfo.bExistsInWorld = true; + spawnerInfo.Cell = cell; + spawnerInfo.SpawnerActor = monoSpawner; + + PS::Log(STR("Spawned {} in {}\n"), spawnerInfo.ToString(), cell->GetName()); + } + + void PalSpawnLoader::SpawnSheet(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, PS::SpawnerInfo& spawnerInfo) + { + static auto sheetBPClass = UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Game/Pal/Blueprint/Spawner/BP_PalSpawner_Standard.BP_PalSpawner_Standard_C")); + if (!sheetBPClass) + { + PS::Log(STR("Unable to get class BP_PalSpawner_Standard, failed to spawn sheet\n")); + return; + } + + auto world = cell->GetWorld(); + if (!world) + { + PS::Log(STR("Unable to get world from cell, failed to spawn sheet\n")); + return; + } + + auto transform = FTransform(spawnerInfo.Rotation, spawnerInfo.Location, { 1.0, 1.0, 1.0 }); + auto spawnedActor = world->SpawnActor(sheetBPClass, &transform); + auto palSheet = static_cast(spawnedActor); + + PalSpawnerGroup spawnerGroup; + for (auto& spawnGroupListItem : spawnerInfo.SpawnGroupList) + { + spawnerGroup.Weight = spawnGroupListItem.Weight; + spawnerGroup.OnlyTime = spawnGroupListItem.OnlyTime; + spawnerGroup.OnlyWeather = spawnGroupListItem.OnlyWeather; + + for (auto& palListInfo : spawnGroupListItem.PalList) + { + spawnerGroup.PalList.push_back(palListInfo); + } + } + + palSheet->SetSpawnerName(spawnerInfo.SpawnerName); + palSheet->SetSpawnerType(spawnerInfo.SpawnerType); + palSheet->AddSpawnerGroup(spawnerGroup); + + spawnerInfo.bExistsInWorld = true; + spawnerInfo.Cell = cell; + spawnerInfo.SpawnerActor = palSheet; + + PS::Log(STR("Spawned {} in {}\n"), spawnerInfo.ToString(), cell->GetName()); + } + + void PalSpawnLoader::AddBossSpawnLocationToMap(PS::SpawnerInfo& spawnerInfo) + { + if (!m_bossSpawnerLocationData) { + PS::Log(STR("Failed to add boss icon to map, DT_BossSpawnerLoactionData was not initialized.\n")); + return; + } + + if (spawnerInfo.SpawnGroupList.size() == 0) + { + PS::Log(STR("Failed to add boss icon to map, SpawnGroupList has no entries.\n")); + return; + } + + auto& firstGroup = spawnerInfo.SpawnGroupList.at(0); + if (firstGroup.PalList.size() == 0) + { + PS::Log(STR("Failed to add boss icon to map, PalList has no entries.\n")); + return; + } + + auto& firstPal = firstGroup.PalList.at(0); + + auto characterId = firstPal.PalId; + if (characterId == NAME_None) + { + characterId = firstPal.NPCID; + } + + TableSerializer serializer(m_bossSpawnerLocationData); + + auto newRow = serializer.Add(spawnerInfo.SpawnerName); + newRow->SetValue(STR("SpawnerID"), spawnerInfo.SpawnerName); + newRow->SetValue(STR("CharacterID"), characterId); + newRow->SetValue(STR("Location"), spawnerInfo.Location); + newRow->SetValue(STR("Level"), firstPal.Level); + + spawnerInfo.bHasMapIcon = true; + } + + void PalSpawnLoader::RemoveBossSpawnLocationFromMap(PS::SpawnerInfo& spawnerInfo) + { + if (!m_bossSpawnerLocationData) { + PS::Log(STR("Failed to remove boss icon from map, DT_BossSpawnerLoactionData was not initialized.\n")); + return; + } + + // This doesn't seem to actually help. + // WBP_Map_Base saves boss icons permanently to a variable called BossIcons. + // The logic goes something like this: + // (WBP_Map_Base):"Setup Boss Icon" -> + // (WBP_Map_Base):"Add Boss Icon" -> + // (WBP_Map_Base).(WBP_Map_Body): Add Icon By Location + // Ideally we'd want to clear the icons manually from WBP_Map_Body and then run "Setup Boss Icon" in WBP_Map_Base again to refresh them. + m_bossSpawnerLocationData->RemoveRow(spawnerInfo.SpawnerName); + } + + void PalSpawnLoader::DestroySpawnersInCell(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell) + { + for (auto& spawn : m_spawns) + { + if (spawn.Cell == cell) + { + spawn.Unload(); + PS::Log(STR("Unloaded spawn in Cell {}\n"), cell->GetName()); + } + } + } + + void PalSpawnLoader::UnloadMod(const std::filesystem::path::string_type& modName) + { + std::erase_if(m_spawns, [&](PS::SpawnerInfo& spawn) { + if (spawn.ModName == modName) + { + spawn.Unload(); + + if (spawn.bHasMapIcon) + { + RemoveBossSpawnLocationFromMap(spawn); + } + + return true; + } + + return false; + }); + } +} \ No newline at end of file diff --git a/src/Loader/Spawner/PalListInfo.cpp b/src/Loader/Spawner/PalListInfo.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/Loader/Spawner/PalSpawnGroupListInfo.cpp b/src/Loader/Spawner/PalSpawnGroupListInfo.cpp new file mode 100644 index 0000000..23f3cf3 --- /dev/null +++ b/src/Loader/Spawner/PalSpawnGroupListInfo.cpp @@ -0,0 +1,54 @@ +#include "Loader/Spawner/PalSpawnGroupListInfo.h" +#include "nlohmann/json.hpp" +#include "Utility/JsonHelpers.h" + +using namespace RC; +using namespace RC::Unreal; + +namespace PS { + void PalSpawnGroupListInfo::AddPalListInfo(const nlohmann::json& value) + { + if (!value.is_object()) + { + throw std::runtime_error("PalList item was not an object."); + } + + Palworld::PalListInfo listInfo; + if (PS::JsonHelpers::FieldExists(value, "NPCID")) + { + PS::JsonHelpers::ParseFName(value, "NPCID", listInfo.NPCID); + } + + if (PS::JsonHelpers::FieldExists(value, "PalId")) + { + PS::JsonHelpers::ParseFName(value, "PalId", listInfo.PalId); + } + + if (listInfo.NPCID == NAME_None && listInfo.PalId == NAME_None) + { + throw std::runtime_error("PalList item must contain either NPCID or PalId."); + } + + if (PS::JsonHelpers::FieldExists(value, "Level")) + { + PS::JsonHelpers::ParseInteger(value, "Level", listInfo.Level); + } + + if (PS::JsonHelpers::FieldExists(value, "Level_Max")) + { + PS::JsonHelpers::ParseInteger(value, "Level_Max", listInfo.LevelMax); + } + + if (PS::JsonHelpers::FieldExists(value, "Num")) + { + PS::JsonHelpers::ParseInteger(value, "Num", listInfo.Num); + } + + if (PS::JsonHelpers::FieldExists(value, "Num_Max")) + { + PS::JsonHelpers::ParseInteger(value, "Num_Max", listInfo.NumMax); + } + + PalList.push_back(listInfo); + } +} \ No newline at end of file diff --git a/src/Loader/Spawner/SpawnerInfo.cpp b/src/Loader/Spawner/SpawnerInfo.cpp new file mode 100644 index 0000000..13b1ec1 --- /dev/null +++ b/src/Loader/Spawner/SpawnerInfo.cpp @@ -0,0 +1,100 @@ +#include "Loader/Spawner/SpawnerInfo.h" +#include "SDK/Classes/AMonoNPCSpawner.h" +#include "nlohmann/json.hpp" +#include "Utility/Logging.h" +#include "Utility/JsonHelpers.h" + +using namespace RC; +using namespace RC::Unreal; + +namespace PS { + void SpawnerInfo::Unload() + { + if (SpawnerActor && !SpawnerActor->IsUnreachable()) + { + PS::Log(STR("[{}] Preparing to destroy spawner actor.\n"), ModName); + SpawnerActor->K2_DestroyActor(); + SpawnerActor = nullptr; + PS::Log(STR("[{}] Finished destroying spawner actor.\n"), ModName); + } + + if (Cell) + { + Cell = nullptr; + } + + bExistsInWorld = false; + } + + void SpawnerInfo::AddSpawnGroupList(const nlohmann::json& value) + { + if (!value.is_object()) + { + throw std::runtime_error("SpawnGroupList item was not an object."); + } + + PS::JsonHelpers::ValidateFieldExists(value, "Weight"); + PS::JsonHelpers::ValidateFieldExists(value, "PalList"); + auto& palList = value.at("PalList"); + + PalSpawnGroupListInfo spawnGroupListInfo; + PS::JsonHelpers::ParseInteger(value, "Weight", spawnGroupListInfo.Weight); + + if (PS::JsonHelpers::FieldExists(value, "OnlyTime")) + { + std::string onlyTime = "Undefined"; + PS::JsonHelpers::ParseString(value, "OnlyTime", onlyTime); + spawnGroupListInfo.OnlyTime = GetOnlyTimeFromString(onlyTime); + } + + if (!palList.is_array()) + { + throw std::runtime_error("PalList must be an array of objects."); + } + + for (auto& palListItem : palList) + { + spawnGroupListInfo.AddPalListInfo(palListItem); + } + + SpawnGroupList.push_back(spawnGroupListInfo); + } + + RC::StringType SpawnerInfo::ToString() + { + if (CachedString != TEXT("")) + { + return CachedString; + } + + RC::StringType location = std::format(TEXT("X: {:.3f}, Y: {:.3f}, Z: {:.3f}"), Location.GetX(), Location.GetY(), Location.GetZ()); + + if (Type == SpawnerType::Sheet) + { + CachedString = std::format(TEXT("(Sheet @ [{}] with {} group{})"), + location, + SpawnGroupList.size(), + SpawnGroupList.size() > 1 ? TEXT("s") : TEXT("")); + return CachedString; + } + + RC::StringType npcId = NPCID.ToString(); + CachedString = std::format(TEXT("({} @ [{}])"), npcId, location);; + return CachedString; + } + + RC::Unreal::uint8 SpawnerInfo::GetOnlyTimeFromString(const std::string& str) + { + if (str == "Day") + { + return 1U; + } + + if (str == "Night") + { + return 2U; + } + + return 0U; + } +} \ No newline at end of file diff --git a/src/SDK/Classes/AMonoNPCSpawner.cpp b/src/SDK/Classes/AMonoNPCSpawner.cpp new file mode 100644 index 0000000..75c7615 --- /dev/null +++ b/src/SDK/Classes/AMonoNPCSpawner.cpp @@ -0,0 +1,45 @@ +#include "SDK/Classes/AMonoNPCSpawner.h" +#include "Unreal/UFunction.hpp" +#include "Unreal/UObjectGlobals.hpp" +#include "Utility/Logging.h" + +using namespace RC::Unreal; + +namespace Palworld { + int& AMonoNPCSpawner::GetLevel() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("Level")); + return *Value; + } + + RC::Unreal::FName& AMonoNPCSpawner::GetHumanName() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("HumanName")); + return *Value; + } + + RC::Unreal::FName& AMonoNPCSpawner::GetCharaName() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("CharaName")); + return *Value; + } + + RC::Unreal::FName& AMonoNPCSpawner::GetOtomoName() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("OtomoName")); + return *Value; + } + + void AMonoNPCSpawner::Spawn() + { + auto Function = this->GetFunctionByNameInChain(TEXT("Spawn")); + + if (!Function) + { + PS::Log(STR("Failed to execute 'Spawn', could not find function /Game/Pal/Blueprint/Spawner/BP_MonoNPCSpawner.BP_MonoNPCSpawner_C:Spawn.\n")); + return; + } + + this->ProcessEvent(Function, nullptr); + } +} \ No newline at end of file diff --git a/src/SDK/Classes/APalSpawnerStandard.cpp b/src/SDK/Classes/APalSpawnerStandard.cpp new file mode 100644 index 0000000..b2c86a1 --- /dev/null +++ b/src/SDK/Classes/APalSpawnerStandard.cpp @@ -0,0 +1,52 @@ +#include "SDK/Classes/APalSpawnerStandard.h" +#include "SDK/Helper/PropertyHelper.h" +#include "SDK/Structs/Custom/FScriptArrayHelper.h" +#include "SDK/Structs/Reflected/PalSpawnerGroupInfo.h" +#include "Unreal/UClass.hpp" +#include "Unreal/Core/Containers/ScriptArray.hpp" +#include "Unreal/FProperty.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace Palworld { + void APalSpawnerStandard::AddSpawnerGroup(const PalSpawnerGroup& spawnerGroup) { + auto spawnGroupListProperty = PropertyHelper::GetPropertyByName(this->GetClassPrivate(), TEXT("SpawnGroupList")); + if (!spawnGroupListProperty) + { + throw std::runtime_error("Could not get SpawnGroupList in APalSpawnerStandard."); + } + + auto spawnGroupList = spawnGroupListProperty->ContainerPtrToValuePtr(this); + + auto scriptArray = static_cast(spawnGroupList); + auto scriptArrayHelper = UECustom::FScriptArrayHelper(scriptArray, static_cast(spawnGroupListProperty)); + + UECustom::FManagedValue valuePtr; + scriptArrayHelper.InitializeValue(valuePtr); + + FPalSpawnerGroupInfo spawnerGroupInfo(valuePtr.GetData()); + spawnerGroupInfo.SetWeight(spawnerGroup.Weight); + spawnerGroupInfo.SetOnlyTime(spawnerGroup.OnlyTime); + spawnerGroupInfo.SetOnlyWeather(spawnerGroup.OnlyWeather); + + for (auto& palListInfo : spawnerGroup.PalList) + { + spawnerGroupInfo.AddPal(palListInfo); + } + + scriptArrayHelper.Add(valuePtr); + } + + void APalSpawnerStandard::SetSpawnerName(const RC::Unreal::FName& spawnerName) + { + auto property = PropertyHelper::GetPropertyByName(this->GetClassPrivate(), TEXT("SpawnerName")); + *property->ContainerPtrToValuePtr(this) = spawnerName; + } + + void APalSpawnerStandard::SetSpawnerType(const RC::Unreal::uint8& spawnerType) + { + auto property = PropertyHelper::GetPropertyByName(this->GetClassPrivate(), TEXT("SpawnerType")); + *property->ContainerPtrToValuePtr(this) = spawnerType; + } +} \ No newline at end of file diff --git a/src/SDK/Classes/Custom/DataTable/TableSerializer.cpp b/src/SDK/Classes/Custom/DataTable/TableSerializer.cpp new file mode 100644 index 0000000..fa9c801 --- /dev/null +++ b/src/SDK/Classes/Custom/DataTable/TableSerializer.cpp @@ -0,0 +1,25 @@ +#include "SDK/Classes/Custom/DataTable/TableSerializer.h" +#include "Unreal/Engine/UDataTable.hpp" + +namespace Palworld { + TableSerializer::TableSerializer(RC::Unreal::UDataTable* table) : m_table(table) + { + + } + + FTableSerializerRow* TableSerializer::Add(const RC::Unreal::FName& rowId) + { + m_rows.push_back(std::make_unique(m_table, rowId, FTableSerializerRow::ETableSerializeMode::Add)); + auto newRow = m_rows.back().get(); + return newRow; + } + + FTableSerializerRow* TableSerializer::Edit(const RC::Unreal::FName& rowId) + { + auto serializeRow = std::make_unique(m_table, rowId, FTableSerializerRow::ETableSerializeMode::Edit); + m_rows.push_back(std::move(serializeRow)); + + auto newRow = m_rows.back().get(); + return newRow; + } +} \ No newline at end of file diff --git a/src/SDK/Classes/KismetGuidLibrary.cpp b/src/SDK/Classes/KismetGuidLibrary.cpp new file mode 100644 index 0000000..48959bf --- /dev/null +++ b/src/SDK/Classes/KismetGuidLibrary.cpp @@ -0,0 +1,55 @@ +#include "SDK/Classes/KismetGuidLibrary.h" +#include "Unreal/UFunction.hpp" +#include "Utility/Logging.h" + +using namespace RC; +using namespace RC::Unreal; + +namespace UECustom { + UECustom::FGuid UKismetGuidLibrary::NewGuid() + { + static auto Function = UObjectGlobals::StaticFindObject(nullptr, nullptr, TEXT("/Script/Engine.KismetGuidLibrary:NewGuid")); + + if (!Function) + { + PS::Log(STR("Function /Script/Engine.KismetGuidLibrary:NewGuid was invalid.\n")); + return {}; + } + + struct { + UECustom::FGuid ReturnValue; + }params; + + GetDefaultObj()->ProcessEvent(Function, ¶ms); + + return params.ReturnValue; + } + + RC::Unreal::FString UKismetGuidLibrary::Conv_GuidToString(const UECustom::FGuid& Guid) + { + static auto Function = UObjectGlobals::StaticFindObject(nullptr, nullptr, TEXT("/Script/Engine.KismetGuidLibrary:Conv_GuidToString")); + + if (!Function) + { + PS::Log(STR("Function /Script/Engine.KismetGuidLibrary:Conv_GuidToString was invalid.\n")); + return {}; + } + + struct { + UECustom::FGuid Guid; + RC::Unreal::FString ReturnValue; + }params; + + params.Guid = Guid; + + GetDefaultObj()->ProcessEvent(Function, ¶ms); + + return params.ReturnValue; + } + + UKismetGuidLibrary* UKismetGuidLibrary::GetDefaultObj() + { + static auto Self = UObjectGlobals::StaticFindObject(nullptr, nullptr, TEXT("/Script/Engine.Default__KismetGuidLibrary")); + return Self; + } +} diff --git a/src/SDK/Classes/UCompositeDataTable.cpp b/src/SDK/Classes/UCompositeDataTable.cpp index cb3d92b..9a06fab 100644 --- a/src/SDK/Classes/UCompositeDataTable.cpp +++ b/src/SDK/Classes/UCompositeDataTable.cpp @@ -1,5 +1,6 @@ #include "SDK/Classes/UCompositeDataTable.h" #include "SDK/Classes/Custom/UObjectGlobals.h" +#include "Unreal/UClass.hpp" #include "Helpers/Casting.hpp" using namespace RC; diff --git a/src/SDK/Classes/ULevelStreaming.cpp b/src/SDK/Classes/ULevelStreaming.cpp new file mode 100644 index 0000000..1d080c3 --- /dev/null +++ b/src/SDK/Classes/ULevelStreaming.cpp @@ -0,0 +1,20 @@ +#include "SDK/Classes/ULevelStreaming.h" +#include "Helpers/Casting.hpp" +#include "Unreal/FWeakObjectPtr.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace UECustom { + UWorldPartition* ULevelStreaming::GetOuterWorldPartition() + { + auto WeakPtr = *Helper::Casting::ptr_cast*>(this, 0x1BC); + auto InnerObject = WeakPtr.Get(); + if (!InnerObject) + { + return nullptr; + } + + return InnerObject; + } +} \ No newline at end of file diff --git a/src/SDK/Classes/UWorldPartition.cpp b/src/SDK/Classes/UWorldPartition.cpp new file mode 100644 index 0000000..970fc99 --- /dev/null +++ b/src/SDK/Classes/UWorldPartition.cpp @@ -0,0 +1,12 @@ +#include "SDK/Classes/UWorldPartition.h" +#include "Helpers/Casting.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace UECustom { + UWorld* UWorldPartition::GetWorld() + { + return *Helper::Casting::ptr_cast(this, 0x70); + } +} \ No newline at end of file diff --git a/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp b/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp new file mode 100644 index 0000000..3707a23 --- /dev/null +++ b/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp @@ -0,0 +1,37 @@ +#include "SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h" +#include "Helpers/Casting.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace UECustom { + FBox& UECustom::UWorldPartitionRuntimeLevelStreamingCell::GetContentBounds() + { + return *Helper::Casting::ptr_cast(this, 0x48); + } + + ULevelStreaming* UWorldPartitionRuntimeLevelStreamingCell::GetLevelStreaming() + { + return *Helper::Casting::ptr_cast(this, 0x138); + } + + bool& UWorldPartitionRuntimeLevelStreamingCell::GetIsHLOD() + { + return *Helper::Casting::ptr_cast(this, 0xB5); + } + + RC::Unreal::FVector& UWorldPartitionRuntimeLevelStreamingCell::GetPosition() + { + return *Helper::Casting::ptr_cast(this, 0xE8); + } + + float& UWorldPartitionRuntimeLevelStreamingCell::GetExtent() + { + return *Helper::Casting::ptr_cast(this, 0x100); + } + + int& UWorldPartitionRuntimeLevelStreamingCell::GetLevel() + { + return *Helper::Casting::ptr_cast(this, 0x104); + } +} \ No newline at end of file diff --git a/src/SDK/Structs/Custom/DataTable/FTableSerializerRow.cpp b/src/SDK/Structs/Custom/DataTable/FTableSerializerRow.cpp new file mode 100644 index 0000000..4f78944 --- /dev/null +++ b/src/SDK/Structs/Custom/DataTable/FTableSerializerRow.cpp @@ -0,0 +1,44 @@ +#include "SDK/Structs/Custom/DataTable/FTableSerializerRow.h" +#include "Unreal/Engine/UDataTable.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace Palworld { + FTableSerializerRow::~FTableSerializerRow() + { + if (m_mode == ETableSerializeMode::Add) + { + m_table->AddRow(m_rowId, *static_cast(m_data)); + + m_struct->DestroyStruct(m_data); + FMemory::Free(m_data); + } + } + + FTableSerializerRow::FTableSerializerRow(RC::Unreal::UDataTable* table, const RC::Unreal::FName& rowId, ETableSerializeMode mode) + : m_table(table), m_mode(mode), m_rowId(rowId) + { + auto rowStruct = table->GetRowStruct().Get(); + + if (mode == ETableSerializeMode::Add) + { + m_data = FMemory::Malloc(rowStruct->GetStructureSize()); + m_struct = rowStruct; + m_struct->InitializeStruct(m_data); + } + else if (mode == ETableSerializeMode::Edit) + { + m_struct = rowStruct; + + auto data = table->FindRowUnchecked(rowId); + if (!data) + { + Output::send(STR("FTableSerializerRow: Failed to find Row '{}'.\n"), rowId.ToString()); + return; + } + + m_data = data; + } + } +} \ No newline at end of file diff --git a/src/SDK/Structs/Reflected/BaseReflectedStruct.cpp b/src/SDK/Structs/Reflected/BaseReflectedStruct.cpp new file mode 100644 index 0000000..feadb20 --- /dev/null +++ b/src/SDK/Structs/Reflected/BaseReflectedStruct.cpp @@ -0,0 +1,47 @@ +#include "SDK/Structs/Reflected/BaseReflectedStruct.h" +#include "Unreal/Core/Containers/ScriptArray.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace UECustom { + BaseReflectedStruct::BaseReflectedStruct(RC::Unreal::UScriptStruct* scriptStruct) : m_scriptStruct(scriptStruct) + { + m_data = FMemory::Malloc(m_scriptStruct->GetStructureSize()); + m_scriptStruct->InitializeStruct(m_data); + } + + BaseReflectedStruct::BaseReflectedStruct(RC::Unreal::UScriptStruct* scriptStruct, void* data) : m_scriptStruct(scriptStruct), m_data(data) + { + } + + BaseReflectedStruct::~BaseReflectedStruct() + { + } + + void BaseReflectedStruct::DestroyStruct() + { + m_scriptStruct->DestroyStruct(m_data); + FMemory::Free(m_data); + } + + void* BaseReflectedStruct::GetData() + { + return m_data; + } + + std::unique_ptr BaseReflectedStruct::GetArrayPropertyValue(RC::Unreal::FProperty* property) + { + auto arrayProperty = CastField(property); + if (!arrayProperty) + { + throw std::runtime_error(RC::fmt("Tried accessing property '%S' as an array property, but property was another type.", property->GetName().c_str())); + } + + auto arrayData = arrayProperty->ContainerPtrToValuePtr(m_data); + auto scriptArray = static_cast(arrayData); + auto scriptArrayHelper = std::make_unique(scriptArray, arrayProperty); + + return std::move(scriptArrayHelper); + } +} \ No newline at end of file diff --git a/src/SDK/Structs/Reflected/PalSpawnerGroupInfo.cpp b/src/SDK/Structs/Reflected/PalSpawnerGroupInfo.cpp new file mode 100644 index 0000000..90802f0 --- /dev/null +++ b/src/SDK/Structs/Reflected/PalSpawnerGroupInfo.cpp @@ -0,0 +1,57 @@ +#include "SDK/Structs/Reflected/PalSpawnerGroupInfo.h" +#include "SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h" +#include "SDK/Classes/Custom/UObjectGlobals.h" +#include "Unreal/Property/FArrayProperty.hpp" +#include "Unreal/Property/FEnumProperty.hpp" +#include "Unreal/Property/FNumericProperty.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace Palworld { + FPalSpawnerGroupInfo::FPalSpawnerGroupInfo() : BaseReflectedStruct(StaticStruct()) {} + FPalSpawnerGroupInfo::FPalSpawnerGroupInfo(void* data) : BaseReflectedStruct(StaticStruct(), data) {}; + + void FPalSpawnerGroupInfo::SetWeight(int value) + { + auto property = GetPropertyChecked(STR("Weight")); + SetPropertyValue(property, value); + } + + void FPalSpawnerGroupInfo::SetOnlyTime(RC::Unreal::uint8 value) + { + auto property = GetPropertyChecked(STR("OnlyTime")); + SetPropertyValue(property, value); + } + + void FPalSpawnerGroupInfo::SetOnlyWeather(RC::Unreal::uint8 value) + { + auto property = GetPropertyChecked(STR("OnlyWeather")); + SetPropertyValue(property, value); + } + + void FPalSpawnerGroupInfo::AddPal(const PalListInfo& value) + { + auto property = GetPropertyChecked(STR("PalList")); + auto array = GetArrayPropertyValue(property); + + UECustom::FManagedValue valuePtr; + array->InitializeValue(valuePtr); + + FPalSpawnerOneTribeInfo tribeInfo(valuePtr.GetData()); + tribeInfo.SetPalId(value.PalId); + tribeInfo.SetNPCID(value.NPCID); + tribeInfo.SetLevel(value.Level); + tribeInfo.SetLevelMax(value.LevelMax); + tribeInfo.SetNum(value.Num); + tribeInfo.SetNumMax(value.NumMax); + + array->Add(valuePtr); + } + + RC::Unreal::UScriptStruct* FPalSpawnerGroupInfo::StaticStruct() + { + static auto Struct = UECustom::UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Script/Pal.PalSpawnerGroupInfo")); + return Struct; + } +} \ No newline at end of file diff --git a/src/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.cpp b/src/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.cpp new file mode 100644 index 0000000..852685b --- /dev/null +++ b/src/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.cpp @@ -0,0 +1,53 @@ +#include "SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h" +#include "SDK/Classes/Custom/UObjectGlobals.h" +#include "Unreal/Property/FNumericProperty.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace Palworld { + FPalSpawnerOneTribeInfo::FPalSpawnerOneTribeInfo() : BaseReflectedStruct(StaticStruct()) {} + FPalSpawnerOneTribeInfo::FPalSpawnerOneTribeInfo(void* data) : BaseReflectedStruct(StaticStruct(), data) {}; + + void FPalSpawnerOneTribeInfo::SetPalId(const RC::Unreal::FName& value) + { + auto property = GetProperty(STR("PalId")); + SetPropertyValue(property, value); + } + + void FPalSpawnerOneTribeInfo::SetNPCID(const RC::Unreal::FName& value) + { + auto property = GetProperty(STR("NPCID")); + SetPropertyValue(property, value); + } + + void FPalSpawnerOneTribeInfo::SetLevel(int value) + { + auto property = GetPropertyChecked(STR("Level")); + SetPropertyValue(property, value); + } + + void FPalSpawnerOneTribeInfo::SetLevelMax(int value) + { + auto property = GetPropertyChecked(STR("Level_Max")); + SetPropertyValue(property, value); + } + + void FPalSpawnerOneTribeInfo::SetNum(int value) + { + auto property = GetPropertyChecked(STR("Num")); + SetPropertyValue(property, value); + } + + void FPalSpawnerOneTribeInfo::SetNumMax(int value) + { + auto property = GetPropertyChecked(STR("Num_Max")); + SetPropertyValue(property, value); + } + + RC::Unreal::UScriptStruct* FPalSpawnerOneTribeInfo::StaticStruct() + { + static auto Struct = UECustom::UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Script/Pal.PalSpawnerOneTribeInfo")); + return Struct; + } +} \ No newline at end of file diff --git a/src/SDK/UnrealOffsets.cpp b/src/SDK/UnrealOffsets.cpp index d0610f9..fc5fb42 100644 --- a/src/SDK/UnrealOffsets.cpp +++ b/src/SDK/UnrealOffsets.cpp @@ -17,6 +17,7 @@ #include "Unreal/ULocalPlayer.hpp" #include "Unreal/UStruct.hpp" #include "Unreal/UScriptStruct.hpp" +#include "Unreal/Engine/UDataTable.hpp" #include "Unreal/World.hpp" #include "Unreal/Property/FArrayProperty.hpp" #include "Unreal/Property/FBoolProperty.hpp" @@ -867,6 +868,20 @@ void Palworld::UnrealOffsets::ApplyMemberVariableLayout() Unreal::FInterfaceProperty::MemberOffsets.emplace(STR("InterfaceClass"), static_cast(val)); if (auto val = parser.get_int64(STR("FFieldPathProperty"), STR("PropertyClass"), -1); val != -1) Unreal::FFieldPathProperty::MemberOffsets.emplace(STR("PropertyClass"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("RowStruct"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("RowStruct"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("RowMap"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("RowMap"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("bStripFromClientBuilds"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("bStripFromClientBuilds"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("bIgnoreExtraFields"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("bIgnoreExtraFields"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("bIgnoreMissingFields"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("bIgnoreMissingFields"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("ImportKeyField"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("ImportKeyField"), static_cast(val)); + if (auto val = parser.get_int64(STR("UDataTable"), STR("bPreserveExistingValues"), -1); val != -1) + Unreal::UDataTable::MemberOffsets.emplace(STR("bPreserveExistingValues"), static_cast(val)); PS::Log(STR("Offsets from MemberVariableLayout.ini applied.\n")); } diff --git a/src/Utility/EnumHelpers.cpp b/src/Utility/EnumHelpers.cpp new file mode 100644 index 0000000..014d286 --- /dev/null +++ b/src/Utility/EnumHelpers.cpp @@ -0,0 +1,32 @@ +#include "Utility/EnumHelpers.h" +#include "Utility/Logging.h" +#include "Helpers/String.hpp" +#include "Unreal/UEnum.hpp" + +using namespace RC; + +namespace PS::EnumHelpers { + RC::Unreal::int64 GetEnumValueByName(RC::Unreal::UEnum* enum_, const std::string& enumString) + { + std::string enumStringFixed = enumString; + + auto enumName = enum_->GetName(); + if (!enumStringFixed.contains("::")) + { + enumStringFixed = std::format("{}::{}", RC::to_string(enumName), enumStringFixed); + } + + auto enumKey = RC::to_generic_string(enumStringFixed); + for (const auto& enumPair : enum_->GetNames()) + { + if (enumPair.Key.ToString() == enumKey) + { + return enumPair.Value; + } + } + + PS::Log(STR("Enum '{}' doesn't exist."), enumKey); + + return 0; + } +} \ No newline at end of file diff --git a/src/Utility/JsonHelpers.cpp b/src/Utility/JsonHelpers.cpp new file mode 100644 index 0000000..5d16bf6 --- /dev/null +++ b/src/Utility/JsonHelpers.cpp @@ -0,0 +1,116 @@ +#include "Utility/JsonHelpers.h" +#include "Unreal/Core/HAL/Platform.hpp" +#include "Unreal/NameTypes.hpp" +#include "Unreal/UnrealCoreStructs.hpp" +#include "Unreal/Rotator.hpp" +#include "nlohmann/json.hpp" + +using namespace RC; +using namespace RC::Unreal; + +namespace PS::JsonHelpers { + bool FieldExists(const nlohmann::json& data, const std::string& fieldName) + { + return data.contains(fieldName); + } + + void ValidateFieldExists(const nlohmann::json& data, const std::string& fieldName) + { + if (!data.contains(fieldName)) + { + throw std::runtime_error(std::format("Missing a required field of '{}'.", fieldName)); + } + } + + void ParseRotator(const nlohmann::json& value, const std::string& fieldName, FRotator& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_object() || !field.contains("Pitch") || !field.contains("Yaw") || !field.contains("Roll")) + { + throw std::runtime_error(std::format("FRotator '{}' must be an object with fields 'Pitch', 'Yaw' and 'Roll'.", fieldName)); + } + + double pitch, yaw, roll; + ParseDouble(field, "Pitch", pitch); + ParseDouble(field, "Yaw", yaw); + ParseDouble(field, "Roll", roll); + + outValue = FRotator{ pitch, yaw, roll }; + } + + void ParseVector(const nlohmann::json& value, const std::string& fieldName, FVector& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_object() || !field.contains("X") || !field.contains("Y") || !field.contains("Z")) + { + throw std::runtime_error(std::format("FVector '{}' must be an object with fields 'X', 'Y' and 'Z'.", fieldName)); + } + + double x, y, z; + ParseDouble(field, "X", x); + ParseDouble(field, "Y", y); + ParseDouble(field, "Z", z); + + outValue = FVector{ x, y, z }; + } + + void ParseFName(const nlohmann::json& value, const std::string& fieldName, FName& outValue) + { + std::string parsedString; + ParseString(value, fieldName, parsedString); + + auto wideString = RC::to_generic_string(parsedString); + + outValue = FName(wideString, FNAME_Add); + } + + void ParseDouble(const nlohmann::json& value, const std::string& fieldName, double& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_number_float()) + { + throw std::runtime_error(std::format("Value '{}' must be a floating point number.", fieldName)); + } + + outValue = field.get(); + } + + void ParseInteger(const nlohmann::json& value, const std::string& fieldName, int& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_number_integer()) + { + throw std::runtime_error(std::format("Value '{}' must be an integer.", fieldName)); + } + + outValue = field.get(); + } + + void ParseUInt8(const nlohmann::json& value, const std::string& fieldName, RC::Unreal::uint8& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_number_integer()) + { + throw std::runtime_error(std::format("Value '{}' must be an integer.", fieldName)); + } + + outValue = field.get(); + } + + void ParseString(const nlohmann::json& value, const std::string& fieldName, std::string& outValue) + { + auto& field = value.at(fieldName); + + if (!field.is_string()) + { + throw std::runtime_error(std::format("Value '{}' must be a string.", fieldName)); + } + + outValue = field.get(); + } +} \ No newline at end of file diff --git a/website/docs/guides/spawners/_category_.json b/website/docs/guides/spawners/_category_.json new file mode 100644 index 0000000..c43d852 --- /dev/null +++ b/website/docs/guides/spawners/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Spawners", + "position": 10, + "link": { + "type": "generated-index" + } +} diff --git a/website/docs/guides/spawners/overview.md b/website/docs/guides/spawners/overview.md new file mode 100644 index 0000000..53e5430 --- /dev/null +++ b/website/docs/guides/spawners/overview.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 1 +--- + +# Overview + +Starting with 0.5.0, you are now able to create new spawns for pals and NPCs which function the exact same as how the game would spawn pals, bosses, npcs, etc. Editing is not currently available and you should use the `blueprints` method of doing it. See the example [BossMammorestSpawnReplacer](https://github.com/Okaetsu/PalSchema/blob/main/assets/examples/BossMammorestSpawnReplacer/blueprints/mammorest_spawn_replacer.jsonc) mod for editing spawns. + +Features: +- Auto-reload is **supported** for spawner mods, [video](https://www.youtube.com/watch?v=jK6SnkGTL50) of it in action. +- When adding bosses, an icon is automatically added to the map without having to edit `DT_BossSpawnerLoactionData` as long as the `SpawnerType` is set to `FieldBoss`. + +We will be going through the structure of a spawner json and the required folder name for it. + +## Folder Name + +The required name for the folder so it can be seen by PalSchema is `spawns`, meaning you want something like this `MyModName/spawns/spawn.json`. + +## JSON Structure + +**IMPORTANT**: You need to start your spawner JSON as an array with square brackets [ ] rather than curly brackets \{ \}. + +Currently the following fields are available: + +### Generic + +These fields are available to both `MonoNPC` and `Sheet`. + +- `Type`: Can be either `MonoNPC` or `Sheet`. +- `Location`: Location of the spawned character(s). +- `Rotation`: Rotation of the spawned character(s). + +### MonoNPC +These fields are only available when `Type` is set to `MonoNPC`. +- `NPCID`: ID of your NPC, e.g. PalDealer. Only available when `Type` is set to `MonoNPC`. +- `OtomoId`: ID of the character that is summoned when the NPC is attacked. Only available when `Type` is set to `MonoNPC`. +- `Level`: Level of the NPC specified by `NPCID`. + +### Sheet +These fields are only available when `Type` is set to `Sheet`. +- `SpawnerName`: Name of the spawner, make sure to specify this, it can be anything you want. Check [Additional Notes](#additional-notes) regarding implementation details of `SpawnerName`. +- `SpawnerType`: Type specified in the `EPalSpawnerPlacementType` enum as a string, e.g. `FieldBoss` for a boss. +- `SpawnGroupList`: Array of `SpawnGroup` objects. + - `SpawnGroup`: + - `Weight`: If there are multiple SpawnGroups, this controls how likely it is that this specific group will be selected over the others. Should be a whole integer from 1 to 100, decimals are not accepted. + - `OnlyTime`: Controls if the SpawnGroup should only spawn at a certain time of day. Accepted string values are `Day` and `Night`. You may also omit this field entirely if you want the group to spawn regardless of the time of day. + - `PalList`: Array of `PalSpawnerOneTribeInfo` objects. + - `PalSpawnerOneTribeInfo`: + - `PalId`: String Id of the pal to spawn, e.g. PinkCat. + - `Level`: Minimum level of the spawned character. + - `Level_Max`: Maximum level of the spawned character. + - `Num`: Minimum number of characters to spawn at the same time. + - `Num_Max`: Maximum number of characters to spawn at the same time. + +### Additional Notes + +- A small implementation detail regarding the `SpawnerName` field when adding spawns: PalSchema will automatically append the name of your mod to the front, so if the name of your mod's main folder is called `MySpawnerMod` and the `SpawnerName` is `Cattiva001`, the final name will be `MySpawnerMod_Cattiva001`. This is to prevent any name collisions with other mods that add spawns with the same name. \ No newline at end of file