// Fill out your copyright notice in the Description page of Project Settings. #include "InventoryComponent.h" #include "ItemDataAsset.h" #include "Net/UnrealNetwork.h" UInventoryComponent::UInventoryComponent() { PrimaryComponentTick.bCanEverTick = false; // Necessaire pour que InitializeComponent() soit appele. bWantsInitializeComponent = true; // Le contenu appartient au serveur et descend vers les clients. SetIsReplicatedByDefault(true); } void UInventoryComponent::InitializeComponent() { Super::InitializeComponent(); EnsureSlotCount(); SelectedHotbarIndex = FMath::Clamp(SelectedHotbarIndex, 0, FMath::Max(0, GetHotbarSlotCount() - 1)); } void UInventoryComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(UInventoryComponent, Slots); } void UInventoryComponent::OnRep_Slots() { // Pas de BroadcastChanged() ici : le regroupement differe ne sert qu'a // eviter vingt reconstructions d'UI pendant un "Tout ranger" execute en // local. La replication, elle, arrive deja groupee -- le serveur a fini son // lot avant d'envoyer quoi que ce soit. OnInventoryChanged.Broadcast(); } /** * SelectedHotbarIndex n'est volontairement PAS replique : le slot actif est une * intention purement locale, chaque joueur choisit le sien et personne d'autre * n'a besoin de le connaitre. Le jour ou l'objet en main devra etre visible par * les autres, ce sera une propriete du PAWN -- ce qu'on tient dans sa main est * une donnee de corps, pas de sac. */ void UInventoryComponent::SelectHotbarSlot(int32 Index) { const int32 Count = GetHotbarSlotCount(); if (Index < 0 || Index >= Count || Index == SelectedHotbarIndex) { return; } SelectedHotbarIndex = Index; OnSelectedHotbarSlotChanged.Broadcast(SelectedHotbarIndex); } void UInventoryComponent::CycleHotbarSelection(int32 Delta) { const int32 Count = GetHotbarSlotCount(); if (Count <= 0 || Delta == 0) { return; } // Modulo positif : en C++, -1 % 6 vaut -1 et non 5. Sans le double // modulo, reculer depuis le premier slot donnerait un index negatif. const int32 NewIndex = ((SelectedHotbarIndex + Delta) % Count + Count) % Count; SelectHotbarSlot(NewIndex); } FInventorySlot UInventoryComponent::GetSelectedSlot() const { return Slots.IsValidIndex(SelectedHotbarIndex) ? Slots[SelectedHotbarIndex] : FInventorySlot(); } void UInventoryComponent::EnsureSlotCount() { const int32 Target = GetTotalSlotCount(); if (Slots.Num() != Target) { Slots.SetNum(Target); } } int32 UInventoryComponent::AddItem(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) { if (!Item || Quantity <= 0) { return Quantity; } EnsureSlotCount(); const int32 MaxStack = Item->GetEffectiveMaxStack(); const int32 MaxUses = FMath::Max(1, Item->MaxUses); // -1 = "plein" : un objet neuf, ou un ajout qui ne precise pas d'etat. const int32 StartingUses = (RemainingUses < 0) ? MaxUses : FMath::Clamp(RemainingUses, 0, MaxUses); int32 Remaining = Quantity; // Premiere passe : completer les piles deja entamees. On le fait avant // d'ouvrir un nouveau slot, sinon on gaspillerait des emplacements en // laissant des piles a moitie vides un peu partout. for (FInventorySlot& Slot : Slots) { if (Remaining <= 0) { break; } if (Slot.Item == Item && Slot.Quantity < MaxStack) { const int32 Added = FMath::Min(Remaining, MaxStack - Slot.Quantity); Slot.Quantity += Added; Remaining -= Added; } } // Deuxieme passe : ouvrir de nouveaux slots. La BARRE RAPIDE est servie en // premier, pour qu'un objet ramasse soit immediatement utilisable sans // passer par l'inventaire. auto FillEmptyRange = [&](int32 First, int32 Last) { for (int32 Index = First; Index < Last && Remaining > 0; ++Index) { FInventorySlot& Slot = Slots[Index]; if (Slot.IsEmpty()) { const int32 Added = FMath::Min(Remaining, MaxStack); Slot.Item = Item; Slot.Quantity = Added; Slot.RemainingUses = StartingUses; Remaining -= Added; } } }; // Inverse ces deux lignes si tu veux un jour servir l'inventaire d'abord. const int32 BackpackStart = GetBackpackStartIndex(); FillEmptyRange(0, BackpackStart); // Barre pleine : on deborde dans l'inventaire. FillEmptyRange(BackpackStart, Slots.Num()); if (Remaining < Quantity) { BroadcastChanged(); } return Remaining; } int32 UInventoryComponent::RemoveItem(UItemDataAsset* Item, int32 Quantity) { if (!Item || Quantity <= 0) { return 0; } int32 Remaining = Quantity; // On vide les plus petites piles d'abord : ca libere des slots au plus // vite au lieu de laisser trainer plusieurs piles a moitie pleines. while (Remaining > 0) { FInventorySlot* Best = nullptr; for (FInventorySlot& Slot : Slots) { if (Slot.Item == Item && Slot.Quantity > 0) { if (!Best || Slot.Quantity < Best->Quantity) { Best = &Slot; } } } if (!Best) { break; } const int32 Taken = FMath::Min(Remaining, Best->Quantity); Best->Quantity -= Taken; Remaining -= Taken; if (Best->Quantity <= 0) { Best->Clear(); } } const int32 Removed = Quantity - Remaining; if (Removed > 0) { BroadcastChanged(); } return Removed; } void UInventoryComponent::Server_TransferAllTo_Implementation(UInventoryComponent* From, UInventoryComponent* To, int32 FirstIndex) { if (From) { From->TransferAllTo(To, FirstIndex); } } void UInventoryComponent::Server_ConsumeUse_Implementation(int32 SlotIndex) { ConsumeUse(SlotIndex); } bool UInventoryComponent::ConsumeUse(int32 SlotIndex) { if (!HasInventoryAuthority()) { Server_ConsumeUse(SlotIndex); return true; } if (!Slots.IsValidIndex(SlotIndex)) { return false; } FInventorySlot& Slot = Slots[SlotIndex]; if (Slot.IsEmpty()) { return false; } const int32 MaxUses = FMath::Max(1, Slot.Item->MaxUses); // Objet a usage unique : un exemplaire disparait, point. if (MaxUses <= 1) { Slot.Quantity -= 1; if (Slot.Quantity <= 0) { Slot.Clear(); } BroadcastChanged(); return true; } Slot.RemainingUses -= 1; if (Slot.RemainingUses <= 0) { Slot.Quantity -= 1; if (Slot.Quantity <= 0) { Slot.Clear(); } else { // Cas theorique : un objet a charges ne s'empile pas. On recharge // quand meme l'exemplaire suivant, au cas ou une pile serait // construite autrement un jour. Slot.RemainingUses = MaxUses; } } BroadcastChanged(); return true; } int32 UInventoryComponent::GetItemCount(const UItemDataAsset* Item) const { if (!Item) { return 0; } int32 Total = 0; for (const FInventorySlot& Slot : Slots) { if (Slot.Item == Item) { Total += Slot.Quantity; } } return Total; } int32 UInventoryComponent::GetRoomFor(const UItemDataAsset* Item) const { if (!Item) { return 0; } const int32 MaxStack = Item->GetEffectiveMaxStack(); int32 Room = 0; for (const FInventorySlot& Slot : Slots) { if (Slot.IsEmpty()) { Room += MaxStack; } else if (Slot.Item == Item) { Room += FMath::Max(0, MaxStack - Slot.Quantity); } } return Room; } int32 UInventoryComponent::GetUsedSlotCount() const { int32 Used = 0; for (const FInventorySlot& Slot : Slots) { if (!Slot.IsEmpty()) { ++Used; } } return Used; } int32 UInventoryComponent::FindFirstEmptySlot() const { for (int32 Index = 0; Index < Slots.Num(); ++Index) { if (Slots[Index].IsEmpty()) { return Index; } } return INDEX_NONE; } bool UInventoryComponent::TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken) { OutTaken.Clear(); if (Quantity <= 0 || !Slots.IsValidIndex(SlotIndex)) { return false; } FInventorySlot& Slot = Slots[SlotIndex]; if (Slot.IsEmpty()) { return false; } const int32 Taken = FMath::Min(Quantity, Slot.Quantity); OutTaken.Item = Slot.Item; OutTaken.Quantity = Taken; // L'etat d'usure part avec l'objet : jeter une canette entamee puis la // reprendre ne doit pas la remplir. OutTaken.RemainingUses = Slot.RemainingUses; Slot.Quantity -= Taken; if (Slot.Quantity <= 0) { Slot.Clear(); } BroadcastChanged(); return true; } bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity) { return TransferSlot(this, FromIndex, this, ToIndex, Quantity); } bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex) { // MAX_int32 depasse forcement la pile : TransferSlot bascule alors sur le // deplacement total, echange compris. return TransferSlot(this, FromIndex, this, ToIndex, MAX_int32); } bool UInventoryComponent::ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity) { if (Quantity <= 0 || From.IsEmpty()) { return false; } const int32 MaxStack = From.Item->GetEffectiveMaxStack(); const bool bTotal = (Quantity >= From.Quantity); if (To.IsEmpty()) { // Deplacement total : la pile entiere part, quelle que soit sa taille -- // on ne la tronque pas a MaxStack, elle etait deja legale a la source. if (bTotal) { To = From; From.Clear(); return true; } const int32 Transferred = FMath::Min(Quantity, MaxStack); To.Item = From.Item; To.Quantity = Transferred; // L'usure suit l'objet : diviser une pile de canettes entamees donne // deux piles au meme niveau, jamais une neuve. To.RemainingUses = From.RemainingUses; From.Quantity -= Transferred; return true; } // Meme objet : on fusionne dans la limite de la pile. Pour un objet a // charges, GetEffectiveMaxStack vaut 1, donc on tombe toujours sur // l'echange -- deux canettes entamees ne fusionnent jamais. if (To.Item == From.Item) { const int32 Wanted = bTotal ? From.Quantity : Quantity; const int32 Transferable = FMath::Min(Wanted, MaxStack - To.Quantity); if (Transferable <= 0) { // Cible pleine. Sur un deplacement total on echange plutot que de // ne rien faire, sinon le geste du joueur passerait pour un bug ; // sur un deplacement partiel on refuse, echanger la moitie d'une // pile contre autre chose n'ayant aucun sens. if (!bTotal) { return false; } Swap(From, To); return true; } To.Quantity += Transferable; From.Quantity -= Transferable; if (From.Quantity <= 0) { From.Clear(); } return true; } // Objets differents : echange, et seulement sur un deplacement total. Il // n'existe aucune facon sensee d'echanger la moitie d'une pile contre autre // chose -- on refuse plutot que d'inventer un comportement incomprehensible. if (!bTotal) { return false; } Swap(From, To); return true; } bool UInventoryComponent::HasInventoryAuthority() const { const AActor* Owner = GetOwner(); return Owner && Owner->HasAuthority(); } UInventoryComponent* UInventoryComponent::FindNetProxy(UInventoryComponent* A, UInventoryComponent* B) { auto IsLocalPawnInventory = [](const UInventoryComponent* Inventory) { const APawn* OwnerPawn = Inventory ? Cast(Inventory->GetOwner()) : nullptr; return OwnerPawn && OwnerPawn->IsLocallyControlled(); }; if (IsLocalPawnInventory(A)) { return A; } return IsLocalPawnInventory(B) ? B : nullptr; } bool UInventoryComponent::TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity) { if (!From || !To || Quantity <= 0) { return false; } // Chez un client : on ne touche a rien, on demande. Le contenu redescendra // par OnRep_Slots, qui rediffuse OnInventoryChanged et rafraichit l'UI. if (!From->HasInventoryAuthority()) { UInventoryComponent* Proxy = FindNetProxy(From, To); if (!Proxy) { UE_LOG(LogTemp, Warning, TEXT("UInventoryComponent::TransferSlot : aucun inventaire de joueur local dans l'echange, demande abandonnee.")); return false; } Proxy->Server_TransferSlot(From, FromIndex, To, ToIndex, Quantity); // True signifie ici "demande partie", pas "contenu modifie". Les // appelants s'en servent pour savoir si le geste a ete pris en compte, // ce qui reste exact. return true; } return ApplyTransferSlot(From, FromIndex, To, ToIndex, Quantity); } void UInventoryComponent::Server_TransferSlot_Implementation(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity) { // From et To arrivent par le reseau : ils peuvent etre nuls si l'acteur a // ete detruit entre l'envoi et l'arrivee -- un coffre casse, un joueur // deconnecte. ApplyTransferSlot les revalide de toute facon. ApplyTransferSlot(From, FromIndex, To, ToIndex, Quantity); } bool UInventoryComponent::ApplyTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity) { if (!From || !To || Quantity <= 0) { return false; } // Meme inventaire et meme case : rien a faire. Le test doit porter sur le // couple et non sur l'index seul, deux inventaires differents ayant tous // les deux une case 3. if (From == To && FromIndex == ToIndex) { return false; } if (!From->Slots.IsValidIndex(FromIndex) || !To->Slots.IsValidIndex(ToIndex)) { return false; } // Deux references dans le meme tableau quand From == To : legal ici parce // qu'aucun redimensionnement n'a lieu entre les deux acces. if (!ApplySlotMove(From->Slots[FromIndex], To->Slots[ToIndex], Quantity)) { return false; } From->BroadcastChanged(); if (To != From) { To->BroadcastChanged(); } return true; } int32 UInventoryComponent::PushSlotInto(int32 SlotIndex, UInventoryComponent& To) { if (!Slots.IsValidIndex(SlotIndex)) { return 0; } FInventorySlot& Slot = Slots[SlotIndex]; if (Slot.IsEmpty()) { return 0; } const int32 Left = To.AddItem(Slot.Item, Slot.Quantity, Slot.RemainingUses); const int32 Moved = Slot.Quantity - Left; if (Moved <= 0) { return 0; } Slot.Quantity = Left; if (Slot.Quantity <= 0) { Slot.Clear(); } return Moved; } int32 UInventoryComponent::QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To) { if (!From || !To || From == To) { return 0; } if (!From->HasInventoryAuthority()) { if (UInventoryComponent* Proxy = FindNetProxy(From, To)) { Proxy->Server_QuickTransferSlot(From, FromIndex, To); // Le nombre reellement deplace n'est connu que du serveur. On rend // une valeur non nulle pour que l'appelant sache que le clic a ete // pris en compte ; l'affichage, lui, viendra de OnRep_Slots. return 1; } return 0; } const int32 Moved = From->PushSlotInto(FromIndex, *To); if (Moved > 0) { From->BroadcastChanged(); } return Moved; } void UInventoryComponent::Server_QuickTransferSlot_Implementation(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To) { QuickTransferSlot(From, FromIndex, To); } int32 UInventoryComponent::TransferAllTo(UInventoryComponent* To, int32 FirstIndex) { if (!To || To == this) { return 0; } if (!HasInventoryAuthority()) { if (UInventoryComponent* Proxy = FindNetProxy(this, To)) { Proxy->Server_TransferAllTo(this, To, FirstIndex); return 1; } return 0; } To->BeginBatch(); int32 Moved = 0; for (int32 Index = FMath::Max(0, FirstIndex); Index < Slots.Num(); ++Index) { Moved += PushSlotInto(Index, *To); } To->EndBatch(); if (Moved > 0) { BroadcastChanged(); } return Moved; } void UInventoryComponent::ConfigureAsContainer(int32 NewSlotCount) { // Aucune barre rapide : GetBackpackStartIndex() vaut alors 0, et la grille // affiche la totalite du conteneur au lieu d'en masquer les dix premieres // cases. HotbarSlotCount = 0; BonusSlotCount = 0; BackpackSlotCount = FMath::Max(0, NewSlotCount); SelectedHotbarIndex = 0; EnsureSlotCount(); } void UInventoryComponent::BroadcastChanged() { if (BatchDepth > 0) { bBatchDirty = true; return; } OnInventoryChanged.Broadcast(); } void UInventoryComponent::BeginBatch() { ++BatchDepth; } void UInventoryComponent::EndBatch() { BatchDepth = FMath::Max(0, BatchDepth - 1); if (BatchDepth > 0 || !bBatchDirty) { return; } bBatchDirty = false; OnInventoryChanged.Broadcast(); } bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots) { const int32 NewTotal = FMath::Max(0, HotbarSlotCount + BackpackSlotCount + FMath::Max(0, NewBonusSlots)); // On refuse plutot que de detruire du butin en silence. C'est a l'appelant // (le code qui deséquipe le sac) de decider quoi faire : bloquer le retrait, // ou vider le surplus au sol avant de reessayer. if (GetUsedSlotCount() > NewTotal) { return false; } BonusSlotCount = FMath::Max(0, NewBonusSlots); // Compacter avant de tronquer : sans ca, un objet range dans un slot de // fin serait supprime alors qu'il reste de la place au debut. CompactSlots(); Slots.SetNum(NewTotal); BroadcastChanged(); return true; } void UInventoryComponent::CompactSlots() { const int32 Start = GetBackpackStartIndex(); const int32 Count = Slots.Num() - Start; if (Count <= 0) { return; } // On ne compacte QUE la portion sac. Compacter tout le tableau ferait // remonter des objets du sac dans la barre rapide sans que le joueur // l'ait demande -- et la troncature ne retire de toute facon que la fin. TArray Backpack; Backpack.Append(Slots.GetData() + Start, Count); Backpack.StableSort([](const FInventorySlot& A, const FInventorySlot& B) { return !A.IsEmpty() && B.IsEmpty(); }); for (int32 Index = 0; Index < Count; ++Index) { Slots[Start + Index] = Backpack[Index]; } }