Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #pragma once
- #include "Actors/AWorldItem.h"
- #include "Management/RancInventoryData.h"
- #include "RancItemContainerComponent.generated.h"
- UCLASS(Blueprintable, ClassGroup = (Custom), Category = "Ranc Inventory | Classes", EditInlineNew, meta = (BlueprintSpawnableComponent))
- class URancItemContainerComponent : public UActorComponent
- {
- GENERATED_BODY()
- public:
- explicit URancItemContainerComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
- virtual void InitializeComponent() override;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- float GetCurrentWeight() const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- float GetMaxWeight() const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- int32 GetCurrentItemCount() const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- const FRancItemInstance& FindItemById(const FGameplayTag& ItemId) const;
- /* Add items to the inventory
- * Should only be called on server as we can't trust client to provide trustworthy ItemInstance
- * Instead have the client send an input like CraftItem or PickupItem to the server which results in server call to AddItem */
- UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
- void AddItems_IfServer(const FRancItemInstance& ItemInstance);
- /* For most games we could probably trust the client to specify ItemInstance but for e.g. a hot potato we can't
- * Instead have the client send an input like UseItem or DropItem */
- UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
- bool RemoveItems_IfServer(const FRancItemInstance& ItemInstance);
- /* Attempts to drop the item from the inventory, attempting to spawn an Item object in the world
- * Specify DropItemClass and DropDistance properties to customize the drop
- * Called on client it will request the drop on the server
- * Returns the quantity actually dropped, on client this is only a "guess" */
- UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
- int32 DropItems(const FRancItemInstance& ItemInstance, float DropAngle = 0);
- UFUNCTION(Server, Reliable)
- int32 DropItems_Server(const FRancItemInstance& ItemInstance, float DropAngle = 0);
- /* Useful for e.g. Death, drops items evenly spaced in a circle with radius DropDistance */
- UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
- int32 DropAllItems_IfServer();
- UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
- bool CanReceiveItem(const FRancItemInstance& ItemInstance) const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- bool ContainsItem(const FGameplayTag& ItemId, int32 Quantity = 1) const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- TArray<FRancItemInstance> GetAllItems() const;
- UFUNCTION(BlueprintPure, Category="Ranc Inventory")
- bool IsEmpty() const;
- DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemAdded, const FRancItemInstance&, ItemInstance);
- UPROPERTY(BlueprintAssignable, Category="Ranc Inventory")
- FOnInventoryItemAdded OnItemAdded;
- DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemRemoved, const FRancItemInstance&, ItemInstance);
- UPROPERTY(BlueprintAssignable, Category="Ranc Inventory")
- FOnInventoryItemRemoved OnItemRemoved;
- /* Distance away from the owning actor to drop items. Only used on server */
- UPROPERTY(EditAnywhere, BlueprintReadWrite)
- float DropDistance = 100;
- /* Class to spawn when dropping items. Only used on server */
- UPROPERTY(EditAnywhere, BlueprintReadWrite)
- TSubclassOf<AWorldItem> DropItemClass = AWorldItem::StaticClass();
- /* Max weight allowed for this inventory */
- UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Ranc Inventory", meta = (AllowPrivateAccess = "true", ClampMin = "0", UIMin = "0"))
- float MaxWeight;
- /* Max num of items allowed for this inventory */
- UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Ranc Inventory", meta = (AllowPrivateAccess = "true", ClampMin = "1", UIMin = "1"))
- int32 MaxNumItems;
- protected:
- UPROPERTY(EditAnywhere, Category="Ranc Inventory")
- TArray<FRancInitialItem> InitialItems;
- UPROPERTY(ReplicatedUsing=OnRep_Items, BlueprintReadOnly, Category="Ranc Inventory")
- TArray<FRancItemInstance> Items;
- // The cache is a copy of Items that is not replicated, used to detect changes after replication, only used on client
- TMap<FGameplayTag, int32> ItemsCache; // Id to quantity;
- AWorldItem* SpawnDroppedItem_IfServer(const FRancItemInstance& ItemInstance, float DropAngle = 0) const;
- virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
- protected:
- virtual void UpdateWeight();
- void CopyItemsToCache();
- void DetectAndPublishChanges();
- private:
- float CurrentWeight;
- TArray<FGameplayTag> _KeysToRemove; // Could be a local value but just slight optimization to avoid creating a new array every time.
- UFUNCTION()
- void OnRep_Items();
- };
- CPP:
- #include "Components/RancItemContainerComponent.h"
- #include "Management/RancInventoryFunctions.h"
- #include "Net/UnrealNetwork.h"
- #include "Net/Core/PushModel/PushModel.h"
- URancItemContainerComponent::URancItemContainerComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), MaxWeight(0.f), MaxNumItems(0), CurrentWeight(0.f)
- {
- PrimaryComponentTick.bCanEverTick = false;
- PrimaryComponentTick.bStartWithTickEnabled = false;
- bWantsInitializeComponent = true;
- SetIsReplicatedByDefault(true);
- }
- void URancItemContainerComponent::InitializeComponent()
- {
- Super::InitializeComponent();
- // add all initial items to items
- if (GetOwnerRole() == ROLE_Authority && GetOwnerRole() != ROLE_None)
- {
- for (const FRancInitialItem& InitialItem : InitialItems)
- {
- const URancItemData* Data = URancInventoryFunctions::GetSingleItemDataById(InitialItem.ItemId, {}, false);
- if (Data && Data->ItemId.IsValid())
- Items.Add(FRancItemInstance(Data->ItemId, InitialItem.Quantity));
- }
- }
- CopyItemsToCache();
- if (DropItemClass == nullptr)
- {
- DropItemClass = AWorldItem::StaticClass();
- }
- }
- void URancItemContainerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
- {
- Super::GetLifetimeReplicatedProps(OutLifetimeProps);
- FDoRepLifetimeParams SharedParams;
- SharedParams.bIsPushBased = true;
- DOREPLIFETIME_WITH_PARAMS_FAST(URancItemContainerComponent, Items, SharedParams);
- }
- void URancItemContainerComponent::OnRep_Items()
- {
- // Recalculate the total weight of the inventory after replication.
- UpdateWeight();
- DetectAndPublishChanges();
- }
- void URancItemContainerComponent::AddItems_IfServer(const FRancItemInstance& ItemInstance)
- {
- if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None) // none needed for tests
- {
- UE_LOG(LogTemp, Warning, TEXT("AddItems called on non-authority!"));
- return;
- }
- // Check if the inventory can receive the item
- if (!CanReceiveItem(ItemInstance))
- {
- UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: %s"), *ItemInstance.ItemId.ToString());
- return;
- }
- bool bItemAdded = false;
- for (auto& ExistingItem : Items)
- {
- // If item exists and is stackable, increase the quantity
- if (ExistingItem.ItemId == ItemInstance.ItemId)
- {
- ExistingItem.Quantity += ItemInstance.Quantity;
- bItemAdded = true;
- break;
- }
- }
- // If item does not exist in the inventory, add it
- if (!bItemAdded)
- {
- Items.Add(ItemInstance);
- }
- // Update the current weight of the inventory
- UpdateWeight();
- OnItemAdded.Broadcast(ItemInstance);
- // Mark the Items array as dirty to ensure replication
- MARK_PROPERTY_DIRTY_FROM_NAME(URancItemContainerComponent, Items, this);
- }
- bool URancItemContainerComponent::RemoveItems_IfServer(const FRancItemInstance& ItemInstance)
- {
- if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None)
- {
- UE_LOG(LogTemp, Warning, TEXT("RemoveItems called on non-authority!"));
- return false;
- }
- // Check if the inventory can give the item
- if (!ContainsItem(ItemInstance.ItemId, ItemInstance.Quantity))
- {
- UE_LOG(LogTemp, Warning, TEXT("Cannot remove item: %s"), *ItemInstance.ItemId.ToString());
- return false;
- }
- for (int i = Items.Num() - 1; i >= 0; --i)
- {
- auto& ExistingItem = Items[i];
- if (ExistingItem.ItemId == ItemInstance.ItemId)
- {
- ExistingItem.Quantity -= ItemInstance.Quantity;
- // If the quantity drops to zero or below, remove the item from the inventory
- if (ExistingItem.Quantity <= 0)
- {
- Items.RemoveAt(i);
- break; // Assuming ItemId is unique and only one instance exists in the inventory
- }
- }
- }
- // Update the current weight of the inventory
- UpdateWeight();
- OnItemRemoved.Broadcast(ItemInstance);
- // Mark the Items array as dirty to ensure replication
- MARK_PROPERTY_DIRTY_FROM_NAME(URancItemContainerComponent, Items, this);
- return true;
- }
- AWorldItem* URancItemContainerComponent::SpawnDroppedItem_IfServer(const FRancItemInstance& ItemInstance, float DropAngle) const
- {
- if (UWorld* World = GetWorld())
- {
- FActorSpawnParameters SpawnParams;
- const FVector DropSpot = DropAngle == 0 ? GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * DropDistance : GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector().RotateAngleAxis(DropAngle, FVector::UpVector) * DropDistance;
- AWorldItem* WorldItem = World->SpawnActorDeferred<AWorldItem>(DropItemClass, FTransform(DropSpot));
- if (WorldItem)
- {
- WorldItem->SetItem(ItemInstance);
- WorldItem->FinishSpawning(FTransform(DropSpot));
- }
- return WorldItem;
- }
- return nullptr;
- }
- int32 URancItemContainerComponent::DropItems(const FRancItemInstance& ItemInstance, float DropAngle)
- {
- int32 QuantityToDrop = DropItems_Server(ItemInstance, DropAngle);
- if (GetOwnerRole() != ROLE_Authority) // if client
- {
- // On client the below is just a guess
- const auto ContainedItemInstance = FindItemById(ItemInstance.ItemId);
- QuantityToDrop = FMath::Min(ItemInstance.Quantity, ContainedItemInstance.Quantity);
- }
- return QuantityToDrop;
- }
- int32 URancItemContainerComponent::DropItems_Server_Implementation(const FRancItemInstance& ItemInstance, float DropAngle)
- {
- auto ContainedItemInstance = FindItemById(ItemInstance.ItemId);
- const int32 QuantityToDrop = FMath::Min(ItemInstance.Quantity, ContainedItemInstance.Quantity);
- if (QuantityToDrop <= 0 || !ContainedItemInstance.ItemId.IsValid())
- {
- return 0;
- }
- AWorldItem* DroppedItem = SpawnDroppedItem_IfServer(FRancItemInstance(ItemInstance.ItemId, QuantityToDrop), DropAngle);
- if (DroppedItem)
- {
- ContainedItemInstance.Quantity -= QuantityToDrop;
- if (ContainedItemInstance.Quantity <= 0)
- {
- Items.Remove(ContainedItemInstance);
- }
- OnItemRemoved.Broadcast(ItemInstance);
- UpdateWeight();
- return QuantityToDrop;
- }
- return 0;
- }
- int32 URancItemContainerComponent::DropAllItems_IfServer()
- {
- if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None)
- {
- UE_LOG(LogTemp, Warning, TEXT("RemoveItems called on non-authority!"));
- return 0;
- }
- // drop with incrementing angle
- int32 DroppedCount = 0;
- float AngleStep = 360.f / Items.Num();
- for (int i = Items.Num() - 1; i >= 0; --i)
- {
- DropItems(Items[i], AngleStep * DroppedCount);
- DroppedCount++;
- }
- UpdateWeight();
- return DroppedCount;
- }
- float URancItemContainerComponent::GetCurrentWeight() const
- {
- return CurrentWeight;
- }
- float URancItemContainerComponent::GetMaxWeight() const
- {
- return MaxWeight <= 0.f ? MAX_flt : MaxWeight;
- }
- const FRancItemInstance& URancItemContainerComponent::FindItemById(const FGameplayTag& ItemId) const
- {
- for (const auto& Item : Items)
- {
- if (Item.ItemId == ItemId)
- {
- return Item;
- }
- }
- // If the item is not found, throw an error or return a reference to a static empty item info
- UE_LOG(LogTemp, Warning, TEXT("Item with ID %s not found."), *ItemId.ToString());
- return FRancItemInstance::EmptyItemInstance;
- }
- bool URancItemContainerComponent::CanReceiveItem(const FRancItemInstance& ItemInstance) const
- {
- // Check if adding this item would exceed the max number of unique items
- if (!ContainsItem(ItemInstance.ItemId) && Items.Num() >= MaxNumItems)
- {
- UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: Inventory is full."));
- return false;
- }
- // Calculate the additional weight this item would add
- if (const URancItemData* const ItemData = URancInventoryFunctions::GetItemDataById(ItemInstance.ItemId))
- {
- float AdditionalWeight = ItemData->ItemWeight * ItemInstance.Quantity;
- if (CurrentWeight + AdditionalWeight > MaxWeight)
- {
- UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: Exceeds max weight."));
- return false;
- }
- }
- else
- {
- UE_LOG(LogTemp, Warning, TEXT("Could not find item data for item: %s"), *ItemInstance.ItemId.ToString());
- return false;
- }
- return true;
- }
- bool URancItemContainerComponent::ContainsItem(const FGameplayTag& ItemId, int32 Quantity) const
- {
- int32 TotalQuantity = 0;
- for (const auto& Item : Items)
- {
- if (Item.ItemId == ItemId)
- {
- TotalQuantity += Item.Quantity;
- if (TotalQuantity >= Quantity)
- {
- return true;
- }
- }
- }
- return false;
- }
- int32 URancItemContainerComponent::GetCurrentItemCount() const
- {
- int32 Count = 0;
- for (const auto& Item : Items)
- {
- Count += Item.Quantity;
- }
- return Count;
- }
- TArray<FRancItemInstance> URancItemContainerComponent::GetAllItems() const
- {
- return Items;
- }
- bool URancItemContainerComponent::IsEmpty() const
- {
- return Items.Num() == 0;
- }
- void URancItemContainerComponent::UpdateWeight()
- {
- CurrentWeight = 0.0f; // Reset weight
- for (const auto& ItemInstance : Items)
- {
- if (const URancItemData* const ItemData = URancInventoryFunctions::GetItemDataById(ItemInstance.ItemId))
- {
- CurrentWeight += ItemData->ItemWeight * ItemInstance.Quantity;
- }
- }
- // This example does not handle the case where item data is not found, which might be important for ensuring accuracy.
- }
- void URancItemContainerComponent::CopyItemsToCache()
- {
- ItemsCache.Reset();
- ItemsCache.Reserve(Items.Num());
- for (int i = 0; i < Items.Num(); ++i)
- {
- ItemsCache[Items[i].ItemId] = Items[i].Quantity;
- }
- }
- void URancItemContainerComponent::DetectAndPublishChanges()
- {
- // First pass: Update existing items or add new ones, mark them by setting quantity to negative.
- for (FRancItemInstance& NewItem : Items)
- {
- int32* OldQuantity = ItemsCache.Find(NewItem.ItemId);
- if (OldQuantity)
- {
- // Item exists, check for quantity change
- if (*OldQuantity != NewItem.Quantity)
- {
- if (*OldQuantity < NewItem.Quantity)
- {
- OnItemAdded.Broadcast(FRancItemInstance(NewItem.ItemId, NewItem.Quantity - *OldQuantity));
- }
- else if (*OldQuantity > NewItem.Quantity)
- {
- OnItemRemoved.Broadcast(FRancItemInstance(NewItem.ItemId, *OldQuantity - NewItem.Quantity));
- }
- }
- // Mark this item as processed by temporarily setting its value to its own negative (or another marker strategy)
- *OldQuantity = -abs(NewItem.Quantity);
- }
- else
- {
- // New item
- OnItemAdded.Broadcast(NewItem);
- ItemsCache.Add(NewItem.ItemId, -abs(NewItem.Quantity)); // Mark as processed
- }
- }
- // Second pass: Remove unmarked items (those not set to negative) and revert marks for processed items
- _KeysToRemove.Reset(ItemsCache.Num());
- for (auto& Pair : ItemsCache)
- {
- if (Pair.Value >= 0)
- {
- // Item was not processed (not found in Items), so it has been removed
- OnItemRemoved.Broadcast(FRancItemInstance(Pair.Key, Pair.Value));
- _KeysToRemove.Add(Pair.Key);
- }
- else
- {
- // Revert the mark to reflect the actual quantity
- Pair.Value = -Pair.Value;
- }
- }
- // Remove items that were not found in the current Items array
- for (const FGameplayTag& Key : _KeysToRemove)
- {
- ItemsCache.Remove(Key);
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment