Function bodies 468 total
DetectOrphanItems method · cpp · L172-L202 (31 LOC)Source/VaultCore/Private/Audit/AnomalyDetectionSubsystem.cpp
TArray<FAnomalyReport> UAnomalyDetectionSubsystem::DetectOrphanItems(
const TArray<FInventoryEntry>& SnapshotEntries,
const TArray<FAuditEvent>& LedgerEvents,
const FString& OwnerID) const
{
TArray<FAnomalyReport> Reports;
TArray<FReconciliationDiscrepancy> Discrepancies =
UEventSourcingHelpers::ReconcileSnapshotVsLedger(
SnapshotEntries, LedgerEvents, OwnerID);
for (const FReconciliationDiscrepancy& Disc : Discrepancies)
{
// Positive delta = phantom items (in snapshot but not accounted for in ledger)
if (Disc.Delta > 0)
{
FAnomalyReport Report;
Report.AnomalyType = VaultTags::Anomaly_OrphanItem;
Report.Severity = 3; // High — potential duplication exploit
Report.AffectedOwnerID = OwnerID;
Report.Evidence = FString::Printf(
TEXT("Item %s: snapshot has %d but ledger accounts for %d (delta: +%d)"),
*Disc.ItemDefTag, Disc.SnapshotQuantity, Disc.LedgerQuantity, Disc.Delta);
Report.DetectedAtUTC = FDateTime::UtcNow();
Reports.Add(Report);
DetectWealthVelocity method · cpp · L203-L246 (44 LOC)Source/VaultCore/Private/Audit/AnomalyDetectionSubsystem.cpp
TArray<FAnomalyReport> UAnomalyDetectionSubsystem::DetectWealthVelocity(
const TArray<FAuditEvent>& Events) const
{
TArray<FAnomalyReport> Reports;
const UVaultSettings* Settings = UVaultSettings::Get();
const int32 ThresholdPerMinute = Settings ? Settings->WealthVelocityThresholdPerMinute : 1000;
// Track currency gains per player in the last 60 seconds
const FString RecentCutoff = (FDateTime::UtcNow() - FTimespan::FromSeconds(60.0)).ToIso8601();
TMap<FString, int32> PlayerGains;
for (const FAuditEvent& Event : Events)
{
if (Event.TimestampUTC < RecentCutoff) { continue; }
// Only track currency-related positive deltas
// Currency events use DefID like "Currency.Soft.Gold"
if (Event.QtyDelta > 0 && Event.DefID.Contains(TEXT("Currency")))
{
int32& Gains = PlayerGains.FindOrAdd(Event.SourceActorID);
Gains += Event.QtyDelta;
}
}
for (const auto& Pair : PlayerGains)
{
if (Pair.Value > ThresholdPerMinute)
{
FAnomalyReport Report;
Report.AnomalyCreate method · cpp · L18-L37 (20 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
FAuditEvent FAuditEvent::Create(
FGameplayTag ActionTag,
const FString& SourceActorID,
const FString& InstanceUUID,
const FString& DefID,
int32 QtyDelta,
const FString& Context)
{
FAuditEvent Event;
Event.TimestampUTC = FDateTime::UtcNow().ToIso8601();
Event.TransactionID = FGuid::NewGuid().ToString();
Event.ActionTag = ActionTag;
Event.SourceActorID = SourceActorID;
Event.InstanceUUID = InstanceUUID;
Event.DefID = DefID;
Event.QtyDelta = QtyDelta;
Event.Context = Context;
return Event;
}ShouldCreateSubsystem method · cpp · L40-L47 (8 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
bool UAuditLogSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
// Server-only: do not create on clients
const UWorld* World = Cast<UWorld>(Outer);
if (!World) return false;
return World->GetNetMode() != NM_Client;
}Initialize method · cpp · L48-L65 (18 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Periodic flush every 5 seconds
if (GetWorld())
{
GetWorld()->GetTimerManager().SetTimer(
FlushTimerHandle,
this,
&UAuditLogSubsystem::FlushLogBuffer,
5.0f,
true);
}
UE_LOG(LogVaultAudit, Display, TEXT("AuditLogSubsystem initialized (server-only)."));
}Deinitialize method · cpp · L66-L81 (16 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::Deinitialize()
{
// Final flush on shutdown
FlushLogBuffer();
if (GetWorld())
{
GetWorld()->GetTimerManager().ClearTimer(FlushTimerHandle);
}
UE_LOG(LogVaultAudit, Display, TEXT("AuditLogSubsystem shut down. Total events: %d"),
TotalEventsDispatched);
Super::Deinitialize();
}DispatchAuditEvent method · cpp · L82-L98 (17 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::DispatchAuditEvent(const FAuditEvent& Event)
{
EventBuffer.Add(Event);
SessionEvents.Add(Event);
TotalEventsDispatched++;
OnAuditEventDispatched.Broadcast(Event);
UE_LOG(LogVaultAudit, Verbose,
TEXT("Audit: Action=%s, Source=%s, InstanceID=%s, Def=%s, Qty=%+d, Context=%s"),
*Event.ActionTag.ToString(),
*Event.SourceActorID,
*Event.InstanceUUID,
*Event.DefID,
Event.QtyDelta,
*Event.Context);
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
FlushLogBuffer method · cpp · L99-L128 (30 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::FlushLogBuffer()
{
if (EventBuffer.Num() == 0 && RetryBuffer.Num() == 0) return;
// Take ownership of the buffer — swap to avoid blocking new events
TArray<FAuditEvent> EventsToFlush;
Swap(EventsToFlush, EventBuffer);
// Prepend any events retained from previous failed dispatch
if (RetryBuffer.Num() > 0)
{
EventsToFlush.Insert(RetryBuffer, 0);
RetryBuffer.Reset();
}
const UVaultSettings* Settings = UVaultSettings::Get();
const bool bIsCloudMode = Settings &&
(Settings->StorageProvider == EVaultStorageProvider::Supabase ||
Settings->StorageProvider == EVaultStorageProvider::AWS);
if (bIsCloudMode)
{
FlushToCloud(EventsToFlush);
}
else
{
FlushToLocalFile(EventsToFlush);
}
}GetAuditTrailForItem method · cpp · L129-L141 (13 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
TArray<FAuditEvent> UAuditLogSubsystem::GetAuditTrailForItem(const FString& InstanceUUID) const
{
TArray<FAuditEvent> Result;
for (const FAuditEvent& Event : SessionEvents)
{
if (Event.InstanceUUID == InstanceUUID)
{
Result.Add(Event);
}
}
return Result;
}FlushToLocalFile method · cpp · L142-L183 (42 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::FlushToLocalFile(const TArray<FAuditEvent>& Events)
{
// Write to daily rolling log file on a background thread
// File: <ProjectSavedDir>/Vault/VaultAudit_YYYYMMDD.log
const FString DateStr = FDateTime::UtcNow().ToString(TEXT("%Y%m%d"));
const FString LogDir = FPaths::ProjectSavedDir() / TEXT("Vault");
const FString AuditLogPath = LogDir / FString::Printf(TEXT("VaultAudit_%s.log"), *DateStr);
// Build the log content as a string
FString LogContent;
LogContent.Reserve(Events.Num() * 256);
for (const FAuditEvent& Event : Events)
{
LogContent += FString::Printf(
TEXT("[%s] TXN=%s CorrID=%s Action=%s Source=%s Target=%s InstanceID=%s Def=%s Qty=%+d Context=%s\n"),
*Event.TimestampUTC,
*Event.TransactionID,
*Event.CorrelationID,
*Event.ActionTag.ToString(),
*Event.SourceActorID,
*Event.TargetActorID,
*Event.InstanceUUID,
*Event.DefID,
Event.QtyDelta,
*Event.Context);
}
// Async file write — never block the game thrFlushToCloud method · cpp · L184-L200 (17 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::FlushToCloud(const TArray<FAuditEvent>& Events)
{
// Cloud dispatch is handled by VaultCoreServer module via delegate (SEC-1).
// If the delegate is bound, the server module handles Supabase/Kinesis dispatch.
// If unbound (client build or server module not loaded), fall back to local file.
if (CloudDispatchDelegate.IsBound())
{
CloudDispatchDelegate.Execute(Events);
}
else
{
UE_LOG(LogVaultAudit, Verbose,
TEXT("Cloud dispatch delegate not bound — falling back to local file."));
FlushToLocalFile(Events);
}
}EventToJson method · cpp · L201-L216 (16 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
TSharedRef<FJsonObject> UAuditLogSubsystem::EventToJson(const FAuditEvent& Event) const
{
TSharedRef<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("timestamp_utc"), Event.TimestampUTC);
Obj->SetStringField(TEXT("transaction_id"), Event.TransactionID);
Obj->SetStringField(TEXT("action_tag"), Event.ActionTag.ToString());
Obj->SetStringField(TEXT("source_actor_id"), Event.SourceActorID);
Obj->SetStringField(TEXT("target_actor_id"), Event.TargetActorID);
Obj->SetStringField(TEXT("instance_uuid"), Event.InstanceUUID);
Obj->SetStringField(TEXT("def_id"), Event.DefID);
Obj->SetNumberField(TEXT("qty_delta"), Event.QtyDelta);
Obj->SetStringField(TEXT("context"), Event.Context);
Obj->SetStringField(TEXT("correlation_id"), Event.CorrelationID);
return Obj;
}DispatchToSupabase method · cpp · L221-L228 (8 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::DispatchToSupabase(const TArray<FAuditEvent>& Events)
{
// Redirected to CloudDispatchDelegate — see FlushToCloud()
UE_LOG(LogVaultAudit, Warning,
TEXT("DispatchToSupabase called directly — use CloudDispatchDelegate instead."));
FlushToLocalFile(Events);
}DispatchToKinesis method · cpp · L229-L236 (8 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
void UAuditLogSubsystem::DispatchToKinesis(const TArray<FAuditEvent>& Events)
{
// Redirected to CloudDispatchDelegate — see FlushToCloud()
UE_LOG(LogVaultAudit, Warning,
TEXT("DispatchToKinesis called directly — use CloudDispatchDelegate instead."));
FlushToLocalFile(Events);
}QueryAuditEvents method · cpp · L239-L273 (35 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
TArray<FAuditEvent> UAuditLogSubsystem::QueryAuditEvents(
const FString& ActorID,
FGameplayTag ActionTag,
const FString& CorrelationID,
int32 MaxResults) const
{
TArray<FAuditEvent> Results;
const bool bFilterActor = !ActorID.IsEmpty();
const bool bFilterAction = ActionTag.IsValid();
const bool bFilterCorrelation = !CorrelationID.IsEmpty();
// Search session events in reverse order (most recent first)
for (int32 i = SessionEvents.Num() - 1; i >= 0 && Results.Num() < MaxResults; --i)
{
const FAuditEvent& Event = SessionEvents[i];
if (bFilterActor && Event.SourceActorID != ActorID && Event.TargetActorID != ActorID)
{
continue;
}
if (bFilterAction && Event.ActionTag != ActionTag)
{
continue;
}
if (bFilterCorrelation && Event.CorrelationID != CorrelationID)
{
continue;
}
Results.Add(Event);
}
return Results;
}Repobility · code-quality intelligence · https://repobility.com
QueryLedgerHistory method · cpp · L274-L310 (37 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
TArray<FAuditEvent> UAuditLogSubsystem::QueryLedgerHistory(
const FString& OwnerID,
const FString& StartTime,
const FString& EndTime,
int32 MaxResults) const
{
TArray<FAuditEvent> Results;
const bool bFilterOwner = !OwnerID.IsEmpty();
const bool bFilterStart = !StartTime.IsEmpty();
const bool bFilterEnd = !EndTime.IsEmpty();
for (int32 i = SessionEvents.Num() - 1; i >= 0 && Results.Num() < MaxResults; --i)
{
const FAuditEvent& Event = SessionEvents[i];
// Filter by owner (SourceActorID)
if (bFilterOwner && Event.SourceActorID != OwnerID)
{
continue;
}
// Filter by time range (lexicographic ISO-8601 comparison)
if (bFilterStart && Event.TimestampUTC < StartTime)
{
continue;
}
if (bFilterEnd && Event.TimestampUTC > EndTime)
{
continue;
}
Results.Add(Event);
}
return Results;
}ExportAuditEvents method · cpp · L313-L331 (19 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
TArray<FAuditEvent> UAuditLogSubsystem::ExportAuditEvents(const FString& OwnerID) const
{
TArray<FAuditEvent> Results;
Results.Reserve(SessionEvents.Num());
for (const FAuditEvent& Event : SessionEvents)
{
if (OwnerID.IsEmpty() || Event.SourceActorID == OwnerID || Event.TargetActorID == OwnerID)
{
Results.Add(Event);
}
}
UE_LOG(LogVaultAudit, Display, TEXT("ExportAuditEvents: %d events for OwnerID=%s"),
Results.Num(), *OwnerID);
return Results;
}ExportProfile method · cpp · L332-L361 (30 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
FString UAuditLogSubsystem::ExportProfile(const FString& ProfileID) const
{
// Build a JSON document containing all data for this profile
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetStringField(TEXT("profile_id"), ProfileID);
Root->SetStringField(TEXT("export_timestamp"), FDateTime::UtcNow().ToIso8601());
// Export audit trail
TArray<TSharedPtr<FJsonValue>> EventArray;
for (const FAuditEvent& Event : SessionEvents)
{
if (Event.SourceActorID == ProfileID || Event.TargetActorID == ProfileID)
{
EventArray.Add(MakeShared<FJsonValueObject>(EventToJson(Event)));
}
}
Root->SetArrayField(TEXT("audit_trail"), EventArray);
Root->SetNumberField(TEXT("total_audit_events"), EventArray.Num());
// Serialize to JSON string
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(Root, Writer);
UE_LOG(LogVaultAudit, Display, TEXT("ExportProfile: ProfileID=%s, %d audit events"),
DeleteProfile method · cpp · L362-L406 (45 LOC)Source/VaultCore/Private/Audit/AuditLogSubsystem.cpp
bool UAuditLogSubsystem::DeleteProfile(const FString& ProfileID)
{
if (ProfileID.IsEmpty())
{
UE_LOG(LogVaultAudit, Warning, TEXT("DeleteProfile: Empty ProfileID — aborted."));
return false;
}
// Remove session events for this profile
int32 RemovedCount = 0;
for (int32 i = SessionEvents.Num() - 1; i >= 0; --i)
{
if (SessionEvents[i].SourceActorID == ProfileID || SessionEvents[i].TargetActorID == ProfileID)
{
SessionEvents.RemoveAt(i);
++RemovedCount;
}
}
// Also remove from event buffer
for (int32 i = EventBuffer.Num() - 1; i >= 0; --i)
{
if (EventBuffer[i].SourceActorID == ProfileID || EventBuffer[i].TargetActorID == ProfileID)
{
EventBuffer.RemoveAt(i);
}
}
// Dispatch a deletion audit event (GDPR compliance trail)
FAuditEvent DeletionEvent = FAuditEvent::Create(
VaultTags::Reason_AdminGrant,
TEXT("System_GDPR"),
TEXT(""),
TEXT(""),
0,
FString::Printf(TEXT("Profile data deleted: ProfileID=%s, %d events removed"),
*ProfileID, RReconstructStateFromLedger method · cpp · L6-L40 (35 LOC)Source/VaultCore/Private/Audit/EventSourcingHelpers.cpp
TMap<FString, int32> UEventSourcingHelpers::ReconstructStateFromLedger(
const TArray<FAuditEvent>& LedgerEvents,
const FString& OwnerID,
const FString& UpToTimestamp)
{
TMap<FString, int32> ItemCounts;
for (const FAuditEvent& Event : LedgerEvents)
{
// Filter by owner
if (!OwnerID.IsEmpty() && Event.SourceActorID != OwnerID)
{
continue;
}
// Filter by timestamp (lexicographic comparison works for ISO-8601)
if (!UpToTimestamp.IsEmpty() && Event.TimestampUTC > UpToTimestamp)
{
continue;
}
// Skip events without a valid item definition
if (Event.DefID.IsEmpty())
{
continue;
}
// Accumulate quantity deltas per item type
int32& Count = ItemCounts.FindOrAdd(Event.DefID);
Count += Event.QtyDelta;
}
return ItemCounts;
}ReconcileSnapshotVsLedger method · cpp · L41-L89 (49 LOC)Source/VaultCore/Private/Audit/EventSourcingHelpers.cpp
TArray<FReconciliationDiscrepancy> UEventSourcingHelpers::ReconcileSnapshotVsLedger(
const TArray<FInventoryEntry>& SnapshotEntries,
const TArray<FAuditEvent>& LedgerEvents,
const FString& OwnerID)
{
// Reconstruct state from ledger (no timestamp filter = all events)
TMap<FString, int32> LedgerState = ReconstructStateFromLedger(
LedgerEvents, OwnerID, TEXT(""));
// Build snapshot state: aggregate by item def tag
TMap<FString, int32> SnapshotState;
for (const FInventoryEntry& Entry : SnapshotEntries)
{
if (IsValid(Entry.ItemDef.Get()))
{
// Use the item def's name as the tag
FString DefTag = Entry.ItemDef.Get()->GetName();
int32& Count = SnapshotState.FindOrAdd(DefTag);
Count += Entry.StackCount;
}
}
// Compare: find discrepancies
TArray<FReconciliationDiscrepancy> Discrepancies;
// Collect all unique item def tags from both sources
TSet<FString> AllTags;
for (const auto& Pair : SnapshotState) { AllTags.Add(Pair.Key); }
for (const auto& Pair : LedComputeGlobalItemCounts method · cpp · L90-L96 (7 LOC)Source/VaultCore/Private/Audit/EventSourcingHelpers.cpp
TMap<FString, int32> UEventSourcingHelpers::ComputeGlobalItemCounts(
const TArray<FAuditEvent>& LedgerEvents)
{
// Aggregate across all owners (empty OwnerID = no filter)
return ReconstructStateFromLedger(LedgerEvents, TEXT(""), TEXT(""));
}GetLedgerPartitionKey method · cpp · L97-L107 (11 LOC)Source/VaultCore/Private/Audit/EventSourcingHelpers.cpp
FString UEventSourcingHelpers::GetLedgerPartitionKey(const FAuditEvent& Event)
{
// Monthly partitioning: extract YYYY-MM from ISO-8601 timestamp
// Event.TimestampUTC format: "2026-04-10T12:00:00Z"
if (Event.TimestampUTC.Len() >= 7)
{
return Event.TimestampUTC.Left(7); // "2026-04"
}
return TEXT("unknown");
}All rows scored by the Repobility analyzer (https://repobility.com)
ShouldCreateSubsystem method · cpp · L9-L15 (7 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
bool UGlobalReconciliationSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
// Global reconciliation is server-side only.
// UGameInstanceSubsystem — always create, but only start timer if enabled.
return true;
}Initialize method · cpp · L16-L22 (7 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
void UGlobalReconciliationSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogVaultRecon, Log, TEXT("GlobalReconciliationSubsystem initialized"));
}Deinitialize method · cpp · L23-L27 (5 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
void UGlobalReconciliationSubsystem::Deinitialize()
{
Super::Deinitialize();
}ComputeExpectedItemCounts method · cpp · L28-L33 (6 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
TMap<FString, int32> UGlobalReconciliationSubsystem::ComputeExpectedItemCounts(
const TArray<FAuditEvent>& LedgerEvents) const
{
return UEventSourcingHelpers::ComputeGlobalItemCounts(LedgerEvents);
}ComputeActualItemCounts method · cpp · L34-L49 (16 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
TMap<FString, int32> UGlobalReconciliationSubsystem::ComputeActualItemCounts(
const TArray<FInventoryEntry>& SnapshotEntries) const
{
TMap<FString, int32> Counts;
for (const FInventoryEntry& Entry : SnapshotEntries)
{
if (IsValid(Entry.ItemDef.Get()))
{
FString DefTag = Entry.ItemDef.Get()->GetName();
int32& Count = Counts.FindOrAdd(DefTag);
Count += Entry.StackCount;
}
}
return Counts;
}RunReconciliation method · cpp · L50-L98 (49 LOC)Source/VaultCore/Private/Audit/GlobalReconciliationSubsystem.cpp
FReconciliationReport UGlobalReconciliationSubsystem::RunReconciliation(
const TArray<FAuditEvent>& LedgerEvents,
const TArray<FInventoryEntry>& SnapshotEntries)
{
FReconciliationReport Report;
Report.TimestampUTC = FDateTime::UtcNow();
TMap<FString, int32> Expected = ComputeExpectedItemCounts(LedgerEvents);
TMap<FString, int32> Actual = ComputeActualItemCounts(SnapshotEntries);
// Collect all item types
TSet<FString> AllTypes;
for (const auto& Pair : Expected) { AllTypes.Add(Pair.Key); }
for (const auto& Pair : Actual) { AllTypes.Add(Pair.Key); }
Report.TotalItemTypes = AllTypes.Num();
for (const FString& Type : AllTypes)
{
const int32 ExpectedQty = Expected.Contains(Type) ? Expected[Type] : 0;
const int32 ActualQty = Actual.Contains(Type) ? Actual[Type] : 0;
Report.TotalItemCount += ActualQty;
if (ExpectedQty != ActualQty)
{
FReconciliationDiscrepancy Disc;
Disc.ItemDefTag = Type;
Disc.LedgerQuantity = ExpectedQty;
Disc.SnapshotQuantity = ActSuper function · cpp · L25-L29 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
: Super(ObjectInitializer)
{
SetIsReplicatedByDefault(true);
bWantsInitializeComponent = true;
}BeginPlay method · cpp · L30-L56 (27 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::BeginPlay()
{
Super::BeginPlay();
// Set the back-pointer so FFastArray callbacks can fire our delegates
InventoryArray.OwningComponent = this;
// Default access policy: owner-only if none assigned in editor
if (!AccessPolicy.GetObject() && GetOwnerRole() == ROLE_Authority)
{
UDefaultOwnerAccessPolicy* DefaultPolicy = NewObject<UDefaultOwnerAccessPolicy>(this);
AccessPolicy.SetObject(DefaultPolicy);
AccessPolicy.SetInterface(Cast<IInventoryAccessPolicy>(DefaultPolicy));
}
// Cache the validation subsystem — avoids repeated GetSubsystem<>() lookups in every RPC handler
if (UWorld* World_BP = GetWorld())
{
CachedValidation = World_BP->GetSubsystem<UInventoryValidationSubsystem>();
}
UE_LOG(LogVault, Display, TEXT("InventoryManagerComponent initialized on %s (Authority=%s, Mode=%d)"),
*GetOwner()->GetName(),
GetOwnerRole() == ROLE_Authority ? TEXT("true") : TEXT("false"),
static_cast<int32>(PersistenceMode));
}About: code-quality intelligence by Repobility · https://repobility.com
GetLifetimeReplicatedProps method · cpp · L57-L65 (9 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// COND_OwnerOnly: players cannot sniff other players' inventory data
DOREPLIFETIME_CONDITION(UInventoryManagerComponent, InventoryArray, COND_OwnerOnly);
// Wallet is not replicated (TMap not supported). Currency state synced via OnCurrencyChanged delegate.
}Authority_AddItem method · cpp · L68-L184 (117 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
FGuid UInventoryManagerComponent::Authority_AddItem(
UItemDefinition* ItemDef,
int32 Quantity,
FGameplayTagContainer InitialTags)
{
if (!ensureMsgf(GetOwnerRole() == ROLE_Authority,
TEXT("Authority_AddItem called on non-authority")))
{
return FGuid();
}
if (!IsValid(ItemDef) || Quantity <= 0)
{
UE_LOG(LogVault, Warning, TEXT("Authority_AddItem: Invalid ItemDef or Quantity=%d"), Quantity);
return FGuid();
}
// Check if we can stack onto an existing entry
const UItemFragment_Stackable* StackFrag = ItemDef->FindFragmentByClass<UItemFragment_Stackable>();
const int32 MaxStack = StackFrag ? StackFrag->MaxStackSize : 1;
int32 RemainingQty = Quantity;
// Try to merge into existing stacks first
if (MaxStack > 1)
{
for (FInventoryEntry& Entry : InventoryArray.Entries)
{
if (RemainingQty <= 0) break;
if (Entry.ItemDef != ItemDef) continue;
if (Entry.StackCount >= MaxStack) continue;
const int32 Space = MaxStack - Entry.StackCount;
const int32 ToAddAuthority_RemoveItem method · cpp · L185-L240 (56 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Authority_RemoveItem(
FGuid InstanceID,
int32 Quantity,
FGameplayTag ReasonContext)
{
if (!ensureMsgf(GetOwnerRole() == ROLE_Authority,
TEXT("Authority_RemoveItem called on non-authority")))
{
return false;
}
const int32 Index = InventoryArray.FindIndexByInstanceID(InstanceID);
if (Index == INDEX_NONE)
{
UE_LOG(LogVault, Warning, TEXT("Authority_RemoveItem: InstanceID=%s not found"),
*InstanceID.ToString());
return false;
}
FInventoryEntry& Entry = InventoryArray.Entries[Index];
// Clamp to actual stack count — never trust external quantity
const int32 SafeQty = FMath::Min(Quantity, Entry.StackCount);
// Audit: log item removal before mutation
if (UAuditLogSubsystem* AuditLog = GetWorld() ? GetWorld()->GetSubsystem<UAuditLogSubsystem>() : nullptr)
{
FAuditEvent Event = FAuditEvent::Create(
ReasonContext, ProfileID,
InstanceID.ToString(),
IsValid(Entry.ItemDef.Get()) ? Entry.ItemDef->GetName() : TEXT("Unknown"),Authority_AddCurrency method · cpp · L241-L265 (25 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Authority_AddCurrency(FGameplayTag CurrencyType, int32 Amount)
{
if (!ensureMsgf(GetOwnerRole() == ROLE_Authority,
TEXT("Authority_AddCurrency called on non-authority")))
{
return;
}
Wallet.AddCurrency(CurrencyType, Amount);
OnCurrencyChanged.Broadcast(CurrencyType, Wallet.GetBalance(CurrencyType));
TriggerAsyncSave();
if (UAuditLogSubsystem* AuditLog = GetWorld() ? GetWorld()->GetSubsystem<UAuditLogSubsystem>() : nullptr)
{
FAuditEvent Event = FAuditEvent::Create(
VaultTags::Reason_Spawn, ProfileID,
TEXT(""), CurrencyType.ToString(), Amount,
FString::Printf(TEXT("AddCurrency %s on %s"), *CurrencyType.ToString(), *GetOwner()->GetName()));
AuditLog->DispatchAuditEvent(Event);
}
UE_LOG(LogVault, Log, TEXT("Authority_AddCurrency: %s += %d (New Balance: %d)"),
*CurrencyType.ToString(), Amount, Wallet.GetBalance(CurrencyType));
}Authority_DeductCurrency method · cpp · L266-L298 (33 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Authority_DeductCurrency(FGameplayTag CurrencyType, int32 Amount)
{
if (!ensureMsgf(GetOwnerRole() == ROLE_Authority,
TEXT("Authority_DeductCurrency called on non-authority")))
{
return false;
}
if (!Wallet.DeductCurrency(CurrencyType, Amount))
{
UE_LOG(LogVault, Warning, TEXT("Authority_DeductCurrency: Insufficient %s (Have=%d, Need=%d)"),
*CurrencyType.ToString(), Wallet.GetBalance(CurrencyType), Amount);
return false;
}
OnCurrencyChanged.Broadcast(CurrencyType, Wallet.GetBalance(CurrencyType));
TriggerAsyncSave();
if (UAuditLogSubsystem* AuditLog = GetWorld() ? GetWorld()->GetSubsystem<UAuditLogSubsystem>() : nullptr)
{
FAuditEvent Event = FAuditEvent::Create(
VaultTags::Reason_Consume, ProfileID,
TEXT(""), CurrencyType.ToString(), -Amount,
FString::Printf(TEXT("DeductCurrency %s on %s"), *CurrencyType.ToString(), *GetOwner()->GetName()));
AuditLog->DispatchAuditEvent(Event);
}
UE_LOG(LogVault, Log, TEXT("AuthAuthority_BulkRemoveItems method · cpp · L301-L398 (98 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
TArray<FGameplayTag> UInventoryManagerComponent::Authority_BulkRemoveItems(
const TArray<FGuid>& InstanceIDs,
FGameplayTag ReasonContext)
{
TArray<FGameplayTag> Results;
Results.SetNum(InstanceIDs.Num());
if (!ensureMsgf(GetOwnerRole() == ROLE_Authority,
TEXT("Authority_BulkRemoveItems called on non-authority")))
{
for (FGameplayTag& Tag : Results)
{
Tag = VaultTags::Validation_Error_ServerNotReady;
}
return Results;
}
// Phase 1: Identify valid items and record expected removal count
struct FPendingRemoval
{
FGuid InstanceID;
int32 StackCount;
};
TArray<FPendingRemoval> Pending;
Pending.Reserve(InstanceIDs.Num());
int32 ExpectedTotalRemoved = 0;
for (int32 i = 0; i < InstanceIDs.Num(); ++i)
{
const FInventoryEntry* Entry = InventoryArray.FindByInstanceID(InstanceIDs[i]);
if (!Entry)
{
Results[i] = VaultTags::Validation_Error_ItemNotFound;
UE_LOG(LogVault, Warning, TEXT("Authority_BulkRemoveItems: InstanceID=%s not found"),
*InstanceServer_RequestMoveItem_Validate method · cpp · L403-L407 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestMoveItem_Validate(int32 SourceIndex, int32 TargetIndex)
{
return ValidateRPCPreconditions();
}Server_RequestMoveItem_Implementation method · cpp · L408-L435 (28 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestMoveItem_Implementation(int32 SourceIndex, int32 TargetIndex)
{
// Route through validation subsystem — ensures index bounds, ownership, locking, expiration
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (!Validation)
{
UE_LOG(LogVaultValidation, Warning, TEXT("MoveItem: Validation subsystem unavailable"));
return;
}
// Derive the requesting player from the pawn that owns this component
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = OwnerPawn ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
// Both source and target are within the same inventory for a move/swap
const FGameplayTag Result = Validation->ValidateMoveIntent(PC, this, this, SourceIndex, TargetIndex);
if (Result != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("MoveItem: Validation failed — %s (Source=%d, Target=%d)"),
*Result.ToString(), SourceIndex, TargetIndex);
rRepobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
Server_RequestDropItem_Validate method · cpp · L436-L440 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestDropItem_Validate(FGuid InstanceID, int32 DropQuantity)
{
return ValidateRPCPreconditions() && InstanceID.IsValid() && DropQuantity > 0;
}Server_RequestDropItem_Implementation method · cpp · L441-L500 (60 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestDropItem_Implementation(FGuid InstanceID, int32 DropQuantity)
{
// Validate existence, ownership, binding, locking, and expiration before allowing drop
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (!Validation)
{
UE_LOG(LogVaultValidation, Warning, TEXT("DropItem: Validation subsystem unavailable"));
return;
}
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = OwnerPawn ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
const FGameplayTag Result = Validation->ValidateItemOperation(PC, this, InstanceID);
if (Result != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("DropItem: Validation failed — %s (InstanceID=%s)"),
*Result.ToString(), *InstanceID.ToString());
return;
}
// Capture item data before removal (Authority_RemoveItem may invalidate the entry)
const FInventoryEntry* DropEntry = FindEntryByID(InstanceID);
UItemDefinition*Server_RequestConsumeItem_Validate method · cpp · L501-L505 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestConsumeItem_Validate(int32 ItemIndex)
{
return ValidateRPCPreconditions();
}Server_RequestConsumeItem_Implementation method · cpp · L506-L552 (47 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestConsumeItem_Implementation(int32 ItemIndex)
{
// Validate index bounds, ownership, player state, locking, and expiration
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (!Validation)
{
UE_LOG(LogVaultValidation, Warning, TEXT("ConsumeItem: Validation subsystem unavailable"));
return;
}
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = OwnerPawn ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
// Check index bounds first since ConsumeItem uses an index, not an InstanceID
FGameplayTag Result = Validation->CheckIndexBounds(this, ItemIndex);
if (Result != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("ConsumeItem: Validation failed — %s (Index=%d)"),
*Result.ToString(), ItemIndex);
return;
}
// Now validate the specific item via InstanceID for full ownership/binding/expiration checks
const FGuid& InstanceID = InventoryArray.EntriesServer_RequestEquipItem_Validate method · cpp · L553-L557 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestEquipItem_Validate(FGuid InstanceID)
{
return ValidateRPCPreconditions() && InstanceID.IsValid();
}Server_RequestEquipItem_Implementation method · cpp · L558-L602 (45 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestEquipItem_Implementation(FGuid InstanceID)
{
// Validate existence, ownership, player state, locking, and expiration before equipping
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (!Validation)
{
UE_LOG(LogVaultValidation, Warning, TEXT("EquipItem: Validation subsystem unavailable"));
return;
}
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = OwnerPawn ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
const FGameplayTag Result = Validation->ValidateItemOperation(PC, this, InstanceID);
if (Result != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("EquipItem: Validation failed — %s (InstanceID=%s)"),
*Result.ToString(), *InstanceID.ToString());
return;
}
FInventoryEntry* Entry = InventoryArray.FindByInstanceID(InstanceID);
if (!Entry) return;
// Trigger Bind-on-Equip — once bound, item cannot be traded/dropped/mailed
if (Entry-Server_RequestUnequipItem_Validate method · cpp · L603-L607 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestUnequipItem_Validate(FGuid InstanceID)
{
return ValidateRPCPreconditions() && InstanceID.IsValid();
}Server_RequestUnequipItem_Implementation method · cpp · L608-L636 (29 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestUnequipItem_Implementation(FGuid InstanceID)
{
// Validate existence, ownership, player state, and locking before unequipping
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (!Validation)
{
UE_LOG(LogVaultValidation, Warning, TEXT("UnequipItem: Validation subsystem unavailable"));
return;
}
APawn* OwnerPawn = Cast<APawn>(GetOwner());
APlayerController* PC = OwnerPawn ? Cast<APlayerController>(OwnerPawn->GetController()) : nullptr;
const FGameplayTag Result = Validation->ValidateItemOperation(PC, this, InstanceID);
if (Result != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("UnequipItem: Validation failed — %s (InstanceID=%s)"),
*Result.ToString(), *InstanceID.ToString());
return;
}
FInventoryEntry* Entry = InventoryArray.FindByInstanceID(InstanceID);
if (!Entry) return;
Entry->DynamicStateTags.RemoveTag(VaultTags::State_Equipped);
InventoryArray.MarkItemDirtyRepobility · code-quality intelligence · https://repobility.com
Server_RequestPickupItem_Validate method · cpp · L639-L643 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestPickupItem_Validate(AActor* WorldItem)
{
return ValidateRPCPreconditions() && IsValid(WorldItem);
}Server_RequestPickupItem_Implementation method · cpp · L644-L688 (45 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
void UInventoryManagerComponent::Server_RequestPickupItem_Implementation(AActor* WorldItem)
{
AVaultWorldItemActor* VaultItem = Cast<AVaultWorldItemActor>(WorldItem);
if (!VaultItem || !IsValid(VaultItem->ItemDef.Get()))
{
UE_LOG(LogVault, Warning, TEXT("PickupItem: Invalid world item or missing ItemDef"));
return;
}
UInventoryValidationSubsystem* Validation = CachedValidation.Get();
if (Validation)
{
// Distance check: player must be close enough
FGameplayTag DistResult = Validation->CheckDistance(GetOwner(), VaultItem);
if (DistResult != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("PickupItem: Too far from world item"));
return;
}
// Capacity check: inventory must have room
FGameplayTag CapResult = Validation->CheckSlotCapacity(this);
if (CapResult != VaultTags::Validation_Success)
{
UE_LOG(LogVaultValidation, Warning, TEXT("PickupItem: Inventory full"));
return;
}
// Access policy check: actor must have dServer_RequestOpenContainer_Validate method · cpp · L691-L695 (5 LOC)Source/VaultCore/Private/Component/InventoryManagerComponent.cpp
bool UInventoryManagerComponent::Server_RequestOpenContainer_Validate(AActor* Container)
{
return ValidateRPCPreconditions() && IsValid(Container);
}