Crovea

Untitled

Feb 17th, 2024
1,522
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 17.35 KB | None | 0 0
  1. #pragma once
  2. #include "Actors/AWorldItem.h"
  3. #include "Management/RancInventoryData.h"
  4. #include "RancItemContainerComponent.generated.h"
  5.  
  6. UCLASS(Blueprintable, ClassGroup = (Custom), Category = "Ranc Inventory | Classes", EditInlineNew, meta = (BlueprintSpawnableComponent))
  7. class URancItemContainerComponent : public UActorComponent
  8. {
  9.     GENERATED_BODY()
  10. public:
  11.    
  12.     explicit URancItemContainerComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
  13.  
  14.     virtual void InitializeComponent() override;
  15.    
  16.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  17.     float GetCurrentWeight() const;
  18.  
  19.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  20.     float GetMaxWeight() const;
  21.  
  22.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  23.     int32 GetCurrentItemCount() const;
  24.  
  25.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  26.     const FRancItemInstance& FindItemById(const FGameplayTag& ItemId) const;
  27.  
  28.     /* Add items to the inventory
  29.      * Should only be called on server as we can't trust client to provide trustworthy ItemInstance
  30.      * Instead have the client send an input like CraftItem or PickupItem to the server which results in server call to AddItem  */
  31.     UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
  32.     void AddItems_IfServer(const FRancItemInstance& ItemInstance);
  33.  
  34.     /* For most games we could probably trust the client to specify ItemInstance but for e.g. a hot potato we can't
  35.      * Instead have the client send an input like UseItem or DropItem  */
  36.     UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
  37.     bool RemoveItems_IfServer(const FRancItemInstance& ItemInstance);
  38.  
  39.     /* Attempts to drop the item from the inventory, attempting to spawn an Item object in the world
  40.      * Specify DropItemClass and DropDistance properties to customize the drop
  41.      * Called on client it will request the drop on the server
  42.      * Returns the quantity actually dropped, on client this is only a "guess" */
  43.     UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
  44.     int32 DropItems(const FRancItemInstance& ItemInstance, float DropAngle = 0);
  45.  
  46.     UFUNCTION(Server, Reliable)
  47.     int32 DropItems_Server(const FRancItemInstance& ItemInstance, float DropAngle = 0);
  48.  
  49.     /* Useful for e.g. Death, drops items evenly spaced in a circle with radius DropDistance */
  50.     UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
  51.     int32 DropAllItems_IfServer();
  52.  
  53.     UFUNCTION(BlueprintCallable, Category="Ranc Inventory")
  54.     bool CanReceiveItem(const FRancItemInstance& ItemInstance) const;
  55.    
  56.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  57.     bool ContainsItem(const FGameplayTag& ItemId, int32 Quantity = 1) const;
  58.  
  59.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  60.     TArray<FRancItemInstance> GetAllItems() const;
  61.    
  62.     UFUNCTION(BlueprintPure, Category="Ranc Inventory")
  63.     bool IsEmpty() const;
  64.    
  65.     DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemAdded, const FRancItemInstance&, ItemInstance);
  66.     UPROPERTY(BlueprintAssignable, Category="Ranc Inventory")
  67.     FOnInventoryItemAdded OnItemAdded;
  68.    
  69.     DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemRemoved, const FRancItemInstance&, ItemInstance);
  70.     UPROPERTY(BlueprintAssignable, Category="Ranc Inventory")
  71.     FOnInventoryItemRemoved OnItemRemoved;
  72.    
  73.     /* Distance away from the owning actor to drop items. Only used on server */
  74.     UPROPERTY(EditAnywhere, BlueprintReadWrite)
  75.     float DropDistance = 100;
  76.  
  77.     /* Class to spawn when dropping items. Only used on server */
  78.     UPROPERTY(EditAnywhere, BlueprintReadWrite)
  79.     TSubclassOf<AWorldItem> DropItemClass = AWorldItem::StaticClass();
  80.  
  81.     /* Max weight allowed for this inventory */
  82.     UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Ranc Inventory", meta = (AllowPrivateAccess = "true", ClampMin = "0", UIMin = "0"))
  83.     float MaxWeight;
  84.  
  85.     /* Max num of items allowed for this inventory */
  86.     UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Ranc Inventory", meta = (AllowPrivateAccess = "true", ClampMin = "1", UIMin = "1"))
  87.     int32 MaxNumItems;
  88.  
  89. protected:
  90.  
  91.     UPROPERTY(EditAnywhere, Category="Ranc Inventory")
  92.     TArray<FRancInitialItem> InitialItems;
  93.    
  94.     UPROPERTY(ReplicatedUsing=OnRep_Items, BlueprintReadOnly, Category="Ranc Inventory")
  95.     TArray<FRancItemInstance> Items;
  96.  
  97.     // The cache is a copy of Items that is not replicated, used to detect changes after replication, only used on client
  98.     TMap<FGameplayTag, int32> ItemsCache; // Id to quantity;
  99.  
  100.     AWorldItem* SpawnDroppedItem_IfServer(const FRancItemInstance& ItemInstance, float DropAngle = 0) const;
  101.    
  102.     virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
  103. protected:
  104.    
  105.     virtual void UpdateWeight();
  106.  
  107.     void CopyItemsToCache();
  108.     void DetectAndPublishChanges();
  109.    
  110. private:
  111.     float CurrentWeight;
  112.  
  113.     TArray<FGameplayTag> _KeysToRemove; // Could be a local value but just slight optimization to avoid creating a new array every time.
  114.  
  115.     UFUNCTION()
  116.     void OnRep_Items();
  117.    
  118. };
  119.  
  120. CPP:
  121.  
  122. #include "Components/RancItemContainerComponent.h"
  123.  
  124. #include "Management/RancInventoryFunctions.h"
  125. #include "Net/UnrealNetwork.h"
  126. #include "Net/Core/PushModel/PushModel.h"
  127.  
  128. URancItemContainerComponent::URancItemContainerComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), MaxWeight(0.f), MaxNumItems(0), CurrentWeight(0.f)
  129. {
  130.     PrimaryComponentTick.bCanEverTick = false;
  131.     PrimaryComponentTick.bStartWithTickEnabled = false;
  132.     bWantsInitializeComponent = true;
  133.     SetIsReplicatedByDefault(true);
  134. }
  135.  
  136. void URancItemContainerComponent::InitializeComponent()
  137. {
  138.     Super::InitializeComponent();
  139.  
  140.     // add all initial items to items
  141.     if (GetOwnerRole() == ROLE_Authority && GetOwnerRole() != ROLE_None)
  142.     {
  143.         for (const FRancInitialItem& InitialItem : InitialItems)
  144.         {
  145.             const URancItemData* Data = URancInventoryFunctions::GetSingleItemDataById(InitialItem.ItemId, {}, false);
  146.            
  147.             if (Data && Data->ItemId.IsValid())
  148.                 Items.Add(FRancItemInstance(Data->ItemId, InitialItem.Quantity));
  149.         }
  150.     }
  151.     CopyItemsToCache();
  152.    
  153.     if (DropItemClass == nullptr)
  154.     {
  155.         DropItemClass = AWorldItem::StaticClass();
  156.     }
  157. }
  158.  
  159.  
  160. void URancItemContainerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
  161. {
  162.     Super::GetLifetimeReplicatedProps(OutLifetimeProps);
  163.  
  164.     FDoRepLifetimeParams SharedParams;
  165.     SharedParams.bIsPushBased = true;
  166.  
  167.     DOREPLIFETIME_WITH_PARAMS_FAST(URancItemContainerComponent, Items, SharedParams);
  168. }
  169.  
  170.  
  171. void URancItemContainerComponent::OnRep_Items()
  172. {
  173.     // Recalculate the total weight of the inventory after replication.
  174.     UpdateWeight();
  175.  
  176.     DetectAndPublishChanges();
  177. }
  178.  
  179. void URancItemContainerComponent::AddItems_IfServer(const FRancItemInstance& ItemInstance)
  180. {
  181.     if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None) // none needed for tests
  182.     {
  183.         UE_LOG(LogTemp, Warning, TEXT("AddItems called on non-authority!"));
  184.         return;
  185.     }
  186.  
  187.     // Check if the inventory can receive the item
  188.     if (!CanReceiveItem(ItemInstance))
  189.     {
  190.         UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: %s"), *ItemInstance.ItemId.ToString());
  191.         return;
  192.     }
  193.  
  194.     bool bItemAdded = false;
  195.     for (auto& ExistingItem : Items)
  196.     {
  197.         // If item exists and is stackable, increase the quantity
  198.         if (ExistingItem.ItemId == ItemInstance.ItemId)
  199.         {
  200.             ExistingItem.Quantity += ItemInstance.Quantity;
  201.             bItemAdded = true;
  202.             break;
  203.         }
  204.     }
  205.  
  206.     // If item does not exist in the inventory, add it
  207.     if (!bItemAdded)
  208.     {
  209.         Items.Add(ItemInstance);
  210.     }
  211.  
  212.     // Update the current weight of the inventory
  213.     UpdateWeight();
  214.  
  215.     OnItemAdded.Broadcast(ItemInstance);
  216.  
  217.     // Mark the Items array as dirty to ensure replication
  218.     MARK_PROPERTY_DIRTY_FROM_NAME(URancItemContainerComponent, Items, this);
  219. }
  220.  
  221. bool URancItemContainerComponent::RemoveItems_IfServer(const FRancItemInstance& ItemInstance)
  222. {
  223.     if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None)
  224.     {
  225.         UE_LOG(LogTemp, Warning, TEXT("RemoveItems called on non-authority!"));
  226.         return false;
  227.     }
  228.  
  229.     // Check if the inventory can give the item
  230.     if (!ContainsItem(ItemInstance.ItemId, ItemInstance.Quantity))
  231.     {
  232.         UE_LOG(LogTemp, Warning, TEXT("Cannot remove item: %s"), *ItemInstance.ItemId.ToString());
  233.         return false;
  234.     }
  235.  
  236.     for (int i = Items.Num() - 1; i >= 0; --i)
  237.     {
  238.         auto& ExistingItem = Items[i];
  239.         if (ExistingItem.ItemId == ItemInstance.ItemId)
  240.         {
  241.             ExistingItem.Quantity -= ItemInstance.Quantity;
  242.  
  243.             // If the quantity drops to zero or below, remove the item from the inventory
  244.             if (ExistingItem.Quantity <= 0)
  245.             {
  246.                 Items.RemoveAt(i);
  247.                 break; // Assuming ItemId is unique and only one instance exists in the inventory
  248.             }
  249.         }
  250.     }
  251.    
  252.     // Update the current weight of the inventory
  253.     UpdateWeight();
  254.  
  255.     OnItemRemoved.Broadcast(ItemInstance);
  256.    
  257.     // Mark the Items array as dirty to ensure replication
  258.     MARK_PROPERTY_DIRTY_FROM_NAME(URancItemContainerComponent, Items, this);
  259.  
  260.     return true;
  261. }
  262.  
  263. AWorldItem* URancItemContainerComponent::SpawnDroppedItem_IfServer(const FRancItemInstance& ItemInstance, float DropAngle) const
  264. {
  265.     if (UWorld* World = GetWorld())
  266.     {
  267.         FActorSpawnParameters SpawnParams;
  268.         const FVector DropSpot = DropAngle == 0 ? GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * DropDistance : GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector().RotateAngleAxis(DropAngle, FVector::UpVector) * DropDistance;
  269.         AWorldItem* WorldItem = World->SpawnActorDeferred<AWorldItem>(DropItemClass, FTransform(DropSpot));
  270.         if (WorldItem)
  271.         {
  272.             WorldItem->SetItem(ItemInstance);
  273.             WorldItem->FinishSpawning(FTransform(DropSpot));
  274.         }
  275.         return WorldItem;
  276.     }
  277.     return nullptr;
  278. }
  279.  
  280. int32 URancItemContainerComponent::DropItems(const FRancItemInstance& ItemInstance, float DropAngle)
  281. {
  282.     int32 QuantityToDrop = DropItems_Server(ItemInstance, DropAngle);
  283.    
  284.     if (GetOwnerRole() != ROLE_Authority) // if client
  285.     {
  286.         // On client the below is just a guess
  287.         const auto ContainedItemInstance = FindItemById(ItemInstance.ItemId);
  288.         QuantityToDrop = FMath::Min(ItemInstance.Quantity, ContainedItemInstance.Quantity);
  289.     }
  290.    
  291.     return QuantityToDrop;
  292. }
  293.  
  294. int32 URancItemContainerComponent::DropItems_Server_Implementation(const FRancItemInstance& ItemInstance, float DropAngle)
  295. {
  296.     auto ContainedItemInstance = FindItemById(ItemInstance.ItemId);
  297.     const int32 QuantityToDrop = FMath::Min(ItemInstance.Quantity, ContainedItemInstance.Quantity);
  298.  
  299.     if (QuantityToDrop <= 0 || !ContainedItemInstance.ItemId.IsValid())
  300.     {
  301.         return 0;
  302.     }
  303.  
  304.     AWorldItem* DroppedItem = SpawnDroppedItem_IfServer(FRancItemInstance(ItemInstance.ItemId, QuantityToDrop), DropAngle);
  305.  
  306.     if (DroppedItem)
  307.     {
  308.         ContainedItemInstance.Quantity -= QuantityToDrop;
  309.         if (ContainedItemInstance.Quantity <= 0)
  310.         {
  311.             Items.Remove(ContainedItemInstance);
  312.         }
  313.         OnItemRemoved.Broadcast(ItemInstance);
  314.         UpdateWeight();
  315.         return QuantityToDrop;
  316.     }
  317.     return 0;
  318. }
  319.  
  320. int32 URancItemContainerComponent::DropAllItems_IfServer()
  321. {
  322.     if (GetOwnerRole() != ROLE_Authority && GetOwnerRole() != ROLE_None)
  323.     {
  324.         UE_LOG(LogTemp, Warning, TEXT("RemoveItems called on non-authority!"));
  325.         return 0;
  326.     }
  327.    
  328.     // drop with incrementing angle
  329.     int32 DroppedCount = 0;
  330.     float AngleStep = 360.f / Items.Num();
  331.  
  332.     for (int i = Items.Num() - 1; i >= 0; --i)
  333.     {
  334.         DropItems(Items[i], AngleStep * DroppedCount);
  335.         DroppedCount++;
  336.     }
  337.    
  338.     UpdateWeight();
  339.    
  340.     return DroppedCount;
  341. }
  342.  
  343. float URancItemContainerComponent::GetCurrentWeight() const
  344. {
  345.     return CurrentWeight;
  346. }
  347.  
  348. float URancItemContainerComponent::GetMaxWeight() const
  349. {
  350.     return MaxWeight <= 0.f ? MAX_flt : MaxWeight;
  351. }
  352.  
  353. const FRancItemInstance& URancItemContainerComponent::FindItemById(const FGameplayTag& ItemId) const
  354. {
  355.     for (const auto& Item : Items)
  356.     {
  357.         if (Item.ItemId == ItemId)
  358.         {
  359.             return Item;
  360.         }
  361.     }
  362.  
  363.     // If the item is not found, throw an error or return a reference to a static empty item info
  364.     UE_LOG(LogTemp, Warning, TEXT("Item with ID %s not found."), *ItemId.ToString());
  365.     return FRancItemInstance::EmptyItemInstance;
  366. }
  367.  
  368. bool URancItemContainerComponent::CanReceiveItem(const FRancItemInstance& ItemInstance) const
  369. {
  370.     // Check if adding this item would exceed the max number of unique items
  371.     if (!ContainsItem(ItemInstance.ItemId) && Items.Num() >= MaxNumItems)
  372.     {
  373.         UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: Inventory is full."));
  374.         return false;
  375.     }
  376.  
  377.     // Calculate the additional weight this item would add
  378.     if (const URancItemData* const ItemData = URancInventoryFunctions::GetItemDataById(ItemInstance.ItemId))
  379.     {
  380.         float AdditionalWeight = ItemData->ItemWeight * ItemInstance.Quantity;
  381.         if (CurrentWeight + AdditionalWeight > MaxWeight)
  382.         {
  383.             UE_LOG(LogTemp, Warning, TEXT("Cannot receive item: Exceeds max weight."));
  384.             return false;
  385.         }
  386.     }
  387.     else
  388.     {
  389.         UE_LOG(LogTemp, Warning, TEXT("Could not find item data for item: %s"), *ItemInstance.ItemId.ToString());
  390.         return false;
  391.     }
  392.  
  393.     return true;
  394. }
  395.  
  396. bool URancItemContainerComponent::ContainsItem(const FGameplayTag& ItemId, int32 Quantity) const
  397. {
  398.     int32 TotalQuantity = 0;
  399.     for (const auto& Item : Items)
  400.     {
  401.         if (Item.ItemId == ItemId)
  402.         {
  403.             TotalQuantity += Item.Quantity;
  404.             if (TotalQuantity >= Quantity)
  405.             {
  406.                 return true;
  407.             }
  408.         }
  409.     }
  410.     return false;
  411. }
  412.  
  413. int32 URancItemContainerComponent::GetCurrentItemCount() const
  414. {
  415.     int32 Count = 0;
  416.     for (const auto& Item : Items)
  417.     {
  418.         Count += Item.Quantity;
  419.     }
  420.     return Count;
  421. }
  422.  
  423. TArray<FRancItemInstance> URancItemContainerComponent::GetAllItems() const
  424. {
  425.     return Items;
  426. }
  427.  
  428. bool URancItemContainerComponent::IsEmpty() const
  429. {
  430.     return Items.Num() == 0;
  431. }
  432.  
  433. void URancItemContainerComponent::UpdateWeight()
  434. {
  435.     CurrentWeight = 0.0f; // Reset weight
  436.     for (const auto& ItemInstance : Items)
  437.     {
  438.         if (const URancItemData* const ItemData = URancInventoryFunctions::GetItemDataById(ItemInstance.ItemId))
  439.         {
  440.             CurrentWeight += ItemData->ItemWeight * ItemInstance.Quantity;
  441.         }
  442.     }
  443.  
  444.     // This example does not handle the case where item data is not found, which might be important for ensuring accuracy.
  445. }
  446.  
  447. void URancItemContainerComponent::CopyItemsToCache()
  448. {
  449.     ItemsCache.Reset();
  450.     ItemsCache.Reserve(Items.Num());
  451.     for (int i = 0; i < Items.Num(); ++i)
  452.     {
  453.         ItemsCache[Items[i].ItemId] = Items[i].Quantity;
  454.     }
  455. }
  456.  
  457. void URancItemContainerComponent::DetectAndPublishChanges()
  458. {
  459.     // First pass: Update existing items or add new ones, mark them by setting quantity to negative.
  460.     for (FRancItemInstance& NewItem : Items)
  461.     {
  462.         int32* OldQuantity = ItemsCache.Find(NewItem.ItemId);
  463.         if (OldQuantity)
  464.         {
  465.             // Item exists, check for quantity change
  466.             if (*OldQuantity != NewItem.Quantity)
  467.             {
  468.                 if (*OldQuantity < NewItem.Quantity)
  469.                 {
  470.                     OnItemAdded.Broadcast(FRancItemInstance(NewItem.ItemId, NewItem.Quantity - *OldQuantity));
  471.                 }
  472.                 else if (*OldQuantity > NewItem.Quantity)
  473.                 {
  474.                     OnItemRemoved.Broadcast(FRancItemInstance(NewItem.ItemId, *OldQuantity - NewItem.Quantity));
  475.                 }
  476.             }
  477.             // Mark this item as processed by temporarily setting its value to its own negative (or another marker strategy)
  478.             *OldQuantity = -abs(NewItem.Quantity);
  479.         }
  480.         else
  481.         {
  482.             // New item
  483.             OnItemAdded.Broadcast(NewItem);
  484.             ItemsCache.Add(NewItem.ItemId, -abs(NewItem.Quantity)); // Mark as processed
  485.         }
  486.     }
  487.  
  488.     // Second pass: Remove unmarked items (those not set to negative) and revert marks for processed items
  489.     _KeysToRemove.Reset(ItemsCache.Num());
  490.     for (auto& Pair : ItemsCache)
  491.     {
  492.         if (Pair.Value >= 0)
  493.         {
  494.             // Item was not processed (not found in Items), so it has been removed
  495.             OnItemRemoved.Broadcast(FRancItemInstance(Pair.Key, Pair.Value));
  496.             _KeysToRemove.Add(Pair.Key);
  497.         }
  498.         else
  499.         {
  500.             // Revert the mark to reflect the actual quantity
  501.             Pair.Value = -Pair.Value;
  502.         }
  503.     }
  504.  
  505.     // Remove items that were not found in the current Items array
  506.     for (const FGameplayTag& Key : _KeysToRemove)
  507.     {
  508.         ItemsCache.Remove(Key);
  509.     }
  510. }
Advertisement
Add Comment
Please, Sign In to add comment