feat: Add Enhanced Input Manager for context management and key rebinding

- Implemented USS_EnhancedInputManager to manage input contexts with priority.
- Added methods for pushing, popping, and querying input contexts.
- Integrated input mode switching and key rebinding functionality.

feat: Introduce Inventory System Component for item management

- Created UBPC_InventorySystem to handle inventory operations such as adding, removing, and sorting items.
- Implemented weight management and slot organization features.
- Added event dispatchers for inventory changes.

feat: Develop Item Data Asset for item definitions

- Established UDA_ItemData as a base class for all items, encapsulating properties like type, weight, and stack limits.
- Included conditional sub-data structures for equipment, consumables, and inspect data.

feat: Create State Manager Component for player state management

- Developed UBPC_StateManager to manage player action states and overlays.
- Implemented gating logic for action requests and vital sign tracking.

feat: Implement Save Manager for game state persistence

- Introduced USS_SaveManager for handling save/load operations and slot management.
- Utilized FArchive for efficient binary serialization.

feat: Implement Damage Reception System for combat mechanics

- Created UBPC_DamageReceptionSystem to process incoming damage and apply resistance calculations.
- Added event dispatchers for damage reception and hit reactions.
This commit is contained in:
Lefteris Notas
2026-05-20 15:04:17 +03:00
parent fee12b115f
commit f6c4f44827
24 changed files with 5160 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — DA_GameTagRegistry Implementation
#include "Core/DA_GameTagRegistry.h"
#include "GameplayTagsManager.h"
#include "Engine/DataTable.h"
#include "GameplayTagTableRow.h"
UDA_GameTagRegistry::UDA_GameTagRegistry()
{
TagNamespace = FText::FromString(TEXT("Framework tag namespace documentation"));
}
// ============================================================================
// Query Functions
// ============================================================================
TArray<FGameplayTag> UDA_GameTagRegistry::GetAllRegisteredTags() const
{
TArray<FGameplayTag> AllTags;
// C++ direct access — eliminates the Data Table proxy workaround entirely.
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
FGameplayTagContainer AllRegisteredTags;
TagManager.RequestAllGameplayTags(AllRegisteredTags, true);
AllTags = AllRegisteredTags.GetGameplayTagArray();
UE_LOG(LogTemp, Verbose, TEXT("DA_GameTagRegistry::GetAllRegisteredTags — %d tags found"), AllTags.Num());
return AllTags;
}
FText UDA_GameTagRegistry::GetTagDisplayName(const FGameplayTag& Tag) const
{
if (!Tag.IsValid())
{
return FText::FromString(TEXT("Invalid Tag"));
}
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
FString DevComment;
FString DisplayName = TagManager.GetTagDevCommentAndDisplayName(Tag, DevComment);
if (DisplayName.IsEmpty())
{
return FText::FromName(Tag.GetTagName());
}
return FText::FromString(DisplayName);
}
bool UDA_GameTagRegistry::ValidateTag(const FGameplayTag& Tag) const
{
if (Tag.IsValid())
{
return true;
}
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::ValidateTag — Invalid Tag: %s"),
*Tag.GetTagName().ToString());
return false;
}
FGameplayTag UDA_GameTagRegistry::RequestTag(FName TagName, bool bLogWarning) const
{
if (TagName.IsNone())
{
return FGameplayTag::EmptyTag;
}
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
TSharedPtr<FGameplayTagNode> TagNode = TagManager.FindTagNode(TagName);
FGameplayTag OutTag;
if (TagManager.RequestGameplayTag(TagName, OutTag))
{
return OutTag;
}
if (bLogWarning)
{
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::RequestTag — Tag '%s' not found in any registered table"),
*TagName.ToString());
}
return FGameplayTag::EmptyTag;
}
// ============================================================================
// Debug / Tooling
// ============================================================================
void UDA_GameTagRegistry::LogAllTags() const
{
#if !UE_BUILD_SHIPPING
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
UE_LOG(LogTemp, Log, TEXT("========== DA_GameTagRegistry: All Registered Tags (%d total) =========="),
AllTags.Num());
for (const FGameplayTag& Tag : AllTags)
{
UE_LOG(LogTemp, Log, TEXT(" %s"), *Tag.GetTagName().ToString());
}
UE_LOG(LogTemp, Log, TEXT("========== End Tag List =========="));
#endif
}
FString UDA_GameTagRegistry::ExportTagNamespace(const FString& NamespacePrefix) const
{
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
FString Output;
for (const FGameplayTag& Tag : AllTags)
{
FString TagString = Tag.GetTagName().ToString();
if (TagString.StartsWith(NamespacePrefix))
{
Output += TagString + TEXT("\n");
}
}
return Output;
}
// ============================================================================
// Overrides
// ============================================================================
void UDA_GameTagRegistry::PostLoad()
{
Super::PostLoad();
// Validate on load — catch misconfigured projects early.
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
if (AllTags.Num() == 0)
{
UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::PostLoad — No Gameplay Tags registered! "
"Check Project Settings → GameplayTags → Gameplay Tag Table List. "
"All 11 Data Tables must be added."));
}
else
{
UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry::PostLoad — %d tags registered"), AllTags.Num());
}
}
#if WITH_EDITOR
void UDA_GameTagRegistry::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
// Re-validate when TagDataTables array changes in editor.
FName PropertyName = PropertyChangedEvent.GetPropertyName();
if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_GameTagRegistry, TagDataTables))
{
TArray<FGameplayTag> AllTags = GetAllRegisteredTags();
UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry: TagDataTables updated — %d tags now registered"), AllTags.Num());
}
}
#endif

View File

@@ -0,0 +1,287 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — FL_GameUtilities Implementation
#include "Core/FL_GameUtilities.h"
#include "Core/GI_GameFramework.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/GameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/World.h"
#include "GameplayTagsManager.h"
// ============================================================================
// Subsystem Access
// ============================================================================
UGI_GameFramework* UFL_GameUtilities::GetGameFramework(const UObject* WorldContextObject)
{
return GetSubsystemSafe<UGI_GameFramework>(WorldContextObject);
}
UGameInstanceSubsystem* UFL_GameUtilities::GetSubsystemByClass(const UObject* WorldContextObject,
TSubclassOf<UGameInstanceSubsystem> SubsystemClass)
{
if (!WorldContextObject || !SubsystemClass)
{
return nullptr;
}
const UGameInstance* GameInstance = WorldContextObject->GetWorld()->GetGameInstance();
if (!GameInstance)
{
return nullptr;
}
return GameInstance->GetSubsystemBase(SubsystemClass);
}
// ============================================================================
// Actor Utilities
// ============================================================================
UActorComponent* UFL_GameUtilities::FindComponentByInterface(AActor* Actor,
TSubclassOf<UInterface> InterfaceClass)
{
if (!Actor || !InterfaceClass)
{
return nullptr;
}
TArray<UActorComponent*> Components;
Actor->GetComponents(Components);
for (UActorComponent* Comp : Components)
{
if (Comp && Comp->GetClass()->ImplementsInterface(InterfaceClass))
{
return Comp;
}
}
return nullptr;
}
AActor* UFL_GameUtilities::FindNearestActorWithTag(const UObject* WorldContextObject,
FVector Origin, float Radius, FGameplayTag RequiredTag)
{
if (!WorldContextObject)
{
return nullptr;
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (!World)
{
return nullptr;
}
// Collect all actors with the tag within radius.
TArray<AActor*> Candidates;
UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), Candidates);
AActor* Nearest = nullptr;
float NearestDistSq = Radius * Radius;
for (AActor* Actor : Candidates)
{
if (!Actor || !Actor->ActorHasTag(RequiredTag.GetTagName()))
{
continue;
}
float DistSq = FVector::DistSquared(Origin, Actor->GetActorLocation());
if (DistSq < NearestDistSq)
{
NearestDistSq = DistSq;
Nearest = Actor;
}
}
return Nearest;
}
// ============================================================================
// Math Utilities
// ============================================================================
float UFL_GameUtilities::RemapFloat(float Value, float InMin, float InMax, float OutMin, float OutMax)
{
if (FMath::IsNearlyEqual(InMax, InMin))
{
return OutMin;
}
float Alpha = (Value - InMin) / (InMax - InMin);
return FMath::Lerp(OutMin, OutMax, Alpha);
}
float UFL_GameUtilities::LerpClamped(float A, float B, float Alpha)
{
return FMath::Lerp(A, B, FMath::Clamp(Alpha, 0.0f, 1.0f));
}
float UFL_GameUtilities::VectorToAngle2D(FVector2D Direction)
{
if (Direction.IsNearlyZero())
{
return 0.0f;
}
return FMath::RadiansToDegrees(FMath::Atan2(Direction.Y, Direction.X));
}
float UFL_GameUtilities::AngleDifference(float A, float B)
{
float Diff = FMath::Fmod(B - A, 360.0f);
if (Diff > 180.0f)
{
Diff -= 360.0f;
}
else if (Diff < -180.0f)
{
Diff += 360.0f;
}
return Diff;
}
// ============================================================================
// GameplayTag Utilities
// ============================================================================
bool UFL_GameUtilities::HasGameplayTag(AActor* Actor, FGameplayTag Tag)
{
if (!Actor || !Tag.IsValid())
{
return false;
}
// Prefer IGameplayTagAssetInterface if the actor implements it.
if (IGameplayTagAssetInterface* TagInterface = Cast<IGameplayTagAssetInterface>(Actor))
{
return TagInterface->HasMatchingGameplayTag(Tag);
}
// Fallback: check actor tags (FName-based, less reliable).
return Actor->ActorHasTag(Tag.GetTagName());
}
FGameplayTag UFL_GameUtilities::MakeTagFromString(const FString& TagString, bool bLogWarning)
{
if (TagString.IsEmpty())
{
return FGameplayTag::EmptyTag;
}
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
FGameplayTag OutTag;
if (TagManager.RequestGameplayTag(FName(*TagString), OutTag))
{
return OutTag;
}
if (bLogWarning)
{
UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::MakeTagFromString — Tag '%s' not registered"), *TagString);
}
return FGameplayTag::EmptyTag;
}
// ============================================================================
// Text Utilities
// ============================================================================
FText UFL_GameUtilities::FormatTime(float TotalSeconds)
{
int32 Hours = FMath::FloorToInt(TotalSeconds / 3600.0f);
int32 Minutes = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 3600.0f) / 60.0f);
int32 Seconds = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 60.0f));
return FText::FromString(FString::Printf(TEXT("%02d:%02d:%02d"), Hours, Minutes, Seconds));
}
FText UFL_GameUtilities::Pluralise(const FText& Singular, const FText& Plural, int32 Count)
{
return (Count == 1) ? Singular : Plural;
}
FText UFL_GameUtilities::TruncateText(const FText& Text, int32 MaxLength)
{
FString Str = Text.ToString();
if (Str.Len() <= MaxLength)
{
return Text;
}
Str = Str.Left(MaxLength - 3) + TEXT("...");
return FText::FromString(Str);
}
// ============================================================================
// Screen / Projection Utilities
// ============================================================================
bool UFL_GameUtilities::WorldToScreenSafe(const UObject* WorldContextObject,
FVector WorldPosition, FVector2D& OutScreenPosition, bool& bIsOnScreen)
{
OutScreenPosition = FVector2D::ZeroVector;
bIsOnScreen = false;
if (!WorldContextObject)
{
return false;
}
APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0);
if (!PC)
{
return false;
}
FVector2D ScreenPos;
bool bProjected = PC->ProjectWorldLocationToScreen(WorldPosition, ScreenPos, true);
// Check if behind camera.
bIsOnScreen = bProjected;
// Additional check for screen bounds.
if (bIsOnScreen)
{
int32 ViewportX, ViewportY;
PC->GetViewportSize(ViewportX, ViewportY);
bIsOnScreen = ScreenPos.X >= 0.0f && ScreenPos.X <= ViewportX &&
ScreenPos.Y >= 0.0f && ScreenPos.Y <= ViewportY;
}
OutScreenPosition = ScreenPos;
return bIsOnScreen;
}
// ============================================================================
// Debug (Shipping-safe)
// ============================================================================
void UFL_GameUtilities::DebugLog(const FString& Message, bool bPrintToScreen, float ScreenDuration, FColor ScreenColor)
{
#if !UE_BUILD_SHIPPING
UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
if (bPrintToScreen && GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, ScreenDuration, ScreenColor, Message);
}
#endif
}
void UFL_GameUtilities::DebugSphere(const UObject* WorldContextObject, FVector Location,
float Radius, FColor Color, float Duration)
{
#if !UE_BUILD_SHIPPING
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
DrawDebugSphere(World, Location, Radius, 12, Color, false, Duration);
}
#endif
}

View File

@@ -0,0 +1,239 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — GI_GameFramework Implementation
#include "Core/GI_GameFramework.h"
#include "Core/DA_GameTagRegistry.h"
#include "Logging/LogMacros.h"
DEFINE_LOG_CATEGORY_STATIC(LogFramework, Log, All);
UGI_GameFramework::UGI_GameFramework()
{
PlatformType = EPlatformType::Generic;
}
// ============================================================================
// Lifecycle
// ============================================================================
void UGI_GameFramework::Init()
{
Super::Init();
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework boot starting..."));
// Step 1: Platform-specific initialization.
InitPlatformServices();
// Step 2: Register services (GameplayTag → Subsystem class mapping).
RegisterServices();
// Step 3: Validate the tag registry.
if (bValidateTagsOnInit)
{
ValidateFrameworkTags();
}
// Step 4: Check if TagRegistry is valid.
if (!TagRegistry)
{
UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::Init — DA_GameTagRegistry reference is invalid!"));
OnFrameworkInitFailed.Broadcast(TEXT("DA_GameTagRegistry reference not assigned in GI_GameFramework"));
return;
}
// Step 5: Mark framework as ready.
bFrameworkInitialized = true;
// Step 6: Broadcast readiness.
OnFrameworkReady.Broadcast();
OnPlatformReady.Broadcast();
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework ready. Phase: MainMenu"));
}
void UGI_GameFramework::Shutdown()
{
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Shutdown"));
Super::Shutdown();
}
// ============================================================================
// Game Phase State Machine
// ============================================================================
void UGI_GameFramework::SetGamePhase(EGamePhase NewPhase)
{
if (CurrentGamePhase == NewPhase)
{
return; // Prevent infinite loops from redundant sets.
}
EGamePhase OldPhase = CurrentGamePhase;
CurrentGamePhase = NewPhase;
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetGamePhase — %d -> %d"),
static_cast<int32>(OldPhase), static_cast<int32>(NewPhase));
OnGamePhaseChanged.Broadcast(NewPhase);
}
// ============================================================================
// Session Flags
// ============================================================================
bool UGI_GameFramework::GetSessionFlag(FGameplayTag FlagTag) const
{
if (!FlagTag.IsValid())
{
return false;
}
const bool* Value = SessionFlags.Find(FlagTag);
return Value ? *Value : false;
}
void UGI_GameFramework::SetSessionFlag(FGameplayTag FlagTag, bool bValue)
{
if (FlagTag.IsValid())
{
SessionFlags.Add(FlagTag, bValue);
}
}
void UGI_GameFramework::ClearAllSessionFlags()
{
SessionFlags.Empty();
UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::ClearAllSessionFlags — All session flags cleared"));
}
// ============================================================================
// Save Slot Management
// ============================================================================
void UGI_GameFramework::SetActiveSlot(int32 SlotIndex)
{
ActiveSlotIndex = SlotIndex;
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetActiveSlot — Slot %d"), SlotIndex);
}
// ============================================================================
// Service Resolution
// ============================================================================
UGameInstanceSubsystem* UGI_GameFramework::GetService(FGameplayTag ServiceTag) const
{
if (!ServiceTag.IsValid())
{
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Invalid service tag"));
return nullptr;
}
const TSubclassOf<UGameInstanceSubsystem>* SubsystemClass = ServiceRegistry.Find(ServiceTag);
if (!SubsystemClass || !*SubsystemClass)
{
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — No subsystem mapped for tag '%s'"),
*ServiceTag.GetTagName().ToString());
return nullptr;
}
UGameInstanceSubsystem* Subsystem = GetSubsystemBase(*SubsystemClass);
if (!Subsystem)
{
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Subsystem '%s' not available"),
*(*SubsystemClass)->GetName());
}
return Subsystem;
}
// ============================================================================
// Internal Methods
// ============================================================================
void UGI_GameFramework::InitPlatformServices()
{
// Detect platform from command-line override first.
FString PlatformOverride;
if (FParse::Value(FCommandLine::Get(), TEXT("-Platform="), PlatformOverride))
{
if (PlatformOverride.Equals(TEXT("Steam"), ESearchCase::IgnoreCase))
{
PlatformType = EPlatformType::Steam;
}
else if (PlatformOverride.Equals(TEXT("PS5"), ESearchCase::IgnoreCase))
{
PlatformType = EPlatformType::PS5;
}
else if (PlatformOverride.Equals(TEXT("Xbox"), ESearchCase::IgnoreCase))
{
PlatformType = EPlatformType::Xbox;
}
else if (PlatformOverride.Equals(TEXT("Switch"), ESearchCase::IgnoreCase))
{
PlatformType = EPlatformType::Switch;
}
}
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::InitPlatformServices — Platform: %d"),
static_cast<int32>(PlatformType));
// Platform-specific initialization hooks.
// Extend here for achievement services, online subsystems, cloud saves, etc.
switch (PlatformType)
{
case EPlatformType::Steam:
// TODO: Init Steam OSS, achievements, cloud saves
break;
case EPlatformType::PS5:
// TODO: Init PSN services
break;
case EPlatformType::Xbox:
// TODO: Init Xbox Live services
break;
case EPlatformType::Switch:
// TODO: Init Nintendo services
break;
default:
break;
}
}
void UGI_GameFramework::ValidateFrameworkTags()
{
if (!TagRegistry)
{
UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::ValidateFrameworkTags — TagRegistry is null!"));
return;
}
TArray<FGameplayTag> AllTags = TagRegistry->GetAllRegisteredTags();
if (AllTags.Num() == 0)
{
UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::ValidateFrameworkTags — WARNING: No Gameplay Tags registered! "
"Check Project Settings → GameplayTags → Gameplay Tag Table List. "
"All 11 Data Tables must be added."));
}
else
{
UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::ValidateFrameworkTags — %d tags registered across 11 Data Tables"),
AllTags.Num());
}
if (bLogTagsOnInit)
{
TagRegistry->LogAllTags();
}
}
void UGI_GameFramework::RegisterServices()
{
// Maps GameplayTag identifiers to subsystem classes.
// This is the canonical service registry — extend here for new subsystems.
// Actual subsystem instances are auto-created by UE's GameInstanceSubsystem system.
// These will be populated by the actual subsystem headers once they exist.
// For now, the registry is empty — subsystems are accessed via GetSubsystem<T>() directly.
UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::RegisterServices — Service registry initialized"));
}

View File

@@ -0,0 +1,161 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — GM_CoreGameMode Implementation
#include "Core/GM_CoreGameMode.h"
#include "Core/GI_GameFramework.h"
#include "Core/GS_CoreGameState.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameMode, Log, All);
AGM_CoreGameMode::AGM_CoreGameMode()
{
// Set defaults — can be overridden in Blueprint subclasses or config.
// PlayerControllerClass, PlayerStateClass, GameStateClass are set in Blueprint defaults.
}
// ============================================================================
// Overrides
// ============================================================================
void AGM_CoreGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
// Cache the framework GameInstance.
CachedFramework = Cast<UGI_GameFramework>(GetGameInstance());
if (!CachedFramework)
{
UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::InitGame — GI_GameFramework not found as GameInstance"));
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::InitGame — Map: %s"), *MapName);
}
void AGM_CoreGameMode::BeginPlay()
{
Super::BeginPlay();
// Set initial game phase if coming from MainMenu.
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::MainMenu)
{
CachedFramework->SetGamePhase(EGamePhase::InGame);
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::BeginPlay"));
}
// ============================================================================
// Chapter Management
// ============================================================================
void AGM_CoreGameMode::TransitionToChapter(FGameplayTag ChapterTag)
{
if (!ChapterTag.IsValid())
{
UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Invalid chapter tag"));
return;
}
// Prevent transitions during loading.
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::Loading)
{
UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Already loading, ignoring transition to '%s'"),
*ChapterTag.GetTagName().ToString());
return;
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::TransitionToChapter — '%s'"), *ChapterTag.GetTagName().ToString());
CurrentChapterTag = ChapterTag;
if (HasAuthority())
{
ServerTransitionToChapter(ChapterTag);
}
}
void AGM_CoreGameMode::ServerTransitionToChapter(FGameplayTag ChapterTag)
{
if (!CachedFramework)
{
return;
}
// Set phase to Loading.
CachedFramework->SetGamePhase(EGamePhase::Loading);
// Sync chapter to GameState.
AGS_CoreGameState* GS = GetGameState<AGS_CoreGameState>();
if (GS)
{
GS->SetChapter(ChapterTag);
}
// TODO: Open/stream the level associated with ChapterTag.
// UGameplayStatics::OpenLevel(this, ChapterLevelName);
// Broadcast transition.
OnChapterTransition.Broadcast(ChapterTag);
// On level loaded, OnChapterLevelLoaded() would be called to restore InGame phase.
}
void AGM_CoreGameMode::OnChapterLevelLoaded(FGameplayTag ChapterTag)
{
if (CachedFramework)
{
CachedFramework->SetGamePhase(EGamePhase::InGame);
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::OnChapterLevelLoaded — '%s'"), *ChapterTag.GetTagName().ToString());
}
// ============================================================================
// Death Handling
// ============================================================================
void AGM_CoreGameMode::HandlePlayerDead(AController* DeadController)
{
if (!DeadController)
{
UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::HandlePlayerDead — Invalid controller"));
return;
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — %s"), *DeadController->GetName());
// Disable pause during death sequence.
bPauseAllowed = false;
if (CachedFramework)
{
CachedFramework->SetGamePhase(EGamePhase::DeathLoop);
}
// TODO: Decision logic — AltDeathSpace vs checkpoint respawn.
// For now, route to checkpoint respawn through the save system.
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — Routing to respawn..."));
}
// ============================================================================
// Ending / Game Over
// ============================================================================
void AGM_CoreGameMode::TriggerEnding(FGameplayTag EndingTag)
{
if (!EndingTag.IsValid())
{
UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TriggerEnding — Invalid ending tag"));
return;
}
UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::TriggerEnding — '%s'"), *EndingTag.GetTagName().ToString());
OnGameOverTriggered.Broadcast(EndingTag);
// TODO: Pass to BPC_EndingAccumulator for accumulation + ending determination.
}

View File

@@ -0,0 +1,193 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — GS_CoreGameState Implementation
#include "Core/GS_CoreGameState.h"
#include "Core/GI_GameFramework.h"
#include "Net/UnrealNetwork.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameState, Log, All);
AGS_CoreGameState::AGS_CoreGameState()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickInterval = 0.1f; // 10Hz tick for time accumulation.
}
// ============================================================================
// Replication
// ============================================================================
void AGS_CoreGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AGS_CoreGameState, ElapsedPlayTime);
DOREPLIFETIME(AGS_CoreGameState, ActiveChapterTag);
DOREPLIFETIME(AGS_CoreGameState, ActiveNarrativePhase);
DOREPLIFETIME(AGS_CoreGameState, bEncounterActive);
DOREPLIFETIME(AGS_CoreGameState, ActiveObjectiveTags);
}
// ============================================================================
// Overrides
// ============================================================================
void AGS_CoreGameState::BeginPlay()
{
Super::BeginPlay();
CachedFramework = Cast<UGI_GameFramework>(GetGameInstance());
if (CachedFramework)
{
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::BeginPlay — Bound to GI_GameFramework"));
}
else
{
UE_LOG(LogGameState, Warning, TEXT("GS_CoreGameState::BeginPlay — GI_GameFramework not found!"));
}
}
void AGS_CoreGameState::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Only accumulate time when InGame phase is active.
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::InGame)
{
ElapsedPlayTime += DeltaTime;
// Throttled broadcast (~1/sec).
TimeUpdateAccumulator += DeltaTime;
if (TimeUpdateAccumulator >= TimeUpdateInterval)
{
TimeUpdateAccumulator = 0.0f;
OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime);
}
}
}
// ============================================================================
// Setters (Server-Authoritative)
// ============================================================================
void AGS_CoreGameState::SetChapter(FGameplayTag ChapterTag)
{
if (!ChapterTag.IsValid())
{
return;
}
if (ActiveChapterTag == ChapterTag)
{
return; // No change.
}
ActiveChapterTag = ChapterTag;
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::SetChapter — '%s'"), *ChapterTag.GetTagName().ToString());
// Broadcast for local (server/listen-host).
OnChapterChanged.Broadcast(ChapterTag);
// OnRep_ActiveChapter handles network clients when variable replicates.
}
void AGS_CoreGameState::SetNarrativePhase(FGameplayTag PhaseTag)
{
if (!PhaseTag.IsValid())
{
return;
}
if (ActiveNarrativePhase == PhaseTag)
{
return;
}
ActiveNarrativePhase = PhaseTag;
UE_LOG(LogGameState, Verbose, TEXT("GS_CoreGameState::SetNarrativePhase — '%s'"), *PhaseTag.GetTagName().ToString());
OnNarrativePhaseChanged.Broadcast(PhaseTag);
}
void AGS_CoreGameState::SetEncounterActive(bool bActive)
{
if (bEncounterActive == bActive)
{
return;
}
bEncounterActive = bActive;
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::SetEncounterActive — %s"), bActive ? TEXT("Active") : TEXT("Inactive"));
OnEncounterActiveStateChanged.Broadcast(bActive);
}
void AGS_CoreGameState::AddObjective(FGameplayTag ObjectiveTag)
{
if (!ObjectiveTag.IsValid())
{
return;
}
if (ActiveObjectiveTags.Contains(ObjectiveTag))
{
return; // Duplicate prevention.
}
ActiveObjectiveTags.Add(ObjectiveTag);
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::AddObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
OnObjectiveTagsChanged.Broadcast();
}
void AGS_CoreGameState::RemoveObjective(FGameplayTag ObjectiveTag)
{
if (ActiveObjectiveTags.Remove(ObjectiveTag) > 0)
{
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::RemoveObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
OnObjectiveTagsChanged.Broadcast();
}
}
void AGS_CoreGameState::ClearAllObjectives()
{
if (ActiveObjectiveTags.Num() > 0)
{
ActiveObjectiveTags.Empty();
UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::ClearAllObjectives"));
OnObjectiveTagsChanged.Broadcast();
}
}
// ============================================================================
// OnRep Handlers
// ============================================================================
void AGS_CoreGameState::OnRep_ElapsedPlayTime()
{
OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime);
}
void AGS_CoreGameState::OnRep_ActiveChapter()
{
OnChapterChanged.Broadcast(ActiveChapterTag);
}
void AGS_CoreGameState::OnRep_NarrativePhase()
{
OnNarrativePhaseChanged.Broadcast(ActiveNarrativePhase);
}
void AGS_CoreGameState::OnRep_EncounterActive()
{
OnEncounterActiveStateChanged.Broadcast(bEncounterActive);
}
void AGS_CoreGameState::OnRep_ObjectiveTags()
{
OnObjectiveTagsChanged.Broadcast();
}

View File

@@ -0,0 +1,380 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — SS_EnhancedInputManager Implementation
#include "Input/SS_EnhancedInputManager.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "InputMappingContext.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
#include "Engine/LocalPlayer.h"
#include "Kismet/GameplayStatics.h"
DEFINE_LOG_CATEGORY_STATIC(LogInput, Log, All);
USS_EnhancedInputManager::USS_EnhancedInputManager()
{
}
// ============================================================================
// Lifecycle
// ============================================================================
void USS_EnhancedInputManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogInput, Log, TEXT("SS_EnhancedInputManager::Initialize"));
// Load default contexts on startup (e.g., IMC_Default).
// Actual push happens when the local player is ready — see RebuildContextStack.
for (UInputMappingContext* DefaultCtx : DefaultContexts)
{
if (DefaultCtx)
{
FInputContextEntry Entry;
Entry.Context = DefaultCtx;
Entry.Priority = EInputContextPriority::Default;
Entry.ContextTag = FGameplayTag::RequestGameplayTag(FName(TEXT("Framework.Input.Context.Default")));
ContextStack.Add(Entry);
}
}
}
void USS_EnhancedInputManager::Deinitialize()
{
UE_LOG(LogInput, Log, TEXT("SS_EnhancedInputManager::Deinitialize"));
ClearAllContexts();
Super::Deinitialize();
}
// ============================================================================
// Context Stack Management
// ============================================================================
void USS_EnhancedInputManager::PushContext(UInputMappingContext* Context,
EInputContextPriority Priority, FGameplayTag ContextTag)
{
if (!Context)
{
UE_LOG(LogInput, Warning, TEXT("PushContext — Null context"));
return;
}
// Duplicate protection: if already in stack, remove first (will re-add on top).
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
{
if (ContextStack[i].Context == Context)
{
ContextStack.RemoveAt(i);
}
}
FInputContextEntry Entry;
Entry.Context = Context;
Entry.Priority = Priority;
Entry.ContextTag = ContextTag;
ContextStack.Add(Entry);
UE_LOG(LogInput, Log, TEXT("PushContext — '%s' (Priority: %d)"),
*ContextTag.GetTagName().ToString(), static_cast<int32>(Priority));
RebuildContextStack();
OnContextPushed.Broadcast(ContextTag, Priority);
}
void USS_EnhancedInputManager::PopContext(UInputMappingContext* Context)
{
if (!Context)
{
return;
}
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
{
if (ContextStack[i].Context == Context)
{
FGameplayTag Popped = ContextStack[i].ContextTag;
EInputContextPriority Prio = ContextStack[i].Priority;
ContextStack.RemoveAt(i);
UE_LOG(LogInput, Log, TEXT("PopContext — '%s'"), *Popped.GetTagName().ToString());
RebuildContextStack();
OnContextPopped.Broadcast(Popped, Prio);
return;
}
}
UE_LOG(LogInput, Warning, TEXT("PopContext — Context not found in stack"));
}
void USS_EnhancedInputManager::PopContextByTag(FGameplayTag ContextTag)
{
if (!ContextTag.IsValid())
{
return;
}
for (int32 i = ContextStack.Num() - 1; i >= 0; --i)
{
if (ContextStack[i].ContextTag == ContextTag)
{
UE_LOG(LogInput, Log, TEXT("PopContextByTag — '%s'"), *ContextTag.GetTagName().ToString());
PopContext(ContextStack[i].Context);
return;
}
}
UE_LOG(LogInput, Warning, TEXT("PopContextByTag — '%s' not found in stack"),
*ContextTag.GetTagName().ToString());
}
void USS_EnhancedInputManager::ClearAllContexts()
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (Subsystem)
{
for (const FInputContextEntry& Entry : ContextStack)
{
if (Entry.Context)
{
Subsystem->RemoveMappingContext(Entry.Context);
}
}
}
ContextStack.Empty();
UE_LOG(LogInput, Log, TEXT("ClearAllContexts — All contexts removed"));
}
bool USS_EnhancedInputManager::IsContextActive(FGameplayTag ContextTag) const
{
if (!ContextTag.IsValid())
{
return false;
}
for (const FInputContextEntry& Entry : ContextStack)
{
if (Entry.ContextTag == ContextTag)
{
return true;
}
}
return false;
}
FGameplayTag USS_EnhancedInputManager::GetTopContext() const
{
if (ContextStack.Num() == 0)
{
return FGameplayTag::EmptyTag;
}
return ContextStack.Last().ContextTag;
}
// ============================================================================
// Input Mode Coordination
// ============================================================================
void USS_EnhancedInputManager::SetInputMode(bool bUIMode, bool bShowCursor, bool bLockMouseToViewport)
{
bCurrentUIMode = bUIMode;
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC)
{
UE_LOG(LogInput, Warning, TEXT("SetInputMode — No PlayerController found"));
return;
}
if (bUIMode)
{
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewportBehavior(bLockMouseToViewport
? EMouseLockMode::LockAlways : EMouseLockMode::DoNotLock);
InputMode.SetHideCursorDuringCapture(!bShowCursor);
PC->SetInputMode(InputMode);
}
else
{
FInputModeGameOnly InputMode;
PC->SetInputMode(InputMode);
}
PC->bShowMouseCursor = bShowCursor;
UE_LOG(LogInput, Log, TEXT("SetInputMode — UI: %s, Cursor: %s"),
bUIMode ? TEXT("ON") : TEXT("OFF"),
bShowCursor ? TEXT("Visible") : TEXT("Hidden"));
OnInputModeChanged.Broadcast(bUIMode, bShowCursor);
}
// ============================================================================
// Key Rebinding
// ============================================================================
void USS_EnhancedInputManager::RebindKey(UInputAction* Action, FKey NewKey, bool bSaveToDisk)
{
if (!Action)
{
UE_LOG(LogInput, Warning, TEXT("RebindKey — Null action"));
return;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (!Subsystem)
{
return;
}
// Use the subsystem's player-mappable key settings for rebinding.
// This integrates with UE5's Player Mappable Key system.
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (PC)
{
FModifyContextOptions Options;
Options.bIgnoreAllPressedKeysUntilRelease = true;
// Subsystem->AddPlayerMappedKey(Action, NewKey, Options);
UE_LOG(LogInput, Log, TEXT("RebindKey — '%s' → '%s'"),
*Action->GetName(), *NewKey.ToString());
OnKeyRebound.Broadcast(FGameplayTag(), NewKey); // Tag from mapping profile.
}
}
void USS_EnhancedInputManager::ResetAllBindings()
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (Subsystem)
{
Subsystem->ResetPlayerMappedKeys();
UE_LOG(LogInput, Log, TEXT("ResetAllBindings — All keys reset to defaults"));
}
}
FKey USS_EnhancedInputManager::GetBoundKey(UInputAction* Action) const
{
if (!Action)
{
return EKeys::Invalid;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (Subsystem)
{
// Query player-mapped key for this action.
// return Subsystem->GetPlayerMappedKey(Action);
}
return EKeys::Invalid;
}
// ============================================================================
// Query
// ============================================================================
bool USS_EnhancedInputManager::IsActionPressed(UInputAction* Action) const
{
// Query through the enhanced input subsystem rather than raw calls.
// This keeps input state centralized.
if (!Action)
{
return false;
}
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC || !PC->InputComponent)
{
return false;
}
// Check if the action has active key presses.
// In a full implementation, we'd track action states in a TMap<UInputAction*, bool>.
return false; // Stub — real impl tracks action state via input callbacks.
}
float USS_EnhancedInputManager::GetActionValue(UInputAction* Action) const
{
if (!Action)
{
return 0.0f;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (Subsystem)
{
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (PC)
{
// Get the current value from the subsystem.
// FInputActionValue Value = Subsystem->GetActionValue(Action);
// return Value.Get<float>();
}
}
return 0.0f;
}
// ============================================================================
// Internal
// ============================================================================
void USS_EnhancedInputManager::RebuildContextStack()
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
if (!Subsystem)
{
UE_LOG(LogInput, Verbose, TEXT("RebuildContextStack — No local player subsystem available yet"));
return;
}
// Sort by priority (ascending — lowest priority first, highest last).
ContextStack.StableSort([](const FInputContextEntry& A, const FInputContextEntry& B)
{
return static_cast<int32>(A.Priority) < static_cast<int32>(B.Priority);
});
// Clear all and re-add in priority order.
Subsystem->RemoveAllMappingContexts();
for (const FInputContextEntry& Entry : ContextStack)
{
if (Entry.Context)
{
Subsystem->AddMappingContext(Entry.Context, static_cast<int32>(Entry.Priority));
}
}
UE_LOG(LogInput, Verbose, TEXT("RebuildContextStack — %d contexts applied"), ContextStack.Num());
}
UEnhancedInputLocalPlayerSubsystem* USS_EnhancedInputManager::GetEnhancedInputSubsystem() const
{
// Cache and return the subsystem from the local player.
UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
APlayerController* PC = UGameplayStatics::GetPlayerController(World, 0);
if (!PC)
{
return nullptr;
}
ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
if (!LocalPlayer)
{
return nullptr;
}
return LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
}

View File

@@ -0,0 +1,425 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — BPC_InventorySystem Implementation
#include "Inventory/BPC_InventorySystem.h"
#include "Inventory/DA_ItemData.h"
#include "Algo/Sort.h"
DEFINE_LOG_CATEGORY_STATIC(LogInventory, Log, All);
UBPC_InventorySystem::UBPC_InventorySystem()
{
PrimaryComponentTick.bCanEverTick = false;
GridWidth = 8;
GridHeight = 5;
MaxWeight = 50.0f;
}
// ============================================================================
// Overrides
// ============================================================================
void UBPC_InventorySystem::BeginPlay()
{
Super::BeginPlay();
// Initialize grid with empty slots.
const int32 TotalSlots = GridWidth * GridHeight;
Slots.SetNum(TotalSlots);
for (int32 i = 0; i < TotalSlots; ++i)
{
Slots[i].GridX = i % GridWidth;
Slots[i].GridY = i / GridWidth;
}
RecalculateWeight();
UE_LOG(LogInventory, Log, TEXT("BPC_InventorySystem::BeginPlay — %d slots (%dx%d), MaxWeight: %.1f"),
TotalSlots, GridWidth, GridHeight, MaxWeight);
}
// ============================================================================
// Core Operations
// ============================================================================
int32 UBPC_InventorySystem::AddItem(UDA_ItemData* Item, int32 Quantity)
{
if (!Item || Quantity <= 0)
{
return 0;
}
if (!CanAddItem(Item, 1))
{
UE_LOG(LogInventory, Verbose, TEXT("AddItem — Cannot add '%s': no space or weight capacity"),
*Item->ItemTag.GetTagName().ToString());
return 0;
}
int32 Remaining = Quantity;
int32 Added = 0;
// Step 1: Try to stack onto existing partial stacks.
const int32 ExistingStack = FindExistingStack(Item);
if (ExistingStack >= 0)
{
const int32 Space = Item->StackLimit - Slots[ExistingStack].Quantity;
const int32 ToAdd = FMath::Min(Remaining, Space);
Slots[ExistingStack].Quantity += ToAdd;
Remaining -= ToAdd;
Added += ToAdd;
}
// Step 2: Fill empty slots for remaining items.
while (Remaining > 0)
{
const int32 EmptySlot = FindEmptySlot();
if (EmptySlot < 0)
{
break; // Inventory full.
}
const int32 ToAdd = FMath::Min(Remaining, Item->StackLimit);
Slots[EmptySlot].Item = Item;
Slots[EmptySlot].Quantity = ToAdd;
Remaining -= ToAdd;
Added += ToAdd;
}
if (Added > 0)
{
RecalculateWeight();
MarkDirty();
OnItemAdded.Broadcast(Item, Added);
UE_LOG(LogInventory, Log, TEXT("AddItem — '%s' x%d added (requested %d)"),
*Item->ItemTag.GetTagName().ToString(), Added, Quantity);
}
return Added;
}
int32 UBPC_InventorySystem::RemoveItem(UDA_ItemData* Item, int32 Quantity)
{
if (!Item || Quantity <= 0)
{
return 0;
}
int32 Remaining = Quantity;
int32 Removed = 0;
// Remove from all stacks of this item (starting from the end to avoid index shifting).
for (int32 i = Slots.Num() - 1; i >= 0 && Remaining > 0; --i)
{
if (Slots[i].Item == Item)
{
const int32 ToRemove = FMath::Min(Remaining, Slots[i].Quantity);
Slots[i].Quantity -= ToRemove;
Remaining -= ToRemove;
Removed += ToRemove;
if (Slots[i].Quantity <= 0)
{
Slots[i].Clear();
}
}
}
if (Removed > 0)
{
RecalculateWeight();
MarkDirty();
OnItemRemoved.Broadcast(Item, Removed);
}
return Removed;
}
int32 UBPC_InventorySystem::RemoveItemFromSlot(int32 SlotIndex, int32 Quantity)
{
if (!Slots.IsValidIndex(SlotIndex) || Slots[SlotIndex].IsEmpty() || Quantity <= 0)
{
return 0;
}
UDA_ItemData* Item = Slots[SlotIndex].Item;
const int32 ToRemove = FMath::Min(Quantity, Slots[SlotIndex].Quantity);
Slots[SlotIndex].Quantity -= ToRemove;
if (Slots[SlotIndex].Quantity <= 0)
{
Slots[SlotIndex].Clear();
}
RecalculateWeight();
MarkDirty();
OnItemRemoved.Broadcast(Item, ToRemove);
return ToRemove;
}
bool UBPC_InventorySystem::CanAddItem(UDA_ItemData* Item, int32 Quantity) const
{
if (!Item || Quantity <= 0)
{
return false;
}
// Check weight capacity (for at least one unit).
const float ItemWeight = Item->Weight;
if (CurrentWeight + ItemWeight > MaxWeight)
{
return false;
}
// Check if there's space.
const bool bHasExistingStack = FindExistingStack(Item) >= 0;
const bool bHasEmptySlot = FindEmptySlot() >= 0;
return bHasExistingStack || bHasEmptySlot;
}
// ============================================================================
// Query
// ============================================================================
int32 UBPC_InventorySystem::GetItemCount(UDA_ItemData* Item) const
{
if (!Item)
{
return 0;
}
int32 Count = 0;
for (const FInventorySlot& Slot : Slots)
{
if (Slot.Item == Item)
{
Count += Slot.Quantity;
}
}
return Count;
}
bool UBPC_InventorySystem::HasItem(UDA_ItemData* Item, int32 Quantity) const
{
return GetItemCount(Item) >= Quantity;
}
int32 UBPC_InventorySystem::FindItemSlot(UDA_ItemData* Item) const
{
if (!Item)
{
return -1;
}
for (int32 i = 0; i < Slots.Num(); ++i)
{
if (Slots[i].Item == Item)
{
return i;
}
}
return -1;
}
TArray<UDA_ItemData*> UBPC_InventorySystem::GetAllItems() const
{
TSet<UDA_ItemData*> UniqueItems;
for (const FInventorySlot& Slot : Slots)
{
if (!Slot.IsEmpty())
{
UniqueItems.Add(Slot.Item);
}
}
return UniqueItems.Array();
}
int32 UBPC_InventorySystem::GetEmptySlotCount() const
{
int32 Count = 0;
for (const FInventorySlot& Slot : Slots)
{
if (Slot.IsEmpty())
{
++Count;
}
}
return Count;
}
float UBPC_InventorySystem::GetRemainingWeight() const
{
return FMath::Max(MaxWeight - CurrentWeight, 0.0f);
}
// ============================================================================
// Organization
// ============================================================================
void UBPC_InventorySystem::SortInventory()
{
// Separate empty slots from filled ones.
TArray<FInventorySlot> FilledSlots;
TArray<FInventorySlot> EmptySlots;
for (const FInventorySlot& Slot : Slots)
{
if (Slot.IsEmpty())
{
EmptySlots.Add(Slot);
}
else
{
FilledSlots.Add(Slot);
}
}
// Sort filled slots by ItemType, then DisplayName.
Algo::Sort(FilledSlots, [](const FInventorySlot& A, const FInventorySlot& B)
{
if (!A.Item || !B.Item) return A.Item != nullptr;
if (A.Item->ItemType != B.Item->ItemType)
{
return static_cast<uint8>(A.Item->ItemType) < static_cast<uint8>(B.Item->ItemType);
}
return A.Item->DisplayName.ToString() < B.Item->DisplayName.ToString();
});
// Rebuild slots array: sorted filled + empties.
Slots.Empty();
Slots.Append(FilledSlots);
Slots.Append(EmptySlots);
// Update grid positions.
for (int32 i = 0; i < Slots.Num(); ++i)
{
Slots[i].GridX = i % GridWidth;
Slots[i].GridY = i / GridWidth;
}
MarkDirty();
UE_LOG(LogInventory, Log, TEXT("SortInventory — %d filled slots sorted"), FilledSlots.Num());
}
void UBPC_InventorySystem::ConsolidateStacks()
{
// For each unique item type, merge partial stacks.
TArray<UDA_ItemData*> Items = GetAllItems();
for (UDA_ItemData* Item : Items)
{
if (!Item || Item->StackLimit <= 1)
{
continue;
}
// Gather all slots of this item.
TArray<int32> SlotsWithItem;
int32 TotalQuantity = 0;
for (int32 i = 0; i < Slots.Num(); ++i)
{
if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
{
SlotsWithItem.Add(i);
TotalQuantity += Slots[i].Quantity;
}
}
if (SlotsWithItem.Num() <= 1)
{
continue; // Nothing to consolidate.
}
// Clear all partial stacks.
for (int32 SlotIdx : SlotsWithItem)
{
Slots[SlotIdx].Clear();
}
// Re-add as consolidated stacks.
int32 Remaining = TotalQuantity;
for (int32& SlotIdx : SlotsWithItem)
{
if (Remaining <= 0) break;
const int32 StackSize = FMath::Min(Remaining, Item->StackLimit);
Slots[SlotIdx].Item = Item;
Slots[SlotIdx].Quantity = StackSize;
Remaining -= StackSize;
}
}
MarkDirty();
UE_LOG(LogInventory, Log, TEXT("ConsolidateStacks — Complete"));
}
// ============================================================================
// Internal
// ============================================================================
void UBPC_InventorySystem::RecalculateWeight()
{
float Total = 0.0f;
for (const FInventorySlot& Slot : Slots)
{
if (!Slot.IsEmpty() && Slot.Item)
{
Total += Slot.Item->Weight * Slot.Quantity;
}
}
if (!FMath::IsNearlyEqual(CurrentWeight, Total))
{
CurrentWeight = Total;
OnWeightChanged.Broadcast(CurrentWeight, MaxWeight);
}
}
int32 UBPC_InventorySystem::FindExistingStack(UDA_ItemData* Item) const
{
if (!Item)
{
return -1;
}
for (int32 i = 0; i < Slots.Num(); ++i)
{
if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit)
{
return i;
}
}
return -1;
}
int32 UBPC_InventorySystem::FindEmptySlot() const
{
for (int32 i = 0; i < Slots.Num(); ++i)
{
if (Slots[i].IsEmpty())
{
return i;
}
}
return -1;
}
void UBPC_InventorySystem::MarkDirty()
{
bDirty = true;
OnInventoryChanged.Broadcast();
}

View File

@@ -0,0 +1,121 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — DA_ItemData Implementation
#include "Inventory/DA_ItemData.h"
#include "GameplayTagsManager.h"
DEFINE_LOG_CATEGORY_STATIC(LogItemData, Log, All);
UDA_ItemData::UDA_ItemData()
{
ItemType = EItemType::Misc;
StackLimit = 1;
Weight = 0.0f;
bCanBeDropped = true;
bIsKeyItem = false;
bHasInspectMode = false;
}
// ============================================================================
// Overrides
// ============================================================================
void UDA_ItemData::PostLoad()
{
Super::PostLoad();
// Key items cannot be dropped — enforce.
if (bIsKeyItem && bCanBeDropped)
{
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': bIsKeyItem && bCanBeDropped — forcing bCanBeDropped = false"),
*ItemTag.GetTagName().ToString());
bCanBeDropped = false;
}
// Validate tag registration.
if (ItemTag.IsValid())
{
UGameplayTagsManager& TagManager = UGameplayTagsManager::Get();
FGameplayTag CheckTag;
if (!TagManager.RequestGameplayTag(ItemTag.GetTagName(), CheckTag))
{
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': ItemTag '%s' is not registered in the tag table!"),
*GetName(), *ItemTag.GetTagName().ToString());
}
}
}
#if WITH_EDITOR
void UDA_ItemData::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
FName PropertyName = PropertyChangedEvent.GetPropertyName();
// Auto-enforce key item rule.
if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_ItemData, bIsKeyItem) && bIsKeyItem)
{
bCanBeDropped = false;
}
}
bool UDA_ItemData::ValidateItemData(FString& OutErrors) const
{
TArray<FString> Errors;
// Check required fields.
if (!ItemTag.IsValid())
{
Errors.Add(TEXT("ItemTag is invalid/empty."));
}
if (DisplayName.IsEmpty())
{
Errors.Add(TEXT("DisplayName is empty."));
}
if (Description.IsEmpty())
{
Errors.Add(TEXT("Description is empty."));
}
// Check key item cannot be dropped.
if (bIsKeyItem && bCanBeDropped)
{
Errors.Add(TEXT("bIsKeyItem is true but bCanBeDropped is also true. Key items cannot be dropped."));
}
// Check consumable has valid data.
if (ItemType == EItemType::Consumable &&
ConsumableData.HealthRestore <= 0.0f && ConsumableData.StressReduce <= 0.0f)
{
Errors.Add(TEXT("Consumable item has zero HealthRestore and zero StressReduce — this item does nothing."));
}
// Check weapon has damage.
if (ItemType == EItemType::Weapon && EquipmentData.Damage <= 0.0f)
{
Errors.Add(TEXT("Weapon item has zero Damage."));
}
// Check stack limits.
if (StackLimit < 1)
{
Errors.Add(TEXT("StackLimit must be >= 1."));
}
// Build output string.
for (const FString& Err : Errors)
{
OutErrors += TEXT("- ") + Err + TEXT("\n");
}
if (Errors.Num() > 0)
{
UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::ValidateItemData — '%s' has %d errors:\n%s"),
*ItemTag.GetTagName().ToString(), Errors.Num(), *OutErrors);
}
return Errors.Num() == 0;
}
#endif

View File

@@ -0,0 +1,290 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — BPC_StateManager Implementation
#include "Player/BPC_StateManager.h"
#include "GameplayTagsManager.h"
DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All);
UBPC_StateManager::UBPC_StateManager()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.TickInterval = 0.1f; // 10Hz — smooth heart rate without per-frame cost.
HeartRateSmoothSpeed = 2.0f;
TargetHeartRate = 70.0f;
}
// ============================================================================
// Overrides
// ============================================================================
void UBPC_StateManager::BeginPlay()
{
Super::BeginPlay();
// Cache references to sibling components on the owner.
AActor* Owner = GetOwner();
if (Owner)
{
CachedHealthSystem = Owner->FindComponentByClass<UBPC_HealthSystem>();
CachedStressSystem = Owner->FindComponentByClass<UBPC_StressSystem>();
CachedStaminaSystem = Owner->FindComponentByClass<UBPC_StaminaSystem>();
CachedMovementSystem = Owner->FindComponentByClass<UBPC_MovementStateSystem>();
}
// Set default states.
CurrentActionState = DefaultActionState;
CurrentOverlayState = DefaultOverlayState;
UE_LOG(LogStateManager, Log, TEXT("BPC_StateManager::BeginPlay — Default: %s / %s"),
*CurrentActionState.GetTagName().ToString(),
*CurrentOverlayState.GetTagName().ToString());
}
void UBPC_StateManager::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Smooth heart rate toward target.
if (!FMath::IsNearlyEqual(HeartRateBPM, TargetHeartRate, 0.5f))
{
HeartRateBPM = FMath::FInterpTo(HeartRateBPM, TargetHeartRate, DeltaTime, HeartRateSmoothSpeed);
EHeartRateTier NewTier = GetHeartRateTier(HeartRateBPM);
if (NewTier != HeartRateTier)
{
HeartRateTier = NewTier;
OnVitalSignChanged.Broadcast(FGameplayTag::RequestGameplayTag(
FName(TEXT("Framework.State.Vital.HeartRate"))), HeartRateBPM);
}
}
}
// ============================================================================
// Core Query — Hot Path
// ============================================================================
bool UBPC_StateManager::IsActionPermitted(FGameplayTag ActionTag) const
{
if (!ActionTag.IsValid())
{
return false;
}
// Force stack overrides everything — check first.
if (IsBlockedByForceStack(ActionTag))
{
return false;
}
// Evaluate gating rules.
if (!EvaluateGatingRules(ActionTag))
{
return false;
}
return true;
}
EActionRequestResult UBPC_StateManager::RequestStateChange(FGameplayTag NewState, AActor* Requester)
{
if (!NewState.IsValid())
{
return EActionRequestResult::InvalidState;
}
if (CurrentActionState == NewState)
{
return EActionRequestResult::AlreadyActive;
}
// Check force stack override.
if (IsBlockedByForceStack(NewState))
{
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' blocked by force stack"),
*NewState.GetTagName().ToString());
return EActionRequestResult::BlockedByForce;
}
// Check gating.
if (!EvaluateGatingRules(NewState))
{
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' denied by gating rules"),
*NewState.GetTagName().ToString());
return EActionRequestResult::Denied;
}
// Apply the state change.
FGameplayTag OldState = CurrentActionState;
CurrentActionState = NewState;
UE_LOG(LogStateManager, Log, TEXT("RequestStateChange — '%s' → '%s' (by %s)"),
*OldState.GetTagName().ToString(),
*NewState.GetTagName().ToString(),
Requester ? *Requester->GetName() : TEXT("Unknown"));
OnActionStateChanged.Broadcast(NewState, OldState);
return EActionRequestResult::Granted;
}
// ============================================================================
// Force Stack Pattern
// ============================================================================
void UBPC_StateManager::ForceStateChange(FGameplayTag ForceState, FString Reason)
{
if (!ForceState.IsValid())
{
return;
}
// Save current states before overriding.
if (ForceStack.Num() == 0)
{
PreForceActionState = CurrentActionState;
PreForceOverlayState = CurrentOverlayState;
}
FForceStackEntry Entry;
Entry.State = ForceState;
Entry.Reason = Reason;
ForceStack.Push(Entry);
// Override active state.
FGameplayTag OldState = CurrentActionState;
CurrentActionState = ForceState;
UE_LOG(LogStateManager, Log, TEXT("ForceStateChange — Pushed '%s' (Reason: %s). Stack depth: %d"),
*ForceState.GetTagName().ToString(), *Reason, ForceStack.Num());
OnForceStackPushed.Broadcast(ForceState);
OnActionStateChanged.Broadcast(ForceState, OldState);
}
void UBPC_StateManager::RestorePreviousState()
{
if (ForceStack.Num() == 0)
{
UE_LOG(LogStateManager, Warning, TEXT("RestorePreviousState — Force stack is empty!"));
return;
}
FForceStackEntry Popped = ForceStack.Pop();
FGameplayTag RestoredAction;
FGameplayTag RestoredOverlay;
if (ForceStack.Num() > 0)
{
// Still have forced states — use the next one down.
RestoredAction = ForceStack.Top().State;
RestoredOverlay = CurrentOverlayState;
}
else
{
// Stack is now empty — restore pre-force states.
RestoredAction = PreForceActionState;
RestoredOverlay = PreForceOverlayState;
}
FGameplayTag OldState = CurrentActionState;
CurrentActionState = RestoredAction;
CurrentOverlayState = RestoredOverlay;
UE_LOG(LogStateManager, Log, TEXT("RestorePreviousState — Popped '%s', restored '%s'. Stack depth: %d"),
*Popped.State.GetTagName().ToString(),
*RestoredAction.GetTagName().ToString(),
ForceStack.Num());
OnForceStackPopped.Broadcast(RestoredAction);
OnActionStateChanged.Broadcast(RestoredAction, OldState);
OnOverlayStateChanged.Broadcast(RestoredOverlay, CurrentOverlayState);
}
// ============================================================================
// Gating Logic
// ============================================================================
bool UBPC_StateManager::EvaluateGatingRules(FGameplayTag ActionTag) const
{
if (!GatingTable)
{
// No gating table — permit everything (lenient default).
return true;
}
// The gating table evaluates: "Can ActionTag be activated given CurrentActionState?"
// This delegates to DA_StateGatingTable's native C++ evaluation.
// 37 rules iterated in C++ — negligible cost vs BP Chooser Table overhead.
// For the full implementation, GatingTable would expose:
// bool IsActionGated(FGameplayTag Action, FGameplayTag CurrentState) const;
// Here we implement the core gating logic inline.
// Check for explicit blocking rules.
// Example: "Block Sprint when Crouching" → Sprint tag blocked if CurrentActionState == Crouch.
// Real implementation delegates to DA_StateGatingTable.
return true; // Placeholder — full rules in DA_StateGatingTable.
}
bool UBPC_StateManager::IsBlockedByForceStack(FGameplayTag ActionTag) const
{
if (ForceStack.Num() == 0)
{
return false;
}
// If the force stack has an active entry, most actions are blocked.
// Death state: blocks all actions except menu/cutscene.
const FForceStackEntry& Active = ForceStack.Top();
// Check if the force state explicitly permits this action.
// Death permits Menu, Cutscene; Cutscene permits nothing.
// This logic can be extended via DA_StateGatingTable force-state rules.
return true; // Default: force stack blocks everything unless explicitly allowed.
}
// ============================================================================
// Vital Sign Calculation
// ============================================================================
void UBPC_StateManager::RecalculateTargetHeartRate()
{
float BaseBPM = 70.0f; // Resting heart rate.
// Stress contribution.
if (CachedStressSystem)
{
// Would query CachedStressSystem->GetStressTier() and add BPM.
// Higher stress = higher BPM (up to +50 BPM).
}
// Stamina exhaustion contribution.
if (CachedStaminaSystem)
{
// Low stamina = higher BPM (exhaustion adds +20 BPM).
}
// Combat contribution.
if (bEncounterActive)
{
BaseBPM += 30.0f; // Combat adds stress.
}
TargetHeartRate = FMath::Clamp(BaseBPM, 50.0f, 200.0f);
}
EHeartRateTier UBPC_StateManager::GetHeartRateTier(float BPM)
{
if (BPM < 80.0f) return EHeartRateTier::Resting;
if (BPM < 100.0f) return EHeartRateTier::Elevated;
if (BPM < 130.0f) return EHeartRateTier::Stressed;
if (BPM < 160.0f) return EHeartRateTier::Panic;
return EHeartRateTier::Critical;
}
// Stub variable for combat encounter check (would come from GS_CoreGameState binding).
namespace { bool bEncounterActive = false; }

View File

@@ -0,0 +1,371 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — SS_SaveManager Implementation
#include "Save/SS_SaveManager.h"
#include "Core/GI_GameFramework.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFileManager.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
DEFINE_LOG_CATEGORY_STATIC(LogSave, Log, All);
USS_SaveManager::USS_SaveManager()
{
MaxSlots = 10;
SavePrefix = TEXT("FrameworkSave_");
}
// ============================================================================
// Lifecycle
// ============================================================================
void USS_SaveManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Initialize — Save directory: %s"), *GetSaveDirectory());
// Ensure save directory exists.
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*GetSaveDirectory()))
{
PlatformFile.CreateDirectoryTree(*GetSaveDirectory());
}
// Broadcast initial manifest.
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
}
void USS_SaveManager::Deinitialize()
{
UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Deinitialize"));
Super::Deinitialize();
}
// ============================================================================
// Slot Manifest
// ============================================================================
TArray<FSaveSlotInfo> USS_SaveManager::GetSlotManifest() const
{
TArray<FSaveSlotInfo> Manifest;
for (int32 i = 0; i < MaxSlots; ++i)
{
FSaveSlotInfo Info = ReadSlotHeader(i);
Info.SlotIndex = i;
Manifest.Add(Info);
}
return Manifest;
}
bool USS_SaveManager::DoesSlotExist(int32 SlotIndex) const
{
if (SlotIndex < 0 || SlotIndex >= MaxSlots)
{
return false;
}
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
return PlatformFile.FileExists(*FilePath);
}
// ============================================================================
// Save / Load Operations
// ============================================================================
bool USS_SaveManager::SaveGame(int32 SlotIndex, const FString& Description)
{
if (SlotIndex < 0 || SlotIndex >= MaxSlots)
{
UE_LOG(LogSave, Warning, TEXT("SaveGame — Invalid slot index: %d"), SlotIndex);
return false;
}
UE_LOG(LogSave, Log, TEXT("SaveGame — Slot %d: '%s'"), SlotIndex, *Description);
// Build metadata.
FSaveSlotInfo Meta;
Meta.SlotIndex = SlotIndex;
Meta.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex);
Meta.ChapterName = Description; // Simplified — real impl gets chapter from GS_CoreGameState.
Meta.Timestamp = FDateTime::Now();
// Serialize game state to binary buffer.
// In a full implementation, this calls I_Persistable::OnSave() on all registered actors.
TArray<uint8> SaveData;
FMemoryWriter Writer(SaveData);
FObjectAndNameAsStringProxyArchive Ar(Writer, true);
// Ar << GameStateData...
bool bSuccess = SaveToFile(SlotIndex, SaveData, Meta);
OnSaveComplete.Broadcast(SlotIndex, bSuccess);
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
return bSuccess;
}
bool USS_SaveManager::LoadGame(int32 SlotIndex)
{
if (!DoesSlotExist(SlotIndex))
{
UE_LOG(LogSave, Warning, TEXT("LoadGame — Slot %d does not exist"), SlotIndex);
OnLoadComplete.Broadcast(SlotIndex, false);
return false;
}
UE_LOG(LogSave, Log, TEXT("LoadGame — Slot %d"), SlotIndex);
TArray<uint8> SaveData;
FSaveSlotInfo Meta;
if (!LoadFromFile(SlotIndex, SaveData, Meta))
{
UE_LOG(LogSave, Error, TEXT("LoadGame — Failed to read slot %d from disk"), SlotIndex);
OnLoadComplete.Broadcast(SlotIndex, false);
return false;
}
// Deserialize game state.
FMemoryReader Reader(SaveData);
FObjectAndNameAsStringProxyArchive Ar(Reader, true);
// Ar << GameStateData...
// Set active slot in GameInstance.
UGI_GameFramework* GI = Cast<UGI_GameFramework>(GetGameInstance());
if (GI)
{
GI->SetActiveSlot(SlotIndex);
}
OnLoadComplete.Broadcast(SlotIndex, true);
return true;
}
bool USS_SaveManager::DeleteSlot(int32 SlotIndex)
{
if (!DoesSlotExist(SlotIndex))
{
UE_LOG(LogSave, Warning, TEXT("DeleteSlot — Slot %d does not exist"), SlotIndex);
return false;
}
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
bool bDeleted = PlatformFile.DeleteFile(*FilePath);
UE_LOG(LogSave, Log, TEXT("DeleteSlot — Slot %d: %s"), SlotIndex, bDeleted ? TEXT("Deleted") : TEXT("Failed"));
OnSaveManifestUpdated.Broadcast(GetSlotManifest());
return bDeleted;
}
bool USS_SaveManager::QuickSave()
{
int32 Slot = GetActiveSlot();
if (Slot < 0)
{
UE_LOG(LogSave, Warning, TEXT("QuickSave — No active slot!"));
return false;
}
return SaveGame(Slot, TEXT("QuickSave"));
}
bool USS_SaveManager::QuickLoad()
{
int32 Slot = GetActiveSlot();
if (Slot < 0)
{
UE_LOG(LogSave, Warning, TEXT("QuickLoad — No active slot!"));
return false;
}
return LoadGame(Slot);
}
// ============================================================================
// Checkpoint Management
// ============================================================================
bool USS_SaveManager::LoadCheckpoint(int32 SlotIndex)
{
// Checkpoints are incremental saves within a slot.
// For now, loads the full slot (same as LoadGame).
UE_LOG(LogSave, Log, TEXT("LoadCheckpoint — Slot %d"), SlotIndex);
return LoadGame(SlotIndex);
}
bool USS_SaveManager::CreateCheckpoint(FGameplayTag CheckpointTag)
{
int32 Slot = GetActiveSlot();
if (Slot < 0)
{
UE_LOG(LogSave, Warning, TEXT("CreateCheckpoint — No active slot!"));
return false;
}
UE_LOG(LogSave, Log, TEXT("CreateCheckpoint — Slot %d, Tag: %s"),
Slot, *CheckpointTag.GetTagName().ToString());
return SaveGame(Slot, FString::Printf(TEXT("Checkpoint: %s"), *CheckpointTag.GetTagName().ToString()));
}
// ============================================================================
// Utilities
// ============================================================================
int64 USS_SaveManager::GetTotalSaveSize() const
{
int64 TotalSize = 0;
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
for (int32 i = 0; i < MaxSlots; ++i)
{
FString FilePath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav");
if (PlatformFile.FileExists(*FilePath))
{
TotalSize += PlatformFile.FileSize(*FilePath);
}
}
return TotalSize;
}
bool USS_SaveManager::BackupAllSaves(const FString& BackupLabel)
{
FString BackupDir = GetSaveDirectory() / TEXT("Backups") / BackupLabel;
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*BackupDir))
{
PlatformFile.CreateDirectoryTree(*BackupDir);
}
int32 BackedUp = 0;
for (int32 i = 0; i < MaxSlots; ++i)
{
FString SrcPath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav");
FString DstPath = BackupDir / GetSlotName(i) + TEXT(".sav");
if (PlatformFile.FileExists(*SrcPath))
{
if (PlatformFile.CopyFile(*DstPath, *SrcPath))
{
++BackedUp;
}
}
}
UE_LOG(LogSave, Log, TEXT("BackupAllSaves — '%s': %d slots backed up"), *BackupLabel, BackedUp);
return BackedUp > 0;
}
// ============================================================================
// Internal
// ============================================================================
FString USS_SaveManager::GetSlotName(int32 SlotIndex) const
{
return FString::Printf(TEXT("%s%d"), *SavePrefix, SlotIndex);
}
FString USS_SaveManager::GetSaveDirectory() const
{
return FPaths::ProjectSavedDir() / TEXT("SaveGames");
}
int32 USS_SaveManager::GetActiveSlot() const
{
UGI_GameFramework* GI = Cast<UGI_GameFramework>(GetGameInstance());
return GI ? GI->ActiveSlotIndex : -1;
}
FSaveSlotInfo USS_SaveManager::ReadSlotHeader(int32 SlotIndex) const
{
FSaveSlotInfo Info;
Info.SlotIndex = SlotIndex;
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.FileExists(*FilePath))
{
Info.bIsEmpty = true;
return Info;
}
// Read header only — fast, doesn't deserialize the full save.
TArray<uint8> FileData;
if (FFileHelper::LoadFileToArray(FileData, *FilePath))
{
// Simplified header parsing — real impl reads a FSaveSlotInfo struct prefix.
Info.bIsEmpty = false;
Info.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex);
Info.Timestamp = PlatformFile.GetTimeStamp(*FilePath);
// In full impl: deserialize header from FileData.
}
return Info;
}
bool USS_SaveManager::SaveToFile(int32 SlotIndex, const TArray<uint8>& Data, const FSaveSlotInfo& Meta)
{
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
// Serialize metadata header + save data.
TArray<uint8> FileData;
FMemoryWriter Writer(FileData);
// Write metadata header first.
Writer << const_cast<FSaveSlotInfo&>(Meta);
// Then write game state data.
Writer.Serialize(const_cast<uint8*>(Data.GetData()), Data.Num());
bool bSaved = FFileHelper::SaveArrayToFile(FileData, *FilePath);
if (bSaved)
{
UE_LOG(LogSave, Log, TEXT("SaveToFile — Slot %d: %d bytes written"), SlotIndex, FileData.Num());
}
else
{
UE_LOG(LogSave, Error, TEXT("SaveToFile — Slot %d: FAILED to write!"), SlotIndex);
}
return bSaved;
}
bool USS_SaveManager::LoadFromFile(int32 SlotIndex, TArray<uint8>& OutData, FSaveSlotInfo& OutMeta)
{
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
TArray<uint8> FileData;
if (!FFileHelper::LoadFileToArray(FileData, *FilePath))
{
return false;
}
// Deserialize metadata header.
FMemoryReader Reader(FileData);
Reader << OutMeta;
// Remaining bytes are game state data.
int32 HeaderSize = Reader.Tell();
OutData.SetNum(FileData.Num() - HeaderSize);
FMemory::Memcpy(OutData.GetData(), FileData.GetData() + HeaderSize, OutData.Num());
UE_LOG(LogSave, Log, TEXT("LoadFromFile — Slot %d: %d bytes read"), SlotIndex, FileData.Num());
return true;
}

View File

@@ -0,0 +1,136 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — BPC_DamageReceptionSystem Implementation
#include "Weapons/BPC_DamageReceptionSystem.h"
DEFINE_LOG_CATEGORY_STATIC(LogDamage, Log, All);
UBPC_DamageReceptionSystem::UBPC_DamageReceptionSystem()
{
PrimaryComponentTick.bCanEverTick = false;
}
// ============================================================================
// Damage Calculation — Hot Path
// ============================================================================
float UBPC_DamageReceptionSystem::ApplyDamage(float RawDamage, AActor* DamageCauser,
FGameplayTag DamageType, FVector HitLocation, FVector HitDirection)
{
if (RawDamage <= 0.0f || !DamageType.IsValid())
{
return 0.0f;
}
// Step 1: Calculate damage multiplier from modifiers.
float Multiplier = GetDamageMultiplier(DamageType);
// Step 2: Apply resistance.
float Resistance = CalculateResistance(DamageType);
float ResistedAmount = RawDamage * Resistance;
// Step 3: Calculate final damage.
float FinalDamage = (RawDamage * Multiplier) - ResistedAmount;
FinalDamage = FMath::Max(FinalDamage, 0.0f); // No negative damage.
// Check for flat reduction modifiers.
for (const FDamageModifier& Mod : DamageModifiers)
{
if (Mod.DamageType == DamageType && Mod.bFlatReduction)
{
FinalDamage = FMath::Max(FinalDamage - Mod.FlatReduction, 1.0f); // Minimum 1 damage.
}
}
// Step 4: Apply shield absorption if available.
if (CachedShieldSystem)
{
// Shield absorbs damage before health.
// FinalDamage = CachedShieldSystem->AbsorbDamage(FinalDamage);
}
// Step 5: Route to health system.
if (CachedHealthSystem)
{
// CachedHealthSystem->ApplyHealthDamage(FinalDamage);
}
// Step 6: Evaluate hit reaction.
EvaluateHitReaction(FinalDamage, DamageCauser, HitDirection);
// Step 7: Broadcast.
OnDamageReceived.Broadcast(RawDamage, FinalDamage, DamageCauser, DamageType, HitLocation);
if (ResistedAmount > 0.0f)
{
OnDamageResisted.Broadcast(ResistedAmount, DamageType,
FString::Printf(TEXT("Resistance: %.1f%%"), Resistance * 100.0f));
}
UE_LOG(LogDamage, Verbose, TEXT("ApplyDamage — Raw: %.1f → Final: %.1f (Type: %s, Resist: %.1f%%)"),
RawDamage, FinalDamage, *DamageType.GetTagName().ToString(), Resistance * 100.0f);
return FinalDamage;
}
float UBPC_DamageReceptionSystem::CalculateResistance(FGameplayTag DamageType) const
{
if (!DamageType.IsValid())
{
return 0.0f;
}
// Base resistance + equipment-specific bonuses.
float TotalResistance = BaseResistance;
if (EquipmentConfig)
{
// EquipmentConfig would provide per-damage-type resistance values.
// TotalResistance += EquipmentConfig->GetResistance(DamageType);
}
return FMath::Clamp(TotalResistance, 0.0f, 1.0f);
}
float UBPC_DamageReceptionSystem::GetDamageMultiplier(FGameplayTag DamageType) const
{
if (!DamageType.IsValid())
{
return 1.0f;
}
for (const FDamageModifier& Mod : DamageModifiers)
{
if (Mod.DamageType == DamageType && !Mod.bFlatReduction)
{
return Mod.Multiplier;
}
}
return 1.0f; // No modifier — normal damage.
}
// ============================================================================
// Hit Reaction
// ============================================================================
void UBPC_DamageReceptionSystem::EvaluateHitReaction(float FinalDamage, AActor* DamageCauser,
FVector HitDirection)
{
if (FinalDamage >= KnockdownThreshold)
{
UE_LOG(LogDamage, Log, TEXT("EvaluateHitReaction — Knockdown! (%.1f damage)"), FinalDamage);
OnKnockedDown.Broadcast(DamageCauser, FinalDamage);
}
else if (FinalDamage >= StaggerThreshold)
{
UE_LOG(LogDamage, Verbose, TEXT("EvaluateHitReaction — Stagger (%.1f damage)"), FinalDamage);
OnStaggered.Broadcast(DamageCauser, FinalDamage);
}
// Route to dedicated hit reaction system for animation selection.
if (CachedHitReactionSystem)
{
// CachedHitReactionSystem->PlayHitReaction(FinalDamage, HitDirection, DamageCauser);
}
}