diff --git a/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset b/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset new file mode 100644 index 0000000..e6a0629 Binary files /dev/null and b/Content/ZMMO/Data/Jobs/DA_Job_Novice.uasset differ diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp index 8263248..853bd01 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp @@ -5,6 +5,7 @@ #include "Engine/World.h" #include "TimerManager.h" #include "UI/Widgets/UIButton_Base.h" +#include "ZMMOJobsLibrary.h" void UUICharCard_Base::NativeConstruct() { @@ -48,7 +49,7 @@ void UUICharCard_Base::SetFromSummary(const FZMMOCharSummary& InSummary) } if (Text_Class) { - Text_Class->SetText(FText::FromString(FString::Printf(TEXT("Classe %d"), Summary.ClassId))); + Text_Class->SetText(UZMMOJobsLibrary::GetJobDisplayName(this, Summary.ClassId)); } const ESlateVisibility ActionVis = bDeleteScheduled ? ESlateVisibility::Collapsed : ESlateVisibility::Visible; diff --git a/Source/ZMMO/ZMMO.Build.cs b/Source/ZMMO/ZMMO.Build.cs index 80ae3e4..f911655 100644 --- a/Source/ZMMO/ZMMO.Build.cs +++ b/Source/ZMMO/ZMMO.Build.cs @@ -19,7 +19,8 @@ public class ZMMO : ModuleRules "CommonInput", "GameplayTags", "ZeusNetwork", - "ZMMOAttributes" + "ZMMOAttributes", + "ZMMOJobs" }); PrivateDependencyModuleNames.AddRange(new string[] { diff --git a/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp b/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp new file mode 100644 index 0000000..2631308 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobDataAsset.cpp @@ -0,0 +1,5 @@ +#include "ZMMOJobDataAsset.h" + +// Implementacao vazia — todas as props sao UPROPERTY EditDefaultsOnly e +// vivem inline no .uasset. Override de GetPrimaryAssetId() esta no header +// (constexpr-friendly). diff --git a/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp new file mode 100644 index 0000000..0f4b861 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsLibrary.cpp @@ -0,0 +1,54 @@ +#include "ZMMOJobsLibrary.h" + +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "ZMMOJobDataAsset.h" +#include "ZMMOJobsSubsystem.h" + +namespace +{ +/// Resolve Subsystem via WorldContextObject. Retorna nullptr em contextos +/// invalidos (CDO em editor, world em destruicao). +UZMMOJobsSubsystem* ResolveSubsystem(const UObject* WorldContextObject) +{ + if (WorldContextObject == nullptr) { return nullptr; } + const UWorld* World = WorldContextObject->GetWorld(); + if (World == nullptr) { return nullptr; } + UGameInstance* GI = World->GetGameInstance(); + if (GI == nullptr) { return nullptr; } + return GI->GetSubsystem(); +} +} // namespace + +FText UZMMOJobsLibrary::GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + if (UZMMOJobDataAsset* Job = Sub->GetJobData(ClassId)) + { + return Job->DisplayName.IsEmpty() + ? FText::FromString(Job->TechnicalName) + : Job->DisplayName; + } + } + // Fallback: nao crasha UI se ClassId desconhecido. + return FText::FromString(FString::Printf(TEXT("Classe %d"), ClassId)); +} + +UZMMOJobDataAsset* UZMMOJobsLibrary::GetJobData(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetJobData(ClassId); + } + return nullptr; +} + +bool UZMMOJobsLibrary::IsJobRegistered(const UObject* WorldContextObject, int32 ClassId) +{ + if (UZMMOJobsSubsystem* Sub = ResolveSubsystem(WorldContextObject)) + { + return Sub->GetJobData(ClassId) != nullptr; + } + return false; +} diff --git a/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp b/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp new file mode 100644 index 0000000..4bfc787 --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsModule.cpp @@ -0,0 +1,13 @@ +#include "ZMMOJobsModule.h" + +#include "Modules/ModuleManager.h" + +void FZMMOJobsModule::StartupModule() +{ +} + +void FZMMOJobsModule::ShutdownModule() +{ +} + +IMPLEMENT_MODULE(FZMMOJobsModule, ZMMOJobs) diff --git a/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp new file mode 100644 index 0000000..bcdac0a --- /dev/null +++ b/Source/ZMMOJobs/Private/ZMMOJobsSubsystem.cpp @@ -0,0 +1,67 @@ +#include "ZMMOJobsSubsystem.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Modules/ModuleManager.h" +#include "ZMMOJobDataAsset.h" + +DEFINE_LOG_CATEGORY_STATIC(LogZMMOJobs, Log, All); + +void UZMMOJobsSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + LoadJobsFromAssetRegistry(); + UE_LOG(LogZMMOJobs, Log, TEXT("[ZMMOJobsSubsystem] Initialize: %d job(s) carregados"), CachedJobs.Num()); +} + +void UZMMOJobsSubsystem::Deinitialize() +{ + CachedJobs.Empty(); + Super::Deinitialize(); +} + +void UZMMOJobsSubsystem::LoadJobsFromAssetRegistry() +{ + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked("AssetRegistry"); + IAssetRegistry& AR = AssetRegistryModule.Get(); + + // Editor pode chamar antes de scan inicial. Standalone/PIE scan ja' rodou. + // SearchAllAssets(false) = nao-blocking; assume cache populado em runtime. +#if WITH_EDITOR + AR.SearchAllAssets(true); // bloqueia editor; ok em Initialize +#endif + + TArray Assets; + const FTopLevelAssetPath ClassPath(UZMMOJobDataAsset::StaticClass()->GetClassPathName()); + AR.GetAssetsByClass(ClassPath, Assets, /*bSearchSubClasses*/ true); + + for (const FAssetData& AssetData : Assets) + { + UZMMOJobDataAsset* Job = Cast(AssetData.GetAsset()); + if (Job == nullptr) { continue; } + + if (CachedJobs.Contains(Job->ClassId)) + { + UE_LOG(LogZMMOJobs, Warning, + TEXT("[ZMMOJobsSubsystem] Duplicate ClassId=%d (existing=%s, new=%s) — ignorando segundo"), + Job->ClassId, + *CachedJobs[Job->ClassId]->GetName(), + *Job->GetName()); + continue; + } + + CachedJobs.Add(Job->ClassId, Job); + UE_LOG(LogZMMOJobs, Verbose, + TEXT("[ZMMOJobsSubsystem] Cached classId=%d name=%s display=%s"), + Job->ClassId, *Job->TechnicalName, *Job->DisplayName.ToString()); + } +} + +UZMMOJobDataAsset* UZMMOJobsSubsystem::GetJobData(int32 ClassId) const +{ + if (const TObjectPtr* Found = CachedJobs.Find(ClassId)) + { + return Found->Get(); + } + return nullptr; +} diff --git a/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h b/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h new file mode 100644 index 0000000..dcb6c3a --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobDataAsset.h @@ -0,0 +1,75 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "ZMMOJobDataAsset.generated.h" + +class UTexture2D; +class USoundBase; +class UAnimInstance; +class UAnimMontage; + +/** + * Dados visuais/UI de UM job (classe). 1 .uasset por job em + * `Content/ZMMO/Data/Jobs/DA_Job_.uasset`. + * + * ClassId eh a CHAVE — bate com `classId` do snapshot do server (rathena + * pc.hpp `job_class`, ex: Novice=0, Swordsman=1, Mage=2, ...). + * + * Soft pointers (TSoftObjectPtr) sao usados pra assets pesados (textures, + * sounds) — lazy-loaded quando precisar (level up, hover icon). DataAsset + * em si fica em memoria depois do scan do Subsystem. + * + * Async pointers (TSubclassOf) sao sync — anim BP precisa estar carregada + * quando o player spawna. + */ +UCLASS(BlueprintType) +class ZMMOJOBS_API UZMMOJobDataAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /// Chave estavel (bate com `Config/Jobs/.json:Id` no server e com + /// `FZMMOAttributesSnapshot::ClassId` recebido do server). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + int32 ClassId = 0; + + /// Nome tecnico estavel (en-US). Ex: "Novice", "Swordsman". Aparece em + /// logs e telemetria — NUNCA exibido pro player. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + FString TechnicalName; + + /// Nome localizado exibido na UI (pt-BR no projeto base). Ex: "Aprendiz". + /// Pode usar FText Localization tables pra multi-idioma futuro. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job") + FText DisplayName; + + /// Icone do job — usado em StatusWindow, CharacterCard, JobChangeUI. + /// Soft ptr: nao carrega ate alguem chamar LoadSynchronous() ou async load. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Assets") + TSoftObjectPtr JobIcon; + + /// AnimInstance class override (opcional). Quando nao-null, AnimBP + /// padrao do PlayerCharacter eh substituido por esta classe no spawn + /// (jobs com move set diferente — Monge bate, Assassin furtivo, etc.). + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim") + TSubclassOf DefaultAnimInstanceClass; + + /// Montage de "job change" (animacao de transformacao). Tocada quando + /// player promove de Novice → 1a classe. V1 nao implementado. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Anim") + TSoftObjectPtr JobChangeMontage; + + /// Som de level up override. Quando null, HUD usa som generico do projeto. + /// Soft ptr: lazy load no primeiro level up. + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Zeus|Job|Audio") + TSoftObjectPtr LevelUpSoundCue; + + /// PrimaryAssetType pro AssetManager (precisa pra GetPrimaryAssetIdList). + /// Hardcoded "ZMMOJob" — registry classifica todos os DAs deste tipo. + virtual FPrimaryAssetId GetPrimaryAssetId() const override + { + return FPrimaryAssetId(FPrimaryAssetType(TEXT("ZMMOJob")), + *FString::Printf(TEXT("Job_%d_%s"), ClassId, *TechnicalName)); + } +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h new file mode 100644 index 0000000..825b348 --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsLibrary.h @@ -0,0 +1,39 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "ZMMOJobsLibrary.generated.h" + +class UZMMOJobDataAsset; + +/** + * Helpers Blueprint pra UI/HUD acessar dados de job sem precisar pegar o + * Subsystem manualmente. Todos usam WorldContextObject pra resolver + * GameInstance → Subsystem internamente. + * + * Padrao de uso em UMG: + * - Text binding: `JobsLibrary::GetJobDisplayName(Self, AttrComp.Current.ClassId)` + * - Image binding: `JobsLibrary::GetJobIcon(Self, ClassId).LoadSynchronous()` + */ +UCLASS() +class ZMMOJOBS_API UZMMOJobsLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /// Nome localizado do job. Fallback "Classe N" se ClassId nao tem DA + /// correspondente (job nao registrado no Content/ZMMO/Data/Jobs/). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static FText GetJobDisplayName(const UObject* WorldContextObject, int32 ClassId); + + /// DataAsset completo (pode ser nullptr — caller checa). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static UZMMOJobDataAsset* GetJobData(const UObject* WorldContextObject, int32 ClassId); + + /// Helper de UI: retorna true se ClassId tem DA registrado. + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs", + meta = (WorldContext = "WorldContextObject")) + static bool IsJobRegistered(const UObject* WorldContextObject, int32 ClassId); +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsModule.h b/Source/ZMMOJobs/Public/ZMMOJobsModule.h new file mode 100644 index 0000000..89817bb --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsModule.h @@ -0,0 +1,23 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" + +/** + * Sub-modulo cliente do JobsSystem. + * + * Espelho de `Server/ZeusServerEngine/Game/MMO/Modules/JobsSystem/`. Roda + * como modulo Runtime em LoadingPhase PreDefault para que o + * `UZMMOJobsSubsystem` esteja registrado antes do ZMMO core spawnar a HUD. + * + * Responsabilidades: + * - Dados visuais por job: DisplayName localizado, JobIcon, anim override + * - Lookup classId → UZMMOJobDataAsset via Subsystem + * - Library Blueprint pra HUD/UI consumir + */ +class FZMMOJobsModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h new file mode 100644 index 0000000..7f8a103 --- /dev/null +++ b/Source/ZMMOJobs/Public/ZMMOJobsSubsystem.h @@ -0,0 +1,52 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "ZMMOJobsSubsystem.generated.h" + +class UZMMOJobDataAsset; + +/** + * Cache central de UZMMOJobDataAsset por ClassId. Vive como GameInstance + * Subsystem — startup automatico quando GameInstance inicializa (PIE, + * standalone, packaged). Sobrevive a map transitions. + * + * Initialize: + * 1. Carrega via AssetRegistry: scan de UZMMOJobDataAsset em todo Content + * 2. Cacheia em TMap por ClassId + * 3. Log: quantos jobs encontrou + * + * Uso: + * - C++: GameInstance->GetSubsystem()->GetJobData(0) + * - BP: UZMMOJobsLibrary::GetJobData(WorldContext, 0) + * + * Fase 7+ (Mod support): SCANEAR PrimaryAssetType="ZMMOJob" do AssetManager + * permite mods adicionarem DAs proprios sem mexer no source. + */ +UCLASS() +class ZMMOJOBS_API UZMMOJobsSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /// Lookup por classId. Retorna nullptr se ClassId nao tem DA correspondente + /// (caller usa Library com fallback FText). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + UZMMOJobDataAsset* GetJobData(int32 ClassId) const; + + /// Total de jobs cacheados (diagnostico). + UFUNCTION(BlueprintPure, Category = "Zeus|Jobs") + int32 GetJobCount() const { return CachedJobs.Num(); } + +private: + /// Scan AssetRegistry + popula CachedJobs. Sincronizo (V1) — chamado UMA + /// vez no Initialize. Pra 50 jobs eh aceitavel; se crescer pra 200+ pode + /// virar async no futuro. + void LoadJobsFromAssetRegistry(); + + UPROPERTY() + TMap> CachedJobs; +}; diff --git a/Source/ZMMOJobs/ZMMOJobs.Build.cs b/Source/ZMMOJobs/ZMMOJobs.Build.cs new file mode 100644 index 0000000..f5f200f --- /dev/null +++ b/Source/ZMMOJobs/ZMMOJobs.Build.cs @@ -0,0 +1,26 @@ +using UnrealBuildTool; + +public class ZMMOJobs : ModuleRules +{ + public ZMMOJobs(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { + "Core", + "CoreUObject", + "Engine" + }); + + PrivateDependencyModuleNames.AddRange(new string[] { + "AssetRegistry" // FAssetRegistryModule pra scan de DA_Job_* + }); + + // Espelho do `Server/.../JobsSystem/module.json` (modulo nucleo MMO). + // Cliente roda em LoadingPhase PreDefault — Subsystem inicializa + // ANTES de o ZMMO core criar a player character / HUD. + // + // Sem dependencia em ZMMOAttributes: server envia classId no snapshot, + // cliente faz lookup local. Mantem o peer-module pattern. + } +} diff --git a/Source/ZMMOJobs/module.json b/Source/ZMMOJobs/module.json new file mode 100644 index 0000000..9afae77 --- /dev/null +++ b/Source/ZMMOJobs/module.json @@ -0,0 +1,18 @@ +{ + "name": "ZMMOJobs", + "gameType": "MMO", + "version": "0.1.0", + "side": "client", + "dependencies": { + "engine": ["Core", "CoreUObject", "Engine", "AssetRegistry"], + "modules": [] + }, + "publicHeaders": [ + "ZMMOJobsModule.h", + "ZMMOJobDataAsset.h", + "ZMMOJobsSubsystem.h", + "ZMMOJobsLibrary.h" + ], + "loadingPhase": "PreDefault", + "description": "Sub-modulo cliente do GameType MMO — dados visuais/UI por job (DisplayName localizado, ícone, anim override, som de level up). Espelha JobsSystem do server mas focado em apresentacao. NAO depende de ZMMOAttributes — server envia classId no snapshot, cliente faz lookup local. Pattern peer-module estabelecido no PR #9." +} diff --git a/ZMMO.uproject b/ZMMO.uproject index 7d4ee6e..a06e717 100644 --- a/ZMMO.uproject +++ b/ZMMO.uproject @@ -13,6 +13,11 @@ "Name": "ZMMOAttributes", "Type": "Runtime", "LoadingPhase": "PreDefault" + }, + { + "Name": "ZMMOJobs", + "Type": "Runtime", + "LoadingPhase": "PreDefault" } ], "Plugins": [