// Copyright 2025 https://yuewu.dev/en All Rights Reserved. #include "GIS_EquipmentSystemComponent.h" #include "Engine/World.h" #include "GameFramework/Controller.h" #include "GIS_EquipmentInstance.h" #include "GIS_InventorySystemComponent.h" #include "GIS_ItemCollection.h" #include "GIS_ItemDefinition.h" #include "GIS_ItemFragment_Equippable.h" #include "GIS_ItemInstance.h" #include "GIS_ItemSlotCollection.h" #include "GIS_LogChannels.h" #include "Engine/ActorChannel.h" #include "Net/UnrealNetwork.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentSystemComponent) UGIS_EquipmentSystemComponent::UGIS_EquipmentSystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), Container(this) { PrimaryComponentTick.bCanEverTick = true; SetIsReplicatedByDefault(true); bReplicateUsingRegisteredSubObjectList = true; bWantsInitializeComponent = true; } void UGIS_EquipmentSystemComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ThisClass, Container); DOREPLIFETIME(ThisClass, bEquipmentSystemInitialized); DOREPLIFETIME(ThisClass, TargetCollectionDefinition); } void UGIS_EquipmentSystemComponent::EquipItemToSlot(UGIS_ItemInstance* Item, const FGameplayTag& SlotTag) { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } if (!SlotTag.IsValid()) { return; } UObject* EquipmentInstance = CreateEquipmentInstance(GetOwner(), Item); if (!IsValid(EquipmentInstance)) { return; } FGIS_EquipmentEntry NewEntry; NewEntry.EquippedSlot = SlotTag; NewEntry.Instance = EquipmentInstance; NewEntry.ItemInstance = Item; NewEntry.bActive = false; NewEntry.bPrevActive = false; AddEquipmentEntry(NewEntry); if (auto Equippable = Item->FindFragmentByClass()) { if (Equippable->bAutoActivate) { SetEquipmentActiveState(SlotTag, true); } } } void UGIS_EquipmentSystemComponent::UnequipBySlot(FGameplayTag SlotTag) { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } int32 Idx = Container.IndexOfBySlot(SlotTag); if (Idx != INDEX_NONE) { RemoveEquipmentEntry(Idx); } } void UGIS_EquipmentSystemComponent::UnequipByItem(const FGuid& ItemId) { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } int32 Idx = Container.IndexOfByItemId(ItemId); if (Idx != INDEX_NONE) { RemoveEquipmentEntry(Idx); } } bool UGIS_EquipmentSystemComponent::OwnerHasAuthority() const { AActor* Owner = GetOwner(); return IsValid(Owner) && Owner->HasAuthority(); } TArray UGIS_EquipmentSystemComponent::GetEquipments(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const { TArray Results; if (SlotQuery.IsEmpty()) { return Results; } if (UClass* RealClass = InstanceType) { const TArray& MatchedEntries = Container.Entries.FilterByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) { return Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); }); for (const FGIS_EquipmentEntry& Entry : MatchedEntries) { Results.AddUnique(Entry.Instance); } } return Results; } TArray UGIS_EquipmentSystemComponent::GetActiveEquipments(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const { TArray Results; if (SlotQuery.IsEmpty()) { return Results; } if (UClass* RealClass = InstanceType) { const TArray& MatchedEntries = Container.Entries.FilterByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) { return Entry.bActive && Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); }); for (const FGIS_EquipmentEntry& Entry : MatchedEntries) { Results.AddUnique(Entry.Instance); } } return Results; } UObject* UGIS_EquipmentSystemComponent::GetEquipment(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const { if (UClass* RealClass = InstanceType) { const FGIS_EquipmentEntry* Found = Container.Entries.FindByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) { return Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); }); if (Found) { return Found->Instance; } } return nullptr; } UObject* UGIS_EquipmentSystemComponent::GetActiveEquipment(TSubclassOf InstanceType, FGameplayTagQuery SlotQuery) const { if (UClass* RealClass = InstanceType) { const FGIS_EquipmentEntry* Found = Container.Entries.FindByPredicate([&SlotQuery, &RealClass](const FGIS_EquipmentEntry& Entry) { return Entry.bActive && Entry.Instance->IsA(RealClass) && SlotQuery.Matches(Entry.EquippedSlot.GetSingleTagContainer()); }); if (Found) { return Found->Instance; } } return nullptr; } UObject* UGIS_EquipmentSystemComponent::GetActiveEquipmentInGroup(FGameplayTag GroupTag, bool bExactMatch) const { if (!GroupTag.IsValid()) { return nullptr; } int32 Idx = Container.Entries.IndexOfByPredicate([GroupTag,bExactMatch](const FGIS_EquipmentEntry& Entry) { return Entry.bActive && Entry.EquippedGroup.IsValid() && bExactMatch ? Entry.EquippedGroup.MatchesTagExact(GroupTag) : Entry.EquippedGroup.MatchesTag(GroupTag); }); if (Idx != INDEX_NONE) { return Container.Entries[Idx].Instance; } return nullptr; } UGIS_EquipmentInstance* UGIS_EquipmentSystemComponent::GetEquipmentInstanceOfActor(AActor* EquipmentActor) const { if (IsValid(EquipmentActor)) { for (const FGIS_EquipmentEntry& Entry : Container.Entries) { if (UGIS_EquipmentInstance* Instance = Cast(Entry.Instance)) { if (Instance->GetEquipmentActors().Contains(EquipmentActor)) { return Instance; } } } } return nullptr; } UGIS_EquipmentInstance* UGIS_EquipmentSystemComponent::GetTypedEquipmentInstanceOfActor(TSubclassOf InstanceType, AActor* EquipmentActor) const { if (UClass* RealClass = InstanceType) { if (UGIS_EquipmentInstance* Instance = GetEquipmentInstanceOfActor(EquipmentActor)) { if (Instance->GetClass()->IsChildOf(RealClass)) { return Instance; } } } return nullptr; } bool UGIS_EquipmentSystemComponent::IsSlotEquipped(FGameplayTag SlotTag) const { return SlotToInstanceMap.Contains(SlotTag); } FGameplayTag UGIS_EquipmentSystemComponent::GetSlotByEquipment(UObject* Equipment) const { if (!IsValid(Equipment)) { return FGameplayTag::EmptyTag; } int32 Idx = Container.IndexOfByInstance(Equipment); if (Idx != INDEX_NONE) { return Container.Entries[Idx].EquippedSlot; } return FGameplayTag::EmptyTag; } FGameplayTag UGIS_EquipmentSystemComponent::GetSlotByItem(const UGIS_ItemInstance* Item) const { if (!IsValid(Item)) { return FGameplayTag::EmptyTag; } int32 Idx = Container.IndexOfByItem(Item); if (Idx != INDEX_NONE) { return Container.Entries[Idx].EquippedSlot; } return FGameplayTag::EmptyTag; } int32 UGIS_EquipmentSystemComponent::SlotTagToEquipmentInex(FGameplayTag InSlotTag) const { return Container.IndexOfBySlot(InSlotTag); } int32 UGIS_EquipmentSystemComponent::ItemIdToEquipmentInex(FGuid InItemId) const { return Container.IndexOfByItemId(InItemId); } UObject* UGIS_EquipmentSystemComponent::GetEquipmentInSlot(FGameplayTag SlotTag) const { int32 Idx = Container.IndexOfBySlot(SlotTag); if (Idx != INDEX_NONE) { return Container.Entries[Idx].Instance; } return nullptr; } UObject* UGIS_EquipmentSystemComponent::GetEquipmentByItem(const UGIS_ItemInstance* Item) { if (Item == nullptr) { return nullptr; } int32 Idx = Container.IndexOfByItem(Item); if (Idx != INDEX_NONE) { return Container.Entries[Idx].Instance; } return nullptr; } void UGIS_EquipmentSystemComponent::SetEquipmentActiveState(FGameplayTag SlotTag, bool NewActiveState) { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } int32 Idx = Container.IndexOfBySlot(SlotTag); if (Idx == INDEX_NONE) { GIS_CLOG(Warning, "invalid equipment entry at slot:%s.", *SlotTag.ToString()) return; } const FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; // same state, return early. if (Entry.bActive == NewActiveState) { GIS_CLOG(Warning, "trying to set same active state to equipment entry at slot:%s.", *SlotTag.ToString()) return; } // check if the item is groupped. FGameplayTag MatchingGroup = FindMatchingGroupForSlot(Entry.EquippedSlot); // has group if (MatchingGroup.IsValid()) { //check occupied if trying to active. if (NewActiveState) { int32 OccupiedIdx = Container.IndexOfByGroup(MatchingGroup); // Already occupied and is not this one. if (OccupiedIdx != INDEX_NONE && OccupiedIdx != Idx) { GIS_CLOG(Warning, "can't active equipment at idx(%d) due to target slot(%s) within group(%s) was occupied.", Idx, *SlotTag.ToString(), *MatchingGroup.ToString()) return; } } UpdateEquipmentState(Idx, NewActiveState, MatchingGroup); } else { UpdateEquipmentState(Idx, NewActiveState, FGameplayTag::EmptyTag); } } void UGIS_EquipmentSystemComponent::ServerSetEquipmentActiveState_Implementation(FGameplayTag SlotTag, bool NewActiveState) { SetEquipmentActiveState(SlotTag, NewActiveState); } void UGIS_EquipmentSystemComponent::UpdateEquipmentState(int32 Idx, bool NewActiveState, FGameplayTag NewGroup) { check(Container.Entries.IsValidIndex(Idx)) if (Container.Entries[Idx].bActive == NewActiveState) { // Same state, return. return; } FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; Entry.bActive = NewActiveState; Entry.PrevEquippedGroup = Entry.EquippedGroup; Entry.EquippedGroup = NewActiveState ? NewGroup : FGameplayTag::EmptyTag; OnEquipmentEntryChanged(Entry, Idx); Container.MarkItemDirty(Entry); } void UGIS_EquipmentSystemComponent::OnTargetCollectionChanged(const FGIS_InventoryStackUpdateMessage& Message) { // only handle equip/unequip on the server side. if (!OwnerHasAuthority()) { return; } // Make use this message comes from the inventory&&collection I care about. bool bIsMyConcern = IsValid(Message.Inventory) && Message.Inventory == Inventory && Message.CollectionId == TargetCollection->GetCollectionId(); if (!bIsMyConcern) { return; } switch (Message.ChangeType) { case EGIS_ItemStackChangeType::WasAdded: { FGameplayTag SlotTag = TargetCollection->GetItemSlotName(Message.Instance); if (!SlotTag.IsValid()) { return; } EquipItemToSlot(Message.Instance, SlotTag); return; } case EGIS_ItemStackChangeType::WasRemoved: { UnequipByItem(Message.Instance->GetItemId()); } default: break; } } void UGIS_EquipmentSystemComponent::ProcessPendingEquipments() { if (!bEquipmentSystemInitialized || !HasBegunPlay() || GetOwner() == nullptr) { return; } TArray AddedEquipments; for (auto& Pair : PendingEquipmentEntries) { if (Pair.Value.CheckClientDataReady()) { AddedEquipments.Add(Pair.Key); } } for (int32 i = 0; i < AddedEquipments.Num(); i++) { int32 idx = AddedEquipments[i]; if (PendingEquipmentEntries.Contains(idx)) { const FGIS_EquipmentEntry& Entry = PendingEquipmentEntries[idx]; OnEquipmentEntryAdded(Entry, idx); PendingEquipmentEntries.Remove(idx); } } } void UGIS_EquipmentSystemComponent::OnEquipmentSystemInitialized_Implementation() { OnEquipmentSystemInitializedEvent.Broadcast(); TArray Delegates = InitializedDelegates; for (FGIS_EquipmentSystem_Initialized_DynamicEvent Delegate : Delegates) { Delegate.ExecuteIfBound(); } InitializedDelegates.Empty(); } void UGIS_EquipmentSystemComponent::OnTargetCollectionRemoved(UGIS_ItemCollection* Collection) { if (Collection && TargetCollection && TargetCollection == Collection) { ResetEquipmentSystem(); } } bool UGIS_EquipmentSystemComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) { bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags); for (FGIS_EquipmentEntry& Entry : Container.Entries) { if (IsValid(Entry.Instance)) { if (IGIS_EquipmentInterface::Execute_IsReplicationManaged(Entry.Instance)) { WroteSomething |= Channel->ReplicateSubobject(Entry.Instance, *Bunch, *RepFlags); } } } return WroteSomething; } void UGIS_EquipmentSystemComponent::OnRegister() { Super::OnRegister(); } void UGIS_EquipmentSystemComponent::InitializeComponent() { Super::InitializeComponent(); if (GetWorld() && !GetWorld()->IsGameWorld()) { return; } Container.OwningComponent = this; if (!GetOwner()->IsUsingRegisteredSubObjectList()) { GIS_CLOG(Error, "requires enable bReplicateUsingRegisteredSubObjectList.") } } void UGIS_EquipmentSystemComponent::ReadyForReplication() { Super::ReadyForReplication(); // Register existing Equipment Instance if (IsUsingRegisteredSubObjectList()) { for (const TObjectPtr& PendingObject : PendingReplicatedEquipments) { if (!IsReplicatedSubObjectRegistered(PendingObject)) { AddReplicatedSubObject(PendingObject); } } PendingReplicatedEquipments.Empty(); } } // Called when the game starts void UGIS_EquipmentSystemComponent::BeginPlay() { Super::BeginPlay(); if (bInitializeOnBeginPlay && OwnerHasAuthority()) { InitializeEquipmentSystem(); } } void UGIS_EquipmentSystemComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGIS_EquipmentSystemComponent::TickComponent"), STAT_UGIS_EquipmentSystemComponent_TickComponent, STATGROUP_GIS) TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__) ProcessPendingEquipments(); TickEquipmentEntries(DeltaTime); } void UGIS_EquipmentSystemComponent::UninitializeComponent() { Super::UninitializeComponent(); } void UGIS_EquipmentSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (bInitializeOnBeginPlay) { ResetEquipmentSystem(); } Super::EndPlay(EndPlayReason); } void UGIS_EquipmentSystemComponent::InitializeEquipmentSystem() { if (bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "already initialized or has no authority!") return; } UGIS_InventorySystemComponent* InventorySystem = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner()); if (InventorySystem == nullptr) { InventorySystem = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetController()); } if (!InventorySystem) { GIS_CLOG(Error, "doesn't have valid inventory system component!") return; } InitializeEquipmentSystemWithInventory(InventorySystem); } void UGIS_EquipmentSystemComponent::InitializeEquipmentSystemWithInventory(UGIS_InventorySystemComponent* InventorySystem) { if (bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "already initialized or has no authority!") return; } if (!IsValid(InventorySystem)) { GIS_CLOG(Error, "the inventory is invalid!") return; } if (!InventorySystem->IsInventoryInitialized()) { GIS_CLOG(Error, "the inventory is not initialized!") return; } if (!TargetCollectionTag.IsValid()) { GIS_CLOG(Error, "doesn't have valid target collection tag!") return; } UGIS_ItemSlotCollection* Collection = Cast(InventorySystem->GetCollectionByTag(GetTargetCollectionTag())); if (Collection == nullptr) { GIS_CLOG(Error, "%s's inventory doesn't have valid item slot collection with name:%s", *InventorySystem->GetOwner()->GetName(), *TargetCollectionTag.ToString()); return; } Inventory = InventorySystem; TargetCollection = Collection; TargetCollectionDefinition = Collection->GetMyDefinition(); TArray Entries; for (const TPair& Pair : TargetCollectionDefinition->SlotGroupMap) { FGIS_EquipmentGroupEntry Entry; Entry.GroupTag = Pair.Key; Entry.ActiveSlot = FGameplayTag::EmptyTag; Entries.Add(Entry); } Inventory->OnInventoryStackUpdate.AddDynamic(this, &ThisClass::OnTargetCollectionChanged); Inventory->OnCollectionRemovedEvent.AddDynamic(this, &ThisClass::OnTargetCollectionRemoved); bEquipmentSystemInitialized = true; OnEquipmentSystemInitialized(); for (const FGIS_ItemInfo& ItemInfo : TargetCollection->GetAllItemInfos()) { FGameplayTag SlotTag = TargetCollection->GetItemSlotName(ItemInfo.Item); EquipItemToSlot(ItemInfo.Item, SlotTag); } } void UGIS_EquipmentSystemComponent::ResetEquipmentSystem() { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } RemoveAllEquipments(); if (IsValid(Inventory)) { Inventory->OnCollectionRemovedEvent.RemoveDynamic(this, &ThisClass::OnTargetCollectionRemoved); Inventory->OnInventoryStackUpdate.RemoveDynamic(this, &ThisClass::OnTargetCollectionChanged); Inventory = nullptr; TargetCollection = nullptr; TargetCollectionDefinition = nullptr; } bEquipmentSystemInitialized = false; OnEquipmentSystemInitialized(); } bool UGIS_EquipmentSystemComponent::IsEquipmentSystemInitialized() const { return bEquipmentSystemInitialized; } void UGIS_EquipmentSystemComponent::BindToEquipmentSystemInitialized(FGIS_EquipmentSystem_Initialized_DynamicEvent Delegate) { if (bEquipmentSystemInitialized) { Delegate.ExecuteIfBound(); } else { InitializedDelegates.Add(Delegate); } } UGIS_EquipmentSystemComponent* UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(const AActor* Actor) { return IsValid(Actor) ? Actor->FindComponentByClass() : nullptr; } bool UGIS_EquipmentSystemComponent::FindEquipmentSystemComponent(const AActor* Actor, UGIS_EquipmentSystemComponent*& Component) { Component = (Actor ? Actor->FindComponentByClass() : nullptr); return Component != nullptr; } bool UGIS_EquipmentSystemComponent::FindTypedEquipmentSystemComponent(AActor* Actor, TSubclassOf DesiredClass, UGIS_EquipmentSystemComponent*& Component) { if (UClass* RealClass = DesiredClass) { if (FindEquipmentSystemComponent(Actor, Component)) { if (Component->GetClass()->IsChildOf(RealClass)) { return true; } } } return false; } void UGIS_EquipmentSystemComponent::RemoveAllEquipments() { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } for (int32 i = 0; i < Container.Entries.Num(); i++) { RemoveEquipmentEntry(i); } } void UGIS_EquipmentSystemComponent::AddEquipmentEntry(const FGIS_EquipmentEntry& NewEntry) { check(NewEntry.IsValidEntry()) int32 Idx = Container.Entries.AddDefaulted(); Container.Entries[Idx] = NewEntry; AddReplicatedEquipmentObject(NewEntry.Instance); OnEquipmentEntryAdded(NewEntry, Idx); Container.MarkItemDirty(Container.Entries[Idx]); } void UGIS_EquipmentSystemComponent::TickEquipmentEntries(float DeltaTime) { if (!bEquipmentSystemInitialized) { return; } for (int32 i = 0; i < Container.Entries.Num(); i++) { if (Container.Entries[i].IsValidEntry()) { IGIS_EquipmentInterface::Execute_OnEquipmentTick(Container.Entries[i].Instance, DeltaTime); } } } void UGIS_EquipmentSystemComponent::RemoveEquipmentEntry(int32 Idx) { check(Container.Entries.IsValidIndex(Idx)); const FGIS_EquipmentEntry& Entry = Container.Entries[Idx]; RemoveReplicatedEquipmentObject(Entry.Instance); OnEquipmentEntryRemoved(Entry, Idx); Container.Entries.RemoveAt(Idx); Container.MarkArrayDirty(); } UObject* UGIS_EquipmentSystemComponent::CreateEquipmentInstance_Implementation(AActor* Owner, UGIS_ItemInstance* ItemInstance) const { if (ItemInstance == nullptr) { GIS_CLOG(Error, "passed in invalid item instance.") return nullptr; } const UGIS_ItemFragment_Equippable* EquippableItem = ItemInstance->FindFragmentByClass(); if (EquippableItem == nullptr) { GIS_CLOG(Error, "missing equippable fragment on item(%s)", *ItemInstance->GetDefinition()->GetName()) return nullptr; } TSubclassOf InstanceType = !EquippableItem->InstanceType.IsNull() ? EquippableItem->InstanceType.LoadSynchronous() : nullptr; if (InstanceType == nullptr) { GIS_CLOG(Error, "missing valid equipment instance type on item(%s)", *ItemInstance->GetDefinition()->GetName()) return nullptr; } if (!InstanceType->ImplementsInterface(UGIS_EquipmentInterface::StaticClass())) { GIS_CLOG(Error, "equipment instance type doesn't implement:%s", *UGIS_EquipmentInterface::StaticClass()->GetName()) return nullptr; } UObject* Instance; if (EquippableItem->bActorBased) { FActorSpawnParameters SpawnParameters; SpawnParameters.Owner = Owner; SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; AActor* SpawnedActor = GetWorld()->SpawnActor(InstanceType, FTransform::Identity, SpawnParameters); if (SpawnedActor == nullptr) { GIS_CLOG(Error, "failed to create equipment instance of type:%s", *InstanceType->GetName()) return nullptr; } Instance = SpawnedActor; } else { UGIS_EquipmentInstance* EquipmentInstance = NewObject(Owner, InstanceType); // Using the actor instead of component as the outer due to UE-127172 if (EquipmentInstance == nullptr) { GIS_CLOG(Error, "failed to create equipment instance of type:%s", *InstanceType->GetName()) return nullptr; } Instance = EquipmentInstance; } return Instance; } void UGIS_EquipmentSystemComponent::OnEquipmentEntryAdded(const FGIS_EquipmentEntry& Entry, int32 Idx) { APawn* OwningPawn = GetPawn(); SlotToInstanceMap.Add(Entry.EquippedSlot, Entry.Instance); // Initialize equipment instance IGIS_EquipmentInterface::Execute_ReceiveOwningPawn(Entry.Instance, OwningPawn); IGIS_EquipmentInterface::Execute_ReceiveSourceItem(Entry.Instance, Entry.ItemInstance); IGIS_EquipmentInterface::Execute_OnEquipmentBeginPlay(Entry.Instance); // Delay event broadcasting to ensure all initialization is complete FGIS_EquipmentEntry AddedEntry = Entry; GetWorld()->GetTimerManager().SetTimerForNextTick(FTimerDelegate::CreateLambda([this, AddedEntry]() { OnEquipmentStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, true); if (AddedEntry.bActive) { IGIS_EquipmentInterface::Execute_OnActiveStateChanged(AddedEntry.Instance, AddedEntry.bActive); OnEquipmentActiveStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, AddedEntry.bActive); } if (AddedEntry.EquippedGroup.IsValid()) { OnEquipmentGroupStateChangedEvent.Broadcast(AddedEntry.Instance, AddedEntry.EquippedSlot, AddedEntry.EquippedGroup);; } })); } void UGIS_EquipmentSystemComponent::OnEquipmentEntryChanged(const FGIS_EquipmentEntry& Entry, int32 Idx) { IGIS_EquipmentInterface::Execute_OnActiveStateChanged(Entry.Instance, Entry.bActive); OnEquipmentActiveStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, Entry.bActive); if (Entry.PrevEquippedGroup != Entry.EquippedGroup) { OnEquipmentGroupStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, Entry.EquippedGroup); } } void UGIS_EquipmentSystemComponent::OnEquipmentEntryRemoved(const FGIS_EquipmentEntry& Entry, int32 Idx) { // remove but still active, so notify instance to do deactivate behavior. SlotToInstanceMap.Remove(Entry.EquippedSlot); if (IsValid(Entry.Instance)) // The instance may alreay in pending kill state, so no point to continues execution. { if (Entry.bActive) { IGIS_EquipmentInterface::Execute_OnActiveStateChanged(Entry.Instance, false); } IGIS_EquipmentInterface::Execute_OnEquipmentEndPlay(Entry.Instance); IGIS_EquipmentInterface::Execute_ReceiveOwningPawn(Entry.Instance, nullptr); IGIS_EquipmentInterface::Execute_ReceiveSourceItem(Entry.Instance, nullptr); } OnEquipmentStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, false); OnEquipmentGroupStateChangedEvent.Broadcast(Entry.Instance, Entry.EquippedSlot, FGameplayTag::EmptyTag); } void UGIS_EquipmentSystemComponent::AddReplicatedEquipmentObject(TObjectPtr Instance) { if (OwnerHasAuthority() && IsValid(Instance)) { checkf(Instance->GetClass()->ImplementsInterface(UGIS_EquipmentInterface::StaticClass()), TEXT("%s doesn't implement GIS_EquipmentInterface"), *Instance->GetClass()->GetName()) bool IsReplicationManaged = IGIS_EquipmentInterface::Execute_IsReplicationManaged(Instance); if (!IsReplicationManaged) { return; } if (IsReadyForReplication() && !IsReplicatedSubObjectRegistered(Instance)) { AddReplicatedSubObject(Instance); } else { PendingReplicatedEquipments.AddUnique(Instance); } } } void UGIS_EquipmentSystemComponent::RemoveReplicatedEquipmentObject(TObjectPtr Instance) { if (OwnerHasAuthority() && IsValid(Instance)) { bool IsReplicationManaged = IGIS_EquipmentInterface::Execute_IsReplicationManaged(Instance); if (IsReplicationManaged && IsReplicatedSubObjectRegistered(Instance)) { RemoveReplicatedSubObject(Instance); } } } #pragma region Equipment Groups TMap UGIS_EquipmentSystemComponent::GetLayoutOfGroup(FGameplayTag GroupTag) const { TMap GroupLayout; if (TargetCollectionDefinition == nullptr) { return GroupLayout; } if (TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) { GroupLayout = TargetCollectionDefinition->SlotGroupMap[GroupTag].IndexToSlotMap; } return GroupLayout; } TMap UGIS_EquipmentSystemComponent::GetSlottedLayoutOfGroup(FGameplayTag GroupTag) const { TMap GroupLayout; if (TargetCollectionDefinition == nullptr) { return GroupLayout; } if (TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) { GroupLayout = TargetCollectionDefinition->SlotGroupMap[GroupTag].SlotToIndexMap; } return GroupLayout; } FGameplayTag UGIS_EquipmentSystemComponent::FindMatchingGroupForSlot(FGameplayTag SlotTag) const { if (TargetCollectionDefinition->SlotGroupMap.IsEmpty()) { return FGameplayTag::EmptyTag; } for (const TPair& Pair : TargetCollectionDefinition->SlotGroupMap) { if (Pair.Value.SlotToIndexMap.Contains(SlotTag)) { return Pair.Key; } } return FGameplayTag::EmptyTag; } TMap UGIS_EquipmentSystemComponent::GetSlottedEquipmentsOfGroup(FGameplayTag GroupTag) const { TMap GroupLayout = GetSlottedLayoutOfGroup(GroupTag); TMap GroupedEquipments; GroupedEquipments.Reserve(GroupLayout.Num()); for (auto& Pair : GroupLayout) { int32 Idx = Container.IndexOfBySlot(Pair.Key); if (Idx != INDEX_NONE) { GroupedEquipments.Emplace(Pair.Key, Container.Entries[Idx].Instance); } else { GroupedEquipments.Emplace(Pair.Key, nullptr); } } return GroupedEquipments; } TMap UGIS_EquipmentSystemComponent::GetEquipmentsOfGroup(FGameplayTag GroupTag) const { TMap GroupLayout = GetLayoutOfGroup(GroupTag); TMap GroupedEquipments; GroupedEquipments.Reserve(GroupLayout.Num()); for (auto& Pair : GroupLayout) { int32 Idx = Container.IndexOfBySlot(Pair.Value); if (Idx != INDEX_NONE) { GroupedEquipments.Emplace(Pair.Key, Container.Entries[Idx].Instance); } else { GroupedEquipments.Emplace(Pair.Key, nullptr); } } return GroupedEquipments; } void UGIS_EquipmentSystemComponent::SetGroupActiveSlot(FGameplayTag GroupTag, FGameplayTag NewSlot) { if (!bEquipmentSystemInitialized || !TargetCollectionDefinition || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } if (TargetCollectionDefinition->SlotGroupMap.IsEmpty()) { GIS_CLOG(Warning, "no equipment groups setup within TargetCollectionDefinition(%s)!", *TargetCollectionDefinition->GetName()) return; } if (!GroupTag.IsValid()) { GIS_CLOG(Warning, "requested invalid equipment group!") return; } if (!TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) { GIS_CLOG(Warning, "requested equipment group doesn't exists within TargetCollectionDefinition(%s)!", *TargetCollectionDefinition->GetName()) return; } int32 IdxToReset = Container.IndexOfByGroup(GroupTag); if (IdxToReset != INDEX_NONE) { const FGIS_EquipmentEntry& ExistingEntry = Container.Entries[IdxToReset]; if (ExistingEntry.EquippedGroup == GroupTag && ExistingEntry.EquippedSlot == NewSlot) { // Same state, return. return; } // Deactivate the equipment for prev active slot. if (Container.Entries[IdxToReset].bActive) { UpdateEquipmentState(IdxToReset, false, FGameplayTag::EmptyTag); } } int32 IdxToSet = NewSlot.IsValid() ? Container.IndexOfBySlot(NewSlot) : INDEX_NONE; if (IdxToSet != INDEX_NONE) { if (!Container.Entries[IdxToSet].bActive) { UpdateEquipmentState(IdxToSet, true, GroupTag); } } } void UGIS_EquipmentSystemComponent::ServerSetGroupActiveSlot_Implementation(FGameplayTag GroupTag, FGameplayTag NewSlot) { SetGroupActiveSlot(GroupTag, NewSlot); } void UGIS_EquipmentSystemComponent::CycleGroupActiveSlot(FGameplayTag GroupTag, bool bDirection) { if (!bEquipmentSystemInitialized || !OwnerHasAuthority()) { GIS_CLOG(Error, "not initialized or has no authority!") return; } if (!TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) { GIS_CLOG(Warning, "has no equipment group named:%s.", *GroupTag.ToString()) return; } FGameplayTag PrevSlot = FGameplayTag::EmptyTag; int32 Idx = Container.IndexOfByGroup(GroupTag); if (Idx != INDEX_NONE) { PrevSlot = Container.Entries[Idx].EquippedSlot; } FGameplayTag NewSlot = CycleGroupNextSlot(GroupTag, PrevSlot, bDirection); if (NewSlot != PrevSlot) { SetGroupActiveSlot(GroupTag, NewSlot); } } FGameplayTag UGIS_EquipmentSystemComponent::CycleGroupNextSlot(FGameplayTag GroupTag, FGameplayTag PrevSlot, bool bDirection) { TMap GroupedEquipments = GetSlottedEquipmentsOfGroup(GroupTag); if (GroupedEquipments.IsEmpty()) { // No equipments in the group, nothing to cycle return FGameplayTag::EmptyTag; } if (!TargetCollectionDefinition || !TargetCollectionDefinition->SlotGroupMap.Contains(GroupTag)) { return FGameplayTag::EmptyTag; } const FGIS_ItemSlotGroup& SlotGroup = TargetCollectionDefinition->SlotGroupMap[GroupTag]; // Get all slots in the group TArray AllSlots; SlotGroup.IndexToSlotMap.GenerateValueArray(AllSlots); if (AllSlots.IsEmpty()) { return FGameplayTag::EmptyTag; } // Starting index for the search int32 StartIndex; if (!PrevSlot.IsValid()) { StartIndex = bDirection ? 0 : AllSlots.Num() - 1; } else { StartIndex = AllSlots.Find(PrevSlot); if (StartIndex == INDEX_NONE) { StartIndex = bDirection ? 0 : AllSlots.Num() - 1; } else { StartIndex = bDirection ? StartIndex + 1 : StartIndex - 1; if (bDirection && StartIndex >= AllSlots.Num()) { StartIndex = 0; } if (!bDirection && StartIndex < 0) { StartIndex = AllSlots.Num() - 1; } } } // Now, search for the next valid slot starting from StartIndex, wrapping around int32 CurrentIndex = StartIndex; bool bWrappedAround = false; do { FGameplayTag CurrentSlot = AllSlots[CurrentIndex]; if (GroupedEquipments.Contains(CurrentSlot) && IsValid(GroupedEquipments[CurrentSlot])) { // Found a valid equipment at this slot return CurrentSlot; } // Move to the next index if (bDirection) { CurrentIndex++; if (CurrentIndex >= AllSlots.Num()) { CurrentIndex = 0; } } else { CurrentIndex--; if (CurrentIndex < 0) { CurrentIndex = AllSlots.Num() - 1; } } // Check if we've wrapped around fully if (CurrentIndex == StartIndex) { bWrappedAround = true; } } while (!bWrappedAround); // If no valid slot found after full cycle, return empty tag return FGameplayTag::EmptyTag; } void UGIS_EquipmentSystemComponent::ServerCycleGroupActiveSlot_Implementation(FGameplayTag GroupTag, bool bDirection) { CycleGroupActiveSlot(GroupTag, bDirection); } #pragma endregion