diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index cfd04dd..c2ea9e0 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -497,7 +497,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add item drop. Added server-authoritative `AgrarianDropItem ItemId Quantity`, inventory stack extraction that preserves dropped stack metadata, pickup spawning in front of the player, and restore-on-spawn-failure handling. -- [ ] Add stack splitting. +- [x] Add stack splitting. Added server-authoritative + `SplitStackByIndex`/`AgrarianSplitStack StackIndex SplitQuantity` support that + validates source slot, quantity, and free slot capacity, preserves stack + metadata, creates a separate stack slot, and avoids immediate re-merge. - [ ] Add item use. - [ ] Add equipment slots if needed. - [ ] Add weight or carry capacity placeholder. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 02fac98..b27d12f 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -106,6 +106,14 @@ of the player, and restores the removed stack if pickup spawning fails. This is the baseline behavior future UI-driven drop flows should call through rather than duplicating inventory mutation logic on the client. +Stack splitting is available through `AgrarianSplitStack StackIndex SplitQuantity` +and `UAgrarianInventoryComponent::SplitStackByIndex`. Splitting is +server-authoritative, validates the source stack index, quantity, and free slot +capacity, copies the source stack metadata, reduces the source quantity, and +creates a separate inventory slot. It intentionally does not re-merge the split +stack through `AddItem`, because the UI needs an actual separate stack to +support later drag/drop and partial drop flows. + ## Time And Environment The MVP gameplay calendar target is: diff --git a/Scripts/verify_stack_splitting.py b/Scripts/verify_stack_splitting.py new file mode 100644 index 0000000..8e72b5d --- /dev/null +++ b/Scripts/verify_stack_splitting.py @@ -0,0 +1,74 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FILES = { + "AgrarianInventoryComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianInventoryComponent.h", + "AgrarianInventoryComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianInventoryComponent.cpp", + "AgrarianGamePlayerController.h": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h", + "AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp", + "TechnicalDesignDocument.md": ROOT / "Docs" / "TechnicalDesignDocument.md", + "InventoryDataModel.md": ROOT / "Docs" / "InventoryDataModel.md", + "AGRARIAN_DEVELOPMENT_ROADMAP.md": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +EXPECTED = { + "AgrarianInventoryComponent.h": [ + "bool SplitStackByIndex(int32 StackIndex, int32 SplitQuantity);", + "void ServerSplitStackByIndex(int32 StackIndex, int32 SplitQuantity);", + ], + "AgrarianInventoryComponent.cpp": [ + "bool UAgrarianInventoryComponent::SplitStackByIndex", + "Items.IsValidIndex(StackIndex)", + "Items.Num() >= MaxSlots", + "SplitQuantity >= SourceStack.Quantity", + "FAgrarianItemStack SplitStack = SourceStack;", + "SplitStack.Quantity = SplitQuantity;", + "SourceStack.Quantity -= SplitQuantity;", + "Items.Add(SplitStack);", + "BroadcastInventoryChanged();", + "void UAgrarianInventoryComponent::ServerSplitStackByIndex_Implementation", + ], + "AgrarianGamePlayerController.h": [ + "void AgrarianSplitStack(int32 StackIndex, int32 SplitQuantity);", + "void ServerAgrarianSplitStack(int32 StackIndex, int32 SplitQuantity);", + ], + "AgrarianGamePlayerController.cpp": [ + "void AAgrarianGamePlayerController::AgrarianSplitStack(int32 StackIndex, int32 SplitQuantity)", + "Usage: AgrarianSplitStack ", + "ServerAgrarianSplitStack(StackIndex, SplitQuantity);", + "void AAgrarianGamePlayerController::ServerAgrarianSplitStack_Implementation", + "InventoryComponent->SplitStackByIndex(StackIndex, SplitQuantity)", + "Split %d items from stack %d.", + ], + "TechnicalDesignDocument.md": [ + "`AgrarianSplitStack StackIndex SplitQuantity`", + "creates a separate inventory slot", + "does not re-merge", + ], + "InventoryDataModel.md": [ + "Stack splitting:", + "server moves a requested quantity into a new stack when slots", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add stack splitting.", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in EXPECTED[label]: + if snippet not in text: + missing.append(f"{label}: {snippet}") + + if missing: + raise RuntimeError("Stack splitting verification failed: " + "; ".join(missing)) + + print("Agrarian stack splitting verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 8d0058d..c25ec59 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -139,6 +139,17 @@ void AAgrarianGamePlayerController::AgrarianDropItem(FName ItemId, int32 Quantit ServerAgrarianDropItem(ItemId, Quantity); } +void AAgrarianGamePlayerController::AgrarianSplitStack(int32 StackIndex, int32 SplitQuantity) +{ + if (StackIndex < 0 || SplitQuantity <= 0) + { + ClientMessage(TEXT("Usage: AgrarianSplitStack ")); + return; + } + + ServerAgrarianSplitStack(StackIndex, SplitQuantity); +} + void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) { ServerAgrarianTravel(FVector(X, Y, Z)); @@ -268,6 +279,26 @@ void AAgrarianGamePlayerController::ServerAgrarianDropItem_Implementation(FName ClientMessage(FString::Printf(TEXT("Dropped %d x %s."), DroppedStack.Quantity, *ItemId.ToString())); } +void AAgrarianGamePlayerController::ServerAgrarianSplitStack_Implementation(int32 StackIndex, int32 SplitQuantity) +{ + AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); + UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter ? AgrarianCharacter->GetInventoryComponent() : nullptr; + if (!InventoryComponent) + { + ClientMessage(TEXT("No Agrarian inventory component found.")); + return; + } + + if (InventoryComponent->SplitStackByIndex(StackIndex, SplitQuantity)) + { + ClientMessage(FString::Printf(TEXT("Split %d items from stack %d."), SplitQuantity, StackIndex)); + } + else + { + ClientMessage(FString::Printf(TEXT("Could not split %d items from stack %d."), SplitQuantity, StackIndex)); + } +} + void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector Destination) { APawn* ControlledPawn = GetPawn(); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index a6da9de..ee01864 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -69,6 +69,9 @@ public: UFUNCTION(Exec) void AgrarianDropItem(FName ItemId, int32 Quantity); + UFUNCTION(Exec) + void AgrarianSplitStack(int32 StackIndex, int32 SplitQuantity); + UFUNCTION(Exec) void AgrarianTravel(float X, float Y, float Z); @@ -91,6 +94,9 @@ protected: UFUNCTION(Server, Reliable) void ServerAgrarianDropItem(FName ItemId, int32 Quantity); + UFUNCTION(Server, Reliable) + void ServerAgrarianSplitStack(int32 StackIndex, int32 SplitQuantity); + UFUNCTION(Server, Reliable) void ServerAgrarianTravel(FVector Destination); }; diff --git a/Source/AgrarianGame/AgrarianInventoryComponent.cpp b/Source/AgrarianGame/AgrarianInventoryComponent.cpp index 1c22423..64fc31e 100644 --- a/Source/AgrarianGame/AgrarianInventoryComponent.cpp +++ b/Source/AgrarianGame/AgrarianInventoryComponent.cpp @@ -115,6 +115,27 @@ bool UAgrarianInventoryComponent::ExtractItem(FName ItemId, int32 Quantity, FAgr return true; } +bool UAgrarianInventoryComponent::SplitStackByIndex(int32 StackIndex, int32 SplitQuantity) +{ + if (!GetOwner() || !GetOwner()->HasAuthority() || SplitQuantity <= 0 || !Items.IsValidIndex(StackIndex) || Items.Num() >= MaxSlots) + { + return false; + } + + FAgrarianItemStack& SourceStack = Items[StackIndex]; + if (!SourceStack.IsValidStack() || SplitQuantity >= SourceStack.Quantity) + { + return false; + } + + FAgrarianItemStack SplitStack = SourceStack; + SplitStack.Quantity = SplitQuantity; + SourceStack.Quantity -= SplitQuantity; + Items.Add(SplitStack); + BroadcastInventoryChanged(); + return true; +} + void UAgrarianInventoryComponent::ServerAddItem_Implementation(const FAgrarianItemStack& Stack) { AddItem(Stack); @@ -125,6 +146,11 @@ void UAgrarianInventoryComponent::ServerRemoveItem_Implementation(FName ItemId, RemoveItem(ItemId, Quantity); } +void UAgrarianInventoryComponent::ServerSplitStackByIndex_Implementation(int32 StackIndex, int32 SplitQuantity) +{ + SplitStackByIndex(StackIndex, SplitQuantity); +} + void UAgrarianInventoryComponent::OnRep_Items() { BroadcastInventoryChanged(); diff --git a/Source/AgrarianGame/AgrarianInventoryComponent.h b/Source/AgrarianGame/AgrarianInventoryComponent.h index 5ffefbe..c9aa860 100644 --- a/Source/AgrarianGame/AgrarianInventoryComponent.h +++ b/Source/AgrarianGame/AgrarianInventoryComponent.h @@ -46,12 +46,18 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory") bool ExtractItem(FName ItemId, int32 Quantity, FAgrarianItemStack& OutStack); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory") + bool SplitStackByIndex(int32 StackIndex, int32 SplitQuantity); + UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Inventory") void ServerAddItem(const FAgrarianItemStack& Stack); UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Inventory") void ServerRemoveItem(FName ItemId, int32 Quantity); + UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Inventory") + void ServerSplitStackByIndex(int32 StackIndex, int32 SplitQuantity); + protected: UFUNCTION() void OnRep_Items();