From 7f45e244195967d04e9446c41de1b3d75296be97 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:01:36 +0200 Subject: [PATCH 01/18] Add FBox and custom FVector with std::set support --- include/SDK/Structs/FBox.h | 16 ++++++++++++++++ include/SDK/Structs/FVector.h | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 include/SDK/Structs/FBox.h create mode 100644 include/SDK/Structs/FVector.h 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 From e3b9e7c065d99c42d7deefc170ce86d151d4368d Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:16:50 +0200 Subject: [PATCH 02/18] Add beginning of spawner code for npcs --- include/Loader/PalHumanModLoader.h | 43 +++++-- include/Loader/PalMainLoader.h | 5 + include/SDK/Classes/AMonoNPCSpawner.h | 14 +++ include/SDK/Classes/ULevelStreaming.h | 13 +++ include/SDK/Classes/UWorldPartition.h | 14 +++ ...UWorldPartitionRuntimeLevelStreamingCell.h | 15 +++ include/SDK/PalSignatures.h | 2 + src/Loader/PalHumanModLoader.cpp | 106 +++++++++++++++++- src/Loader/PalMainLoader.cpp | 20 ++++ src/SDK/Classes/AMonoNPCSpawner.cpp | 33 ++++++ src/SDK/Classes/ULevelStreaming.cpp | 20 ++++ src/SDK/Classes/UWorldPartition.cpp | 12 ++ ...orldPartitionRuntimeLevelStreamingCell.cpp | 17 +++ 13 files changed, 301 insertions(+), 13 deletions(-) create mode 100644 include/SDK/Classes/AMonoNPCSpawner.h create mode 100644 include/SDK/Classes/ULevelStreaming.h create mode 100644 include/SDK/Classes/UWorldPartition.h create mode 100644 include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h create mode 100644 src/SDK/Classes/AMonoNPCSpawner.cpp create mode 100644 src/SDK/Classes/ULevelStreaming.cpp create mode 100644 src/SDK/Classes/UWorldPartition.cpp create mode 100644 src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp diff --git a/include/Loader/PalHumanModLoader.h b/include/Loader/PalHumanModLoader.h index 358f219..549be44 100644 --- a/include/Loader/PalHumanModLoader.h +++ b/include/Loader/PalHumanModLoader.h @@ -1,13 +1,24 @@ #pragma once +#include #include "Loader/PalModLoaderBase.h" #include "nlohmann/json.hpp" +#include "SDK/Structs/FVector.h" namespace RC::Unreal { class UDataTable; } +namespace UECustom { + class UWorldPartitionRuntimeLevelStreamingCell; +} + namespace Palworld { + struct PalHumanSpawnParams { + RC::Unreal::FVector Location; + RC::Unreal::FName CharacterId; + }; + class PalHumanModLoader : public PalModLoaderBase { public: PalHumanModLoader(); @@ -28,22 +39,32 @@ namespace Palworld { void AddLoot(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); + void AddSpawn(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); + void AddTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data); void EditTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data); 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; + // This is called whenever a world partition is loaded within the main world. + void OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + + void SpawnNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, const PalHumanSpawnParams& params); + + std::vector m_spawns; + std::set m_occupiedLocations; + + 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..0254808 100644 --- a/include/Loader/PalMainLoader.h +++ b/include/Loader/PalMainLoader.h @@ -23,6 +23,7 @@ namespace RC::Unreal { namespace UECustom { class UCompositeDataTable; + class UWorldPartitionRuntimeLevelStreamingCell; } namespace Palworld { @@ -90,11 +91,14 @@ namespace Palworld { static RC::Unreal::UObject* StaticItemDataTable_Get(UPalStaticItemDataTable* This, RC::Unreal::FName ItemId); + static void UWorldPartitionRuntimeLevelStreamingCell_Activate(UECustom::UWorldPartitionRuntimeLevelStreamingCell* This); + bool m_hasInit = false; static inline std::vector> DatatableSerializeCallbacks; static inline std::vector> GameInstanceInitCallbacks; static inline std::vector> PostLoadCallbacks; + static inline std::vector> StreamingCell_Activate_Callbacks; static inline std::vector> GetPakFoldersCallback; static inline SafetyHookInline DatatableSerialize_Hook; @@ -102,5 +106,6 @@ namespace Palworld { static inline SafetyHookInline PostLoad_Hook; static inline SafetyHookInline GetPakFolders_Hook; static inline SafetyHookInline StaticItemDataTable_Get_Hook; + static inline SafetyHookInline UWorldPartitionRuntimeLevelStreamingCell_Activate_Hook; }; } \ 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..fe0526d --- /dev/null +++ b/include/SDK/Classes/AMonoNPCSpawner.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Unreal/AActor.hpp" + +namespace Palworld { + class AMonoNPCSpawner : public RC::Unreal::AActor { + public: + int& GetLevel(); + + RC::Unreal::FName& GetHumanName(); + + void Spawn(); + }; +} \ 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..8c9c97e --- /dev/null +++ b/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h @@ -0,0 +1,15 @@ +#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(); + }; +} \ No newline at end of file diff --git a/include/SDK/PalSignatures.h b/include/SDK/PalSignatures.h index bb132a1..2f08f40 100644 --- a/include/SDK/PalSignatures.h +++ b/include/SDK/PalSignatures.h @@ -29,6 +29,8 @@ namespace Palworld { { "FField::IsA", "48 8B 41 08 48 8B 4A 08 48 85 C9 74 08 48 85 48 10 0F 95 C0 C3" }, // Important, we need this early. { "FName::Constructor", "48 89 5C 24 08 57 48 83 EC 30 48 8B D9 48 89 54 24 20" }, + // UWorldPartitionRuntimeLevelStreamingCell + { "UWorldPartitionRuntimeLevelStreamingCell::Activate", "40 53 48 83 EC 20 E8 ?? ?? ?? ?? 48 8B D8 48 85 C0 74 3B 4C 8B 00 B2 01 48 8B C8 41 FF 90 B8 02 00 00 B2 01" }, }; static inline std::unordered_map SignaturesCallResolve { // Don't ask, I know it's long.. diff --git a/src/Loader/PalHumanModLoader.cpp b/src/Loader/PalHumanModLoader.cpp index e4ce078..d014df1 100644 --- a/src/Loader/PalHumanModLoader.cpp +++ b/src/Loader/PalHumanModLoader.cpp @@ -1,8 +1,14 @@ +#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/Classes/ULevelStreaming.h" +#include "SDK/Classes/UWorldPartition.h" +#include "SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h" #include "SDK/Helper/PropertyHelper.h" #include "Utility/Logging.h" #include "Helpers/String.hpp" @@ -12,6 +18,7 @@ #include "SDK/Structs/FPalItemShopLotteryDataRow.h" #include "SDK/Structs/FPalItemShopSettingDataRow.h" #include "Loader/PalHumanModLoader.h" +#include using namespace RC; using namespace RC::Unreal; @@ -102,6 +109,10 @@ namespace Palworld { { AddShop(CharacterId, value); } + else if (KeyName == STR("SpawnLocations")) + { + AddSpawn(CharacterId, value); + } else { auto Property = NpcRowStruct->GetPropertyByName(KeyName.c_str()); @@ -300,6 +311,45 @@ namespace Palworld { n_dropItemTable->AddRow(FName(RowName, FNAME_Add), *reinterpret_cast(NpcDropItemData)); } + void PalHumanModLoader::AddSpawn(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties) + { + if (!properties.is_array()) + { + throw std::runtime_error(std::format("SpawnLocations in {} must be an array.", RC::to_string(CharacterId.ToString()))); + } + + for (auto& spawnLocation : properties) + { + if (!spawnLocation.contains("X") && !spawnLocation.at("X").is_number_float()) + { + throw std::runtime_error(std::format("X for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); + } + + if (!spawnLocation.contains("Y") && !spawnLocation.at("Y").is_number_float()) + { + throw std::runtime_error(std::format("Y for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); + } + + if (!spawnLocation.contains("Z") && !spawnLocation.at("Z").is_number_float()) + { + throw std::runtime_error(std::format("Z for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); + } + + auto X = spawnLocation.at("X").get(); + auto Y = spawnLocation.at("Y").get(); + auto Z = spawnLocation.at("Z").get(); + + PS::Log(STR("Spawn Location for {} is X {:.3f}, Y {:.3f}, Z {:.3f}\n"), CharacterId.ToString(), X, Y, Z); + + PalHumanSpawnParams params; + params.CharacterId = CharacterId; + params.Location = FVector{ X, Y, Z }; + + // Gets processed by PalHumanModLoader::OnCellLoaded later on. + m_spawns.push_back(params); + } + } + //Add New NPC Translations void PalHumanModLoader::AddTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data) { @@ -532,4 +582,56 @@ namespace Palworld { } } + void PalHumanModLoader::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. + // I decided to make it so that there can only be one spawner per EXACT location since you wouldn't want them to overlap anyway. + // e.g. (0.001, 0.001, 0.001) can't have two spawners in this location, but a spawner with a location of (0.001, 0.001, 0.002) would be allowed. + auto& Box = cell->GetContentBounds(); + for (auto& spawn : m_spawns) + { + if (Box.IsInside(spawn.Location)) + { + auto loc = UECustom::FVector(spawn.Location); + if (!m_occupiedLocations.count(loc)) + { + m_occupiedLocations.insert(loc); + SpawnNPC(cell, spawn); + } + } + } + } + + void PalHumanModLoader::SpawnNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, const PalHumanSpawnParams& params) + { + PS::Log(STR("Spawning {} ...\n"), params.CharacterId.ToString()); + + auto levelStreaming = cell->GetLevelStreaming(); + auto worldPartition = levelStreaming->GetOuterWorldPartition(); + if (!worldPartition) + { + PS::Log(STR("Unable to get world partition, failed to spawn {}\n"), params.CharacterId.ToString()); + return; + } + + static auto bpClass = UObjectGlobals::StaticFindObject(nullptr, nullptr, STR("/Game/Pal/Blueprint/Spawner/BP_MonoNPCSpawner.BP_MonoNPCSpawner_C")); + + auto world = worldPartition->GetWorld(); + PS::Log(STR("World is {} ...\n"), world->GetFullName()); + + auto rotation = FRotator{}; + auto transform = FTransform(rotation, params.Location, { 1.0, 1.0, 1.0 }); + + auto actor = UGameplayStatics::BeginDeferredActorSpawnFromClass(world, bpClass, transform); + auto monoSpawner = static_cast(actor); + monoSpawner->GetHumanName() = FName(STR("PalDealer"), FNAME_Add); + monoSpawner->GetLevel() = 55; + auto finalActor = static_cast(UGameplayStatics::FinishSpawningActor(actor, transform)); + PS::Log(STR("Spawner is {} ...\n"), finalActor->GetFullName()); + finalActor->Spawn(); + + // TODO: investigate why npcs aren't appearing..? + // Spawner seems to spawn in just fine with the correct values.. + } } \ No newline at end of file diff --git a/src/Loader/PalMainLoader.cpp b/src/Loader/PalMainLoader.cpp index a28bf57..c4989c6 100644 --- a/src/Loader/PalMainLoader.cpp +++ b/src/Loader/PalMainLoader.cpp @@ -96,6 +96,17 @@ namespace Palworld { PS::Log(STR("Unable to apply dummy item fix, signature for UPalStaticItemDataTable::Get is outdated.\n")); } + auto StreamingCell_Activate_Signature = Palworld::SignatureManager::GetSignature("UWorldPartitionRuntimeLevelStreamingCell::Activate"); + if (StreamingCell_Activate_Signature) + { + StreamingCell_Activate_Callbacks.push_back([&](UECustom::UWorldPartitionRuntimeLevelStreamingCell* Cell) { + HumanModLoader.OnCellLoaded(Cell); + }); + + UWorldPartitionRuntimeLevelStreamingCell_Activate_Hook = safetyhook::create_inline(reinterpret_cast(StreamingCell_Activate_Signature), + UWorldPartitionRuntimeLevelStreamingCell_Activate); + } + SetupAlternativePakPathReader(); } @@ -582,4 +593,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::UWorldPartitionRuntimeLevelStreamingCell_Activate(UECustom::UWorldPartitionRuntimeLevelStreamingCell* This) + { + UWorldPartitionRuntimeLevelStreamingCell_Activate_Hook.call(This); + for (auto& Callback : StreamingCell_Activate_Callbacks) + { + Callback(This); + } + } } diff --git a/src/SDK/Classes/AMonoNPCSpawner.cpp b/src/SDK/Classes/AMonoNPCSpawner.cpp new file mode 100644 index 0000000..51e2fcc --- /dev/null +++ b/src/SDK/Classes/AMonoNPCSpawner.cpp @@ -0,0 +1,33 @@ +#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; + } + + 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/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..ddd137c --- /dev/null +++ b/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp @@ -0,0 +1,17 @@ +#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); + } +} \ No newline at end of file From 389d80b1f9524d748b38dab6d72563fa39132a6c Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Fri, 5 Dec 2025 09:02:23 +0200 Subject: [PATCH 03/18] Add sanity checks to spawn logic and fix spawns not working --- include/Loader/PalHumanModLoader.h | 9 ++++++--- include/SDK/Classes/AMonoNPCSpawner.h | 2 ++ src/Loader/PalHumanModLoader.cpp | 25 ++++++++++++++++--------- src/SDK/Classes/AMonoNPCSpawner.cpp | 6 ++++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/include/Loader/PalHumanModLoader.h b/include/Loader/PalHumanModLoader.h index 549be44..3936799 100644 --- a/include/Loader/PalHumanModLoader.h +++ b/include/Loader/PalHumanModLoader.h @@ -19,6 +19,8 @@ namespace Palworld { RC::Unreal::FName CharacterId; }; + class AMonoNPCSpawner; + class PalHumanModLoader : public PalModLoaderBase { public: PalHumanModLoader(); @@ -29,6 +31,9 @@ namespace Palworld { virtual void Load(const nlohmann::json& json) override final; + // This is called whenever a world partition is loaded within the main world. + void OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); + 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); @@ -47,12 +52,10 @@ namespace Palworld { void AddShop(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); - // This is called whenever a world partition is loaded within the main world. - void OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); - void SpawnNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, const PalHumanSpawnParams& params); std::vector m_spawns; + std::vector m_spawners; std::set m_occupiedLocations; RC::Unreal::UDataTable* n_dataTable = nullptr; diff --git a/include/SDK/Classes/AMonoNPCSpawner.h b/include/SDK/Classes/AMonoNPCSpawner.h index fe0526d..82532a7 100644 --- a/include/SDK/Classes/AMonoNPCSpawner.h +++ b/include/SDK/Classes/AMonoNPCSpawner.h @@ -9,6 +9,8 @@ namespace Palworld { RC::Unreal::FName& GetHumanName(); + RC::Unreal::FName& GetCharaName(); + void Spawn(); }; } \ No newline at end of file diff --git a/src/Loader/PalHumanModLoader.cpp b/src/Loader/PalHumanModLoader.cpp index d014df1..95b431f 100644 --- a/src/Loader/PalHumanModLoader.cpp +++ b/src/Loader/PalHumanModLoader.cpp @@ -12,13 +12,13 @@ #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" #include "SDK/Structs/FPalItemShopLotteryDataRow.h" #include "SDK/Structs/FPalItemShopSettingDataRow.h" #include "Loader/PalHumanModLoader.h" -#include using namespace RC; using namespace RC::Unreal; @@ -608,6 +608,12 @@ namespace Palworld { PS::Log(STR("Spawning {} ...\n"), params.CharacterId.ToString()); auto levelStreaming = cell->GetLevelStreaming(); + if (!levelStreaming) + { + PS::Log(STR("Unable to get level streaming, failed to spawn {}\n"), params.CharacterId.ToString()); + return; + } + auto worldPartition = levelStreaming->GetOuterWorldPartition(); if (!worldPartition) { @@ -616,6 +622,11 @@ namespace Palworld { } 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"), params.CharacterId.ToString()); + return; + } auto world = worldPartition->GetWorld(); PS::Log(STR("World is {} ...\n"), world->GetFullName()); @@ -623,15 +634,11 @@ namespace Palworld { auto rotation = FRotator{}; auto transform = FTransform(rotation, params.Location, { 1.0, 1.0, 1.0 }); - auto actor = UGameplayStatics::BeginDeferredActorSpawnFromClass(world, bpClass, transform); - auto monoSpawner = static_cast(actor); + auto spawnedActor = world->SpawnActor(bpClass, &transform); + auto monoSpawner = static_cast(spawnedActor); monoSpawner->GetHumanName() = FName(STR("PalDealer"), FNAME_Add); + monoSpawner->GetCharaName() = FName(STR("PalDealer"), FNAME_Add); monoSpawner->GetLevel() = 55; - auto finalActor = static_cast(UGameplayStatics::FinishSpawningActor(actor, transform)); - PS::Log(STR("Spawner is {} ...\n"), finalActor->GetFullName()); - finalActor->Spawn(); - - // TODO: investigate why npcs aren't appearing..? - // Spawner seems to spawn in just fine with the correct values.. + m_spawners.push_back(monoSpawner); } } \ No newline at end of file diff --git a/src/SDK/Classes/AMonoNPCSpawner.cpp b/src/SDK/Classes/AMonoNPCSpawner.cpp index 51e2fcc..199b098 100644 --- a/src/SDK/Classes/AMonoNPCSpawner.cpp +++ b/src/SDK/Classes/AMonoNPCSpawner.cpp @@ -18,6 +18,12 @@ namespace Palworld { return *Value; } + RC::Unreal::FName& AMonoNPCSpawner::GetCharaName() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("CharaName")); + return *Value; + } + void AMonoNPCSpawner::Spawn() { auto Function = this->GetFunctionByNameInChain(TEXT("Spawn")); From fd7907a81241da4e58eea1727ad4e5712b541da9 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:56:50 +0200 Subject: [PATCH 04/18] Fix warnings from UECustom::UObjectGlobals --- include/SDK/Classes/Custom/UObjectGlobals.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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, From c8662aa1b16067c350b0d7316a3e2c46a6ad13e3 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:59:25 +0200 Subject: [PATCH 05/18] Add json and enum helpers --- CMakeLists.txt | 2 + include/Utility/EnumHelpers.h | 12 ++++ include/Utility/JsonHelpers.h | 24 +++++++ src/Utility/EnumHelpers.cpp | 32 ++++++++++ src/Utility/JsonHelpers.cpp | 116 ++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 include/Utility/EnumHelpers.h create mode 100644 include/Utility/JsonHelpers.h create mode 100644 src/Utility/EnumHelpers.cpp create mode 100644 src/Utility/JsonHelpers.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d38533..83370e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,8 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Structs/Custom/*.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/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/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 From b0d540052ffb4a3aac9731c87bc597cf6b39e315 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:00:09 +0200 Subject: [PATCH 06/18] Include UDataTable offsets in UnrealOffsets --- src/SDK/UnrealOffsets.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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")); } From 796c110ca7e81b71e50c9da20a01820a7c1f7426 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:02:55 +0200 Subject: [PATCH 07/18] Fix missing includes --- src/Loader/PalEnumLoader.cpp | 1 + src/SDK/Classes/UCompositeDataTable.cpp | 1 + 2 files changed, 2 insertions(+) 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/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; From 0b5d54c1f9b910f1632dcaf1d7009c1f89ecaf64 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:08:27 +0200 Subject: [PATCH 08/18] Update SDK --- include/SDK/Classes/AMonoNPCSpawner.h | 2 + include/SDK/Classes/KismetGuidLibrary.h | 15 +++++ ...UWorldPartitionRuntimeLevelStreamingCell.h | 8 +++ include/SDK/Structs/Guid.h | 41 ++++++++++++++ include/SDK/Structs/PalListInfo.h | 15 +++++ include/SDK/Structs/PalSpawnerGroup.h | 14 +++++ src/SDK/Classes/AMonoNPCSpawner.cpp | 6 ++ src/SDK/Classes/KismetGuidLibrary.cpp | 55 +++++++++++++++++++ ...orldPartitionRuntimeLevelStreamingCell.cpp | 20 +++++++ 9 files changed, 176 insertions(+) create mode 100644 include/SDK/Classes/KismetGuidLibrary.h create mode 100644 include/SDK/Structs/Guid.h create mode 100644 include/SDK/Structs/PalListInfo.h create mode 100644 include/SDK/Structs/PalSpawnerGroup.h create mode 100644 src/SDK/Classes/KismetGuidLibrary.cpp diff --git a/include/SDK/Classes/AMonoNPCSpawner.h b/include/SDK/Classes/AMonoNPCSpawner.h index 82532a7..525b3da 100644 --- a/include/SDK/Classes/AMonoNPCSpawner.h +++ b/include/SDK/Classes/AMonoNPCSpawner.h @@ -11,6 +11,8 @@ namespace Palworld { RC::Unreal::FName& GetCharaName(); + RC::Unreal::FName& GetOtomoName(); + void Spawn(); }; } \ No newline at end of file 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/UWorldPartitionRuntimeLevelStreamingCell.h b/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h index 8c9c97e..26c7d9c 100644 --- a/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h +++ b/include/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h @@ -11,5 +11,13 @@ namespace UECustom { 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/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/src/SDK/Classes/AMonoNPCSpawner.cpp b/src/SDK/Classes/AMonoNPCSpawner.cpp index 199b098..75c7615 100644 --- a/src/SDK/Classes/AMonoNPCSpawner.cpp +++ b/src/SDK/Classes/AMonoNPCSpawner.cpp @@ -24,6 +24,12 @@ namespace Palworld { return *Value; } + RC::Unreal::FName& AMonoNPCSpawner::GetOtomoName() + { + auto Value = this->GetValuePtrByPropertyNameInChain(TEXT("OtomoName")); + return *Value; + } + void AMonoNPCSpawner::Spawn() { auto Function = this->GetFunctionByNameInChain(TEXT("Spawn")); 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/UWorldPartitionRuntimeLevelStreamingCell.cpp b/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp index ddd137c..3707a23 100644 --- a/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp +++ b/src/SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.cpp @@ -14,4 +14,24 @@ namespace UECustom { { 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 From add749e27ac5ffbabc23bec684bea77a498e9a57 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:14:14 +0200 Subject: [PATCH 09/18] Add reflection utility for structs --- .../Structs/Reflected/BaseReflectedStruct.h | 74 +++++++++++++++++++ .../Structs/Reflected/BaseReflectedStruct.cpp | 47 ++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 include/SDK/Structs/Reflected/BaseReflectedStruct.h create mode 100644 src/SDK/Structs/Reflected/BaseReflectedStruct.cpp 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/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 From b5498bd8ce76be6a8aa1c25bd6f3b395819ec164 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:14:38 +0200 Subject: [PATCH 10/18] Add reflected structs --- .../Structs/Reflected/PalSpawnerGroupInfo.h | 19 +++++++ .../Reflected/PalSpawnerOneTribeInfo.h | 20 +++++++ .../Structs/Reflected/PalSpawnerGroupInfo.cpp | 57 +++++++++++++++++++ .../Reflected/PalSpawnerOneTribeInfo.cpp | 53 +++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 include/SDK/Structs/Reflected/PalSpawnerGroupInfo.h create mode 100644 include/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.h create mode 100644 src/SDK/Structs/Reflected/PalSpawnerGroupInfo.cpp create mode 100644 src/SDK/Structs/Reflected/PalSpawnerOneTribeInfo.cpp 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/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 From de00fcdc94a3220d9be6d504c7c99b99d32bb5ae Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:16:04 +0200 Subject: [PATCH 11/18] Update CMakeLists --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 83370e7..786f307 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/SDK/Classes/Custom/*.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/Tools/EnumSchemaDefinitionGenerator.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Utility/Config.cpp" From 14f4ebc6673104d7010946b8af6495cd91001bcf Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:17:39 +0200 Subject: [PATCH 12/18] Add table utilities to make editing and adding rows easier --- CMakeLists.txt | 2 + .../Custom/DataTable/TableSerializer.h | 22 +++++++++ .../Custom/DataTable/FTableSerializerRow.h | 45 +++++++++++++++++++ .../Custom/DataTable/TableSerializer.cpp | 25 +++++++++++ .../Custom/DataTable/FTableSerializerRow.cpp | 44 ++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 include/SDK/Classes/Custom/DataTable/TableSerializer.h create mode 100644 include/SDK/Structs/Custom/DataTable/FTableSerializerRow.h create mode 100644 src/SDK/Classes/Custom/DataTable/TableSerializer.cpp create mode 100644 src/SDK/Structs/Custom/DataTable/FTableSerializerRow.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 786f307..8409d1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,10 +14,12 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS "${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" 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/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/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/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 From a28993ad5b43df6d354a299ba81898a33a453eb2 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:23:18 +0200 Subject: [PATCH 13/18] Add spawn loader, migrated spawn logic from human mod loader --- CMakeLists.txt | 3 + include/Loader/PalHumanModLoader.h | 23 - include/Loader/PalMainLoader.h | 14 +- include/Loader/PalSpawnLoader.h | 64 +++ .../Loader/Spawner/PalSpawnGroupListInfo.h | 16 + include/Loader/Spawner/SpawnerInfo.h | 63 +++ include/SDK/Classes/APalSpawnerStandard.h | 15 + include/SDK/PalSignatures.h | 3 +- src/Loader/PalHumanModLoader.cpp | 106 ----- src/Loader/PalMainLoader.cpp | 134 ++++-- src/Loader/PalSpawnLoader.cpp | 415 ++++++++++++++++++ src/Loader/Spawner/PalListInfo.cpp | 0 src/Loader/Spawner/PalSpawnGroupListInfo.cpp | 54 +++ src/Loader/Spawner/SpawnerInfo.cpp | 98 +++++ src/SDK/Classes/APalSpawnerStandard.cpp | 52 +++ 15 files changed, 880 insertions(+), 180 deletions(-) create mode 100644 include/Loader/PalSpawnLoader.h create mode 100644 include/Loader/Spawner/PalSpawnGroupListInfo.h create mode 100644 include/Loader/Spawner/SpawnerInfo.h create mode 100644 include/SDK/Classes/APalSpawnerStandard.h create mode 100644 src/Loader/PalSpawnLoader.cpp create mode 100644 src/Loader/Spawner/PalListInfo.cpp create mode 100644 src/Loader/Spawner/PalSpawnGroupListInfo.cpp create mode 100644 src/Loader/Spawner/SpawnerInfo.cpp create mode 100644 src/SDK/Classes/APalSpawnerStandard.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8409d1f..efd6b39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,9 @@ 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" diff --git a/include/Loader/PalHumanModLoader.h b/include/Loader/PalHumanModLoader.h index 3936799..51ad4aa 100644 --- a/include/Loader/PalHumanModLoader.h +++ b/include/Loader/PalHumanModLoader.h @@ -3,24 +3,12 @@ #include #include "Loader/PalModLoaderBase.h" #include "nlohmann/json.hpp" -#include "SDK/Structs/FVector.h" namespace RC::Unreal { class UDataTable; } -namespace UECustom { - class UWorldPartitionRuntimeLevelStreamingCell; -} - namespace Palworld { - struct PalHumanSpawnParams { - RC::Unreal::FVector Location; - RC::Unreal::FName CharacterId; - }; - - class AMonoNPCSpawner; - class PalHumanModLoader : public PalModLoaderBase { public: PalHumanModLoader(); @@ -30,9 +18,6 @@ namespace Palworld { void Initialize(); virtual void Load(const nlohmann::json& json) override final; - - // This is called whenever a world partition is loaded within the main world. - void OnCellLoaded(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell); private: void Add(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); @@ -44,20 +29,12 @@ namespace Palworld { void AddLoot(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); - void AddSpawn(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); - void AddTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data); void EditTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data); void AddShop(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties); - void SpawnNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, const PalHumanSpawnParams& params); - - std::vector m_spawns; - std::vector m_spawners; - std::set m_occupiedLocations; - RC::Unreal::UDataTable* n_dataTable = nullptr; RC::Unreal::UDataTable* n_iconDataTable = nullptr; RC::Unreal::UDataTable* n_palBpClassTable = nullptr; diff --git a/include/Loader/PalMainLoader.h b/include/Loader/PalMainLoader.h index 0254808..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 { @@ -54,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'. @@ -91,14 +99,14 @@ namespace Palworld { static RC::Unreal::UObject* StaticItemDataTable_Get(UPalStaticItemDataTable* This, RC::Unreal::FName ItemId); - static void UWorldPartitionRuntimeLevelStreamingCell_Activate(UECustom::UWorldPartitionRuntimeLevelStreamingCell* This); + 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> StreamingCell_Activate_Callbacks; + static inline std::vector> WorldCleanUp_Callbacks; static inline std::vector> GetPakFoldersCallback; static inline SafetyHookInline DatatableSerialize_Hook; @@ -106,6 +114,6 @@ namespace Palworld { static inline SafetyHookInline PostLoad_Hook; static inline SafetyHookInline GetPakFolders_Hook; static inline SafetyHookInline StaticItemDataTable_Get_Hook; - static inline SafetyHookInline UWorldPartitionRuntimeLevelStreamingCell_Activate_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/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/PalSignatures.h b/include/SDK/PalSignatures.h index 2f08f40..835c951 100644 --- a/include/SDK/PalSignatures.h +++ b/include/SDK/PalSignatures.h @@ -29,8 +29,6 @@ namespace Palworld { { "FField::IsA", "48 8B 41 08 48 8B 4A 08 48 85 C9 74 08 48 85 48 10 0F 95 C0 C3" }, // Important, we need this early. { "FName::Constructor", "48 89 5C 24 08 57 48 83 EC 30 48 8B D9 48 89 54 24 20" }, - // UWorldPartitionRuntimeLevelStreamingCell - { "UWorldPartitionRuntimeLevelStreamingCell::Activate", "40 53 48 83 EC 20 E8 ?? ?? ?? ?? 48 8B D8 48 85 C0 74 3B 4C 8B 00 B2 01 48 8B C8 41 FF 90 B8 02 00 00 B2 01" }, }; static inline std::unordered_map SignaturesCallResolve { // Don't ask, I know it's long.. @@ -44,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/src/Loader/PalHumanModLoader.cpp b/src/Loader/PalHumanModLoader.cpp index 95b431f..04bf04a 100644 --- a/src/Loader/PalHumanModLoader.cpp +++ b/src/Loader/PalHumanModLoader.cpp @@ -6,9 +6,6 @@ #include "Unreal/World.hpp" #include "Unreal/GameplayStatics.hpp" #include "SDK/Classes/KismetInternationalizationLibrary.h" -#include "SDK/Classes/ULevelStreaming.h" -#include "SDK/Classes/UWorldPartition.h" -#include "SDK/Classes/UWorldPartitionRuntimeLevelStreamingCell.h" #include "SDK/Helper/PropertyHelper.h" #include "Utility/Logging.h" #include "Helpers/String.hpp" @@ -109,10 +106,6 @@ namespace Palworld { { AddShop(CharacterId, value); } - else if (KeyName == STR("SpawnLocations")) - { - AddSpawn(CharacterId, value); - } else { auto Property = NpcRowStruct->GetPropertyByName(KeyName.c_str()); @@ -311,45 +304,6 @@ namespace Palworld { n_dropItemTable->AddRow(FName(RowName, FNAME_Add), *reinterpret_cast(NpcDropItemData)); } - void PalHumanModLoader::AddSpawn(const RC::Unreal::FName& CharacterId, const nlohmann::json& properties) - { - if (!properties.is_array()) - { - throw std::runtime_error(std::format("SpawnLocations in {} must be an array.", RC::to_string(CharacterId.ToString()))); - } - - for (auto& spawnLocation : properties) - { - if (!spawnLocation.contains("X") && !spawnLocation.at("X").is_number_float()) - { - throw std::runtime_error(std::format("X for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); - } - - if (!spawnLocation.contains("Y") && !spawnLocation.at("Y").is_number_float()) - { - throw std::runtime_error(std::format("Y for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); - } - - if (!spawnLocation.contains("Z") && !spawnLocation.at("Z").is_number_float()) - { - throw std::runtime_error(std::format("Z for a Spawn Location in {} wasn't a float.", RC::to_string(CharacterId.ToString()))); - } - - auto X = spawnLocation.at("X").get(); - auto Y = spawnLocation.at("Y").get(); - auto Z = spawnLocation.at("Z").get(); - - PS::Log(STR("Spawn Location for {} is X {:.3f}, Y {:.3f}, Z {:.3f}\n"), CharacterId.ToString(), X, Y, Z); - - PalHumanSpawnParams params; - params.CharacterId = CharacterId; - params.Location = FVector{ X, Y, Z }; - - // Gets processed by PalHumanModLoader::OnCellLoaded later on. - m_spawns.push_back(params); - } - } - //Add New NPC Translations void PalHumanModLoader::AddTranslations(const RC::Unreal::FName& CharacterId, const nlohmann::json& Data) { @@ -581,64 +535,4 @@ namespace Palworld { PS::Log(STR("Failed to fully add shop for {} (ShopTableId: {}) - some parts were not added correctly.\n"), CharacterId.ToString(), ShopTableId); } } - - void PalHumanModLoader::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. - // I decided to make it so that there can only be one spawner per EXACT location since you wouldn't want them to overlap anyway. - // e.g. (0.001, 0.001, 0.001) can't have two spawners in this location, but a spawner with a location of (0.001, 0.001, 0.002) would be allowed. - auto& Box = cell->GetContentBounds(); - for (auto& spawn : m_spawns) - { - if (Box.IsInside(spawn.Location)) - { - auto loc = UECustom::FVector(spawn.Location); - if (!m_occupiedLocations.count(loc)) - { - m_occupiedLocations.insert(loc); - SpawnNPC(cell, spawn); - } - } - } - } - - void PalHumanModLoader::SpawnNPC(UECustom::UWorldPartitionRuntimeLevelStreamingCell* cell, const PalHumanSpawnParams& params) - { - PS::Log(STR("Spawning {} ...\n"), params.CharacterId.ToString()); - - auto levelStreaming = cell->GetLevelStreaming(); - if (!levelStreaming) - { - PS::Log(STR("Unable to get level streaming, failed to spawn {}\n"), params.CharacterId.ToString()); - return; - } - - auto worldPartition = levelStreaming->GetOuterWorldPartition(); - if (!worldPartition) - { - PS::Log(STR("Unable to get world partition, failed to spawn {}\n"), params.CharacterId.ToString()); - return; - } - - 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"), params.CharacterId.ToString()); - return; - } - - auto world = worldPartition->GetWorld(); - PS::Log(STR("World is {} ...\n"), world->GetFullName()); - - auto rotation = FRotator{}; - auto transform = FTransform(rotation, params.Location, { 1.0, 1.0, 1.0 }); - - auto spawnedActor = world->SpawnActor(bpClass, &transform); - auto monoSpawner = static_cast(spawnedActor); - monoSpawner->GetHumanName() = FName(STR("PalDealer"), FNAME_Add); - monoSpawner->GetCharaName() = FName(STR("PalDealer"), FNAME_Add); - monoSpawner->GetLevel() = 55; - m_spawners.push_back(monoSpawner); - } } \ No newline at end of file diff --git a/src/Loader/PalMainLoader.cpp b/src/Loader/PalMainLoader.cpp index c4989c6..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,57 +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")); - } - - auto StreamingCell_Activate_Signature = Palworld::SignatureManager::GetSignature("UWorldPartitionRuntimeLevelStreamingCell::Activate"); - if (StreamingCell_Activate_Signature) - { - StreamingCell_Activate_Callbacks.push_back([&](UECustom::UWorldPartitionRuntimeLevelStreamingCell* Cell) { - HumanModLoader.OnCellLoaded(Cell); - }); - - UWorldPartitionRuntimeLevelStreamingCell_Activate_Hook = safetyhook::create_inline(reinterpret_cast(StreamingCell_Activate_Signature), - UWorldPartitionRuntimeLevelStreamingCell_Activate); - } - + HookDatatableSerialize(); + HookStaticItemDataTable_Get(); + HookWorldCleanup(); SetupAlternativePakPathReader(); } @@ -127,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(); @@ -208,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); @@ -309,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")); } @@ -326,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")); @@ -394,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) { @@ -594,10 +636,10 @@ namespace Palworld { return PalItemModLoader::AddDummyItem(This, ItemId); } - void PalMainLoader::UWorldPartitionRuntimeLevelStreamingCell_Activate(UECustom::UWorldPartitionRuntimeLevelStreamingCell* This) + void PalMainLoader::UWorld_CleanupWorld(UWorld* This, bool bSessionEnded, bool bCleanupResources, UWorld* NewWorld) { - UWorldPartitionRuntimeLevelStreamingCell_Activate_Hook.call(This); - for (auto& Callback : StreamingCell_Activate_Callbacks) + 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..9a8b170 --- /dev/null +++ b/src/Loader/PalSpawnLoader.cpp @@ -0,0 +1,415 @@ +#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_spawns.clear(); + 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..dc0f028 --- /dev/null +++ b/src/Loader/Spawner/SpawnerInfo.cpp @@ -0,0 +1,98 @@ +#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()) + { + SpawnerActor->K2_DestroyActor(); + SpawnerActor = nullptr; + } + + 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/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 From 5b295af92806e7d1f27a0d3e69bcf77ec259372b Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:56:31 +0200 Subject: [PATCH 14/18] Fix a small error and add some more debug messages --- src/Loader/PalSpawnLoader.cpp | 1 - src/Loader/Spawner/SpawnerInfo.cpp | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Loader/PalSpawnLoader.cpp b/src/Loader/PalSpawnLoader.cpp index 9a8b170..eace6ef 100644 --- a/src/Loader/PalSpawnLoader.cpp +++ b/src/Loader/PalSpawnLoader.cpp @@ -75,7 +75,6 @@ namespace Palworld { spawnInfo.bExistsInWorld = false; } - m_spawns.clear(); m_loadedCells.Empty(); PS::Log(STR("Session ending, spawners have been cleaned up.\n")); diff --git a/src/Loader/Spawner/SpawnerInfo.cpp b/src/Loader/Spawner/SpawnerInfo.cpp index dc0f028..13b1ec1 100644 --- a/src/Loader/Spawner/SpawnerInfo.cpp +++ b/src/Loader/Spawner/SpawnerInfo.cpp @@ -12,8 +12,10 @@ namespace PS { { 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) From 87f88c854fb5001e12737a6a402e09f39932d95c Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:15:06 +0200 Subject: [PATCH 15/18] Add example mod for spawn loader --- .../spawns/new_spawns.jsonc | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 assets/examples/NewSpawnsAtPlateau/spawns/new_spawns.jsonc 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 From a4a697e46899d85b40965df3566ac571649937fa Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sat, 13 Dec 2025 05:35:24 +0200 Subject: [PATCH 16/18] Add documentation for spawner loader --- website/docs/guides/spawners/_category_.json | 7 +++ website/docs/guides/spawners/overview.md | 53 ++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 website/docs/guides/spawners/_category_.json create mode 100644 website/docs/guides/spawners/overview.md 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..fea9104 --- /dev/null +++ b/website/docs/guides/spawners/overview.md @@ -0,0 +1,53 @@ +--- +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 `spawners`, meaning you want something like this `MyModName/spawners/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. +- `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. \ No newline at end of file From c9aac60f37909068cd2f95708f5a4cb798231dd8 Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sat, 13 Dec 2025 05:43:48 +0200 Subject: [PATCH 17/18] Update spawner overview in docs to include additional notes --- website/docs/guides/spawners/overview.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/website/docs/guides/spawners/overview.md b/website/docs/guides/spawners/overview.md index fea9104..9bca2a8 100644 --- a/website/docs/guides/spawners/overview.md +++ b/website/docs/guides/spawners/overview.md @@ -38,7 +38,7 @@ These fields are only available when `Type` is set to `MonoNPC`. ### Sheet These fields are only available when `Type` is set to `Sheet`. -- `SpawnerName`: Name of the spawner. +- `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`: @@ -50,4 +50,8 @@ These fields are only available when `Type` is set to `Sheet`. - `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. \ No newline at end of file + - `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 From d3f4560d8bd1c76846d5d1dc07778ee44c1fa2ab Mon Sep 17 00:00:00 2001 From: Okaetsu <111317036+Okaetsu@users.noreply.github.com> Date: Sat, 13 Dec 2025 05:51:32 +0200 Subject: [PATCH 18/18] docs: Fix folder name in spawners overview --- website/docs/guides/spawners/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/guides/spawners/overview.md b/website/docs/guides/spawners/overview.md index 9bca2a8..53e5430 100644 --- a/website/docs/guides/spawners/overview.md +++ b/website/docs/guides/spawners/overview.md @@ -14,7 +14,7 @@ We will be going through the structure of a spawner json and the required folder ## Folder Name -The required name for the folder so it can be seen by PalSchema is `spawners`, meaning you want something like this `MyModName/spawners/spawn.json`. +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