Initial commit: Item Interaction Ecosystem plugin (UE5.7)
Server-authoritative, data-driven item/inventory/interaction/carry/equipment/ world-slot/placement/crafting framework. 8 modules, compiles against UE 5.7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Unreal Engine plugin build artifacts
|
||||||
|
Binaries/
|
||||||
|
Intermediate/
|
||||||
|
Saved/
|
||||||
|
DerivedDataCache/
|
||||||
|
|
||||||
|
# IDE / OS
|
||||||
|
.vs/
|
||||||
|
*.VC.db
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
.idea/
|
||||||
|
*.pdb
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
10
Docs/ExampleData/DT_ItemFeatures.csv
Normal file
10
Docs/ExampleData/DT_ItemFeatures.csv
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Name,ItemId,Feature,Param,Value
|
||||||
|
raw_meat_decay,raw_meat,Feature.Rotting,DecayRatePerHour,0.08
|
||||||
|
raw_meat_rotten,raw_meat,Feature.Rotting,RottenItemId,rotten_meat
|
||||||
|
raw_meat_smell,raw_meat,Feature.Smell,SmellWhenRotten,1.0
|
||||||
|
flashlight_drain,flashlight,Feature.Battery,DrainPerSecond,0.003
|
||||||
|
flashlight_break,flashlight,Feature.Durability,BreakOnImpactVelocity,800
|
||||||
|
fuel_canister_max,fuel_canister,Feature.Fuel,MaxFuel,20
|
||||||
|
fuel_canister_flammable,fuel_canister,Feature.Flammable,ExplosionRadius,500
|
||||||
|
glass_bottle_noise,glass_bottle,Feature.NoiseOnDrop,Radius,1200
|
||||||
|
car_battery_carry,car_battery,Feature.Installable,MoveSpeedMultiplier,0.6
|
||||||
|
6
Docs/ExampleData/DT_ItemInteractions.csv
Normal file
6
Docs/ExampleData/DT_ItemInteractions.csv
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Name,ItemId,Context,ActionTag,InteractionMode,Priority,bRequiresHold,HoldDuration,ActionDuration,Requirement,Result
|
||||||
|
car_battery_install,car_battery,Held+Vehicle,Action.Install,InstallIntoWorldSlot,1000,false,0,3,,InstallIntoSlot
|
||||||
|
car_battery_install_gen,car_battery,Held+Generator,Action.Install,InstallIntoWorldSlot,1000,false,0,3,,InstallIntoSlot
|
||||||
|
fuel_pour,fuel_canister,Held+Generator,Action.UseHeldOnTarget,UseHeldOnTarget,1000,true,0.3,5,FuelAmount>0,TransferFuel
|
||||||
|
flashlight_equip,flashlight,Inventory,Action.Equip,EquipFromInventory,800,false,0,0,BatteryCharge>0,EquipItem
|
||||||
|
raw_meat_bait,raw_meat,Held+Trap,Action.Install,InstallIntoWorldSlot,900,false,0,2,,InstallIntoSlot
|
||||||
|
8
Docs/ExampleData/DT_ItemProperties.csv
Normal file
8
Docs/ExampleData/DT_ItemProperties.csv
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Name,ItemId,PropertyTag,DefaultValue,Min,Max,ReplicationPolicy,bSave
|
||||||
|
raw_meat_freshness,raw_meat,Property.Freshness,1.0,0,1,OwnerOnly,true
|
||||||
|
raw_meat_contamination,raw_meat,Property.Contamination,0,0,1,OwnerOnly,true
|
||||||
|
flashlight_battery,flashlight,Property.BatteryCharge,1.0,0,1,OwnerOnly,true
|
||||||
|
axe_durability,axe,Property.Durability,1.0,0,1,OwnerOnly,true
|
||||||
|
fuel_canister_fuel,fuel_canister,Property.FuelAmount,10.0,0,20,OwnerOnly,true
|
||||||
|
car_battery_charge,car_battery,Property.Charge,0.65,0,1,OwnerOnly,true
|
||||||
|
generator_fuel,generator_small,Property.FuelAmount,0,0,40,Everyone,true
|
||||||
|
3
Docs/ExampleData/DT_ItemRedirects.csv
Normal file
3
Docs/ExampleData/DT_ItemRedirects.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Name,OldItemId,NewItemId
|
||||||
|
redirect_medkit,medkit,medkit_small
|
||||||
|
redirect_flashlight,old_flashlight,flashlight
|
||||||
|
11
Docs/ExampleData/DT_Items.csv
Normal file
11
Docs/ExampleData/DT_Items.csv
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Name,ItemId,DisplayName,Description,ItemType,ItemTags,Icon,WorldMesh,PickupActorClass,EquippedActorClass,MaxStack,Weight,GridSize,bCanStoreInInventory,bCanInstantPickup,bCanWorldCarry,bCanEquip,bCanPlace,bCanInstallIntoWorldSlot,bCanDrop,bIsQuestCritical,DefaultInteraction,AlternativeInteraction,AllowedEquipmentSlots,CompatibleWorldSlots,ItemFeatures,PlacementRules
|
||||||
|
ammo_9mm,ammo_9mm,9mm Ammo,,Item.Ammo.9mm,,,,,,60,0.01,"(X=1,Y=1)",true,true,false,false,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
medkit_small,medkit_small,Small Medkit,,Item.Medical.Medkit,,,,,,5,0.5,"(X=1,Y=1)",true,true,false,false,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
flashlight,flashlight,Flashlight,,Item.Tool.Flashlight,,,,,,1,0.4,"(X=1,Y=2)",true,true,true,true,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
raw_meat,raw_meat,Raw Meat,,Item.Food.Meat,,,,,,10,0.3,"(X=1,Y=1)",true,true,false,false,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
rotten_meat,rotten_meat,Rotten Meat,,Item.Food.Meat,,,,,,10,0.3,"(X=1,Y=1)",true,true,false,false,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
fuel_canister,fuel_canister,Fuel Canister,,Item.Resource.Fuel,,,,,,1,5,"(X=2,Y=2)",true,true,true,false,false,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
car_battery,car_battery,Car Battery,,Item.VehiclePart.Battery,,,,,,1,12,"(X=2,Y=2)",false,false,true,false,false,true,true,false,Action.Carry,,,"(GameplayTags=((TagName=""Vehicle.Socket.Battery""),(TagName=""Generator.Socket.Battery"")))",,
|
||||||
|
generator_small,generator_small,Portable Generator,,Item.Device.Generator,,,,,,1,30,"(X=3,Y=3)",false,false,true,false,true,false,true,false,Action.Carry,,,,,
|
||||||
|
trap_small,trap_small,Small Trap,,Item.Trap,,,,,,3,1.0,"(X=2,Y=1)",true,true,false,false,true,false,true,false,Action.Pickup.Instant,,,,,
|
||||||
|
key_bunker_a,key_bunker_a,Bunker Key A,,Item.Key,,,,,,1,0.05,"(X=1,Y=1)",true,true,false,false,false,false,false,true,Action.Pickup.Instant,,,,,
|
||||||
|
65
ItemEcosystem.uplugin
Normal file
65
ItemEcosystem.uplugin
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"Version": 1,
|
||||||
|
"VersionName": "0.1.0",
|
||||||
|
"FriendlyName": "Item Interaction Ecosystem",
|
||||||
|
"Description": "Universal, server-authoritative, data-driven item / inventory / interaction / placement ecosystem framework for UE5.",
|
||||||
|
"Category": "Gameplay",
|
||||||
|
"CreatedBy": "ExByte Studios",
|
||||||
|
"CreatedByURL": "",
|
||||||
|
"DocsURL": "",
|
||||||
|
"MarketplaceURL": "",
|
||||||
|
"SupportURL": "",
|
||||||
|
"CanContainContent": true,
|
||||||
|
"IsBetaVersion": true,
|
||||||
|
"IsExperimentalVersion": false,
|
||||||
|
"Installed": false,
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Name": "ItemCore",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "PreDefault"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemDatabase",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemInventory",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemInteraction",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemWorld",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemPlacement",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemDebug",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ItemDeveloper",
|
||||||
|
"Type": "Editor",
|
||||||
|
"LoadingPhase": "PostEngineInit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Plugins": [
|
||||||
|
{
|
||||||
|
"Name": "EnhancedInput",
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
138
README.md
Normal file
138
README.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Item Interaction Ecosystem (UE5 Plugin)
|
||||||
|
|
||||||
|
Universal, server-authoritative, data-driven framework for items, inventory,
|
||||||
|
interaction, carrying, equipment, world-slot installation, placement and item
|
||||||
|
features. Built for co-op horror / survival / extraction / puzzle projects.
|
||||||
|
Tested compiling against **Unreal Engine 5.7**.
|
||||||
|
|
||||||
|
> Status: all C++ runtime + editor modules implemented and compiling (spec stages
|
||||||
|
> 1–11). Binary content (UMG widgets, the test map, and the DataTable *assets*
|
||||||
|
> themselves) must be authored in-editor — see "Content to author" below. Example
|
||||||
|
> data is provided as importable CSV under `Docs/ExampleData/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | Type | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `ItemCore` | Runtime | Base structs (`FItemInstanceData`, `FItemRuntimeProperties`, lock data), enums, native gameplay tags, logs |
|
||||||
|
| `ItemDatabase` | Runtime | `FItemDefinitionRow` + aux rows, `UItemDatabaseSubsystem` (loading, lookup, redirects, validation), Project Settings |
|
||||||
|
| `ItemInventory` | Runtime | `UInventoryComponent` (FastArray replication), containers, stacking, `UItemFeatureLibrary` |
|
||||||
|
| `ItemInteraction` | Runtime | Interactable/trace components, `UInteractionResolver`, `UItemInteractionComponent` (server requests + validation) |
|
||||||
|
| `ItemWorld` | Runtime | `APickupItemActor`, carry (`UHeldItemComponent`), equipment, world slots, `UWorldInteractionComponent`, storage actor |
|
||||||
|
| `ItemPlacement` | Runtime | `UPlacementPreviewComponent` (ghost preview, rules, server confirm) |
|
||||||
|
| `ItemDebug` | Runtime | `ItemEco.*` console commands |
|
||||||
|
| `ItemDeveloper` | Editor | Tools menu → **Validate Item Data** |
|
||||||
|
|
||||||
|
Design principle (section 3): the **client never mutates item state** — it sends
|
||||||
|
requests; the server re-validates and replicates results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Enable the plugin** (already referenced by `InventorySystem.uproject`). Build the
|
||||||
|
`InventorySystemEditor` target.
|
||||||
|
2. **Create the DataTables** (Content Browser → Miscellaneous → Data Table):
|
||||||
|
- `DT_Items` — row struct `FItemDefinitionRow`
|
||||||
|
- `DT_ItemProperties` — `FItemPropertyRow`
|
||||||
|
- `DT_ItemFeatures` — `FItemFeatureRow`
|
||||||
|
- `DT_ItemInteractions` — `FItemInteractionRow`
|
||||||
|
- `DT_ItemRedirects` — `FItemRedirectRow`
|
||||||
|
|
||||||
|
Import the CSVs in `Docs/ExampleData/` as a quick start (the flat tables import
|
||||||
|
cleanly; in `DT_Items` the gameplay-tag *container* columns are easiest to finish
|
||||||
|
in-editor).
|
||||||
|
3. **Project Settings → Game → Item Ecosystem** — assign the five tables.
|
||||||
|
4. **Register the gameplay tags** the data uses (Project Settings → GameplayTags), or
|
||||||
|
rely on the native ones shipped in `ItemNativeTags.h` (Property.*, Feature.*,
|
||||||
|
Action.*, Lock.*). Item types / world sockets (e.g. `Vehicle.Socket.Battery`) are
|
||||||
|
project tags you add.
|
||||||
|
|
||||||
|
### Player pawn components
|
||||||
|
|
||||||
|
Add to your pawn:
|
||||||
|
|
||||||
|
- `UInventoryComponent` — set `DefaultContainers` (e.g. a Backpack SlotList + a Hotbar).
|
||||||
|
- `UWorldInteractionComponent` — the executor (use this, not the base, to get carry /
|
||||||
|
drop / equip / install).
|
||||||
|
- `UHeldItemComponent` — for two-handed carry; set `CarrySocket`.
|
||||||
|
- `UEquipmentComponent` — for equip visuals; map `SlotSockets`.
|
||||||
|
- `UInteractionTraceComponent` — local aim trace (drives the prompt).
|
||||||
|
- `UPlacementPreviewComponent` — if you support placement.
|
||||||
|
|
||||||
|
### Input (Enhanced Input, section 24)
|
||||||
|
|
||||||
|
Bind these to call into the components:
|
||||||
|
|
||||||
|
| Key | Call |
|
||||||
|
|---|---|
|
||||||
|
| `E` | `Resolver.GetPrimaryInteraction(pawn, trace.CurrentTarget)` → `WorldInteraction.RequestInteraction(option)` |
|
||||||
|
| `G` | `HeldItemComponent.RequestDrop()` |
|
||||||
|
| LMB | `PlacementPreview.ConfirmPlacement()` (while placing) |
|
||||||
|
| RMB | `PlacementPreview.CancelPlacement()` |
|
||||||
|
| Wheel | `PlacementPreview.RotatePreview(±10)` |
|
||||||
|
| `Tab` | open inventory UI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Console commands (ItemDebug)
|
||||||
|
|
||||||
|
```
|
||||||
|
ItemEco.Give <ItemId> [Qty] Give to the local player inventory
|
||||||
|
ItemEco.Spawn <ItemId> [Qty] Spawn a world pickup in front of the player
|
||||||
|
ItemEco.Clear Clear the player inventory
|
||||||
|
ItemEco.Dump Log all inventory entries
|
||||||
|
ItemEco.Validate Run database validation
|
||||||
|
ItemEco.Rot Force-rot the first inventory item
|
||||||
|
ItemEco.Break Break the first inventory item
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo scenarios → code (section 31)
|
||||||
|
|
||||||
|
- **Ammo pickup** — look at `ammo_9mm` pickup → resolver builds `[E] Pick up 9mm Ammo x12`
|
||||||
|
(capability option) → `WorldInteraction` → `ExecutePickup` adds to inventory, destroys actor.
|
||||||
|
- **Loot crate** — `AStorageContainerActor` (shared inventory) → `[E] Open` (local UI event
|
||||||
|
`OnOpenContainer`) → drag → `RequestMoveItem(storage, …)` → validated transfer.
|
||||||
|
- **Car battery** — carry from world (`ServerHoldWorldItem`, move-speed penalty) → look at
|
||||||
|
vehicle with `UWorldItemSlotComponent(Vehicle.Socket.Battery)` → DT_ItemInteractions row
|
||||||
|
`Held+Vehicle → InstallIntoWorldSlot` → `ExecuteInstall` hands the carried actor to the slot.
|
||||||
|
- **Fuel + generator** — `Held+Generator → UseHeldOnTarget` (hold E). Wire the transfer with
|
||||||
|
`UItemFeatureLibrary::TransferFuel` in a custom `UInteractionAction` or extend
|
||||||
|
`WorldInteraction` (`UseHeldOnTarget` currently routes to action classes).
|
||||||
|
- **Flashlight** — equip from inventory (`UEquipmentComponent::ServerEquip`), item stays in
|
||||||
|
inventory, `EquippedActorClass` spawns; drain with `UItemFeatureLibrary::DrainBattery`.
|
||||||
|
- **Placement trap** — `PlacementPreview.StartPlacement(id)` → ghost + `ValidatePlacement`
|
||||||
|
red/green → confirm → server re-validates → spawns placed actor, removes from inventory.
|
||||||
|
- **Raw meat rotting** — `UItemFeatureLibrary::UpdateInventoryFeatures` (call on container
|
||||||
|
open / save load / world-time pulse). Lazy: nothing ticks per item. At freshness 0 it
|
||||||
|
transforms `raw_meat → rotten_meat`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content to author in-editor (binary assets, not creatable from source)
|
||||||
|
|
||||||
|
- DataTable assets (import the CSVs above).
|
||||||
|
- UMG widgets (section 23): `WBP_InventoryRoot`, `WBP_InteractionPrompt`,
|
||||||
|
`WBP_LootWindow`, `WBP_ActionProgress`, `WBP_PlacementHint`, etc. Bind them to the
|
||||||
|
components' delegates (`OnInventoryChanged`, `OnTargetChanged`, `OnOpenContainer`,
|
||||||
|
`OnPlacementStateChanged`) — UI only displays and sends requests (section 25).
|
||||||
|
- `InventoryInteractionTestMap` (section 30) with pickups, a vehicle (battery/fuel
|
||||||
|
slots), a generator, a storage chest, a placeable trap surface.
|
||||||
|
- Skeletal-mesh sockets named to match `CarrySocket` / equipment `SlotSockets` /
|
||||||
|
world-slot `AttachSocketName`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build / validate
|
||||||
|
|
||||||
|
```
|
||||||
|
"<UE>\Engine\Build\BatchFiles\Build.bat" InventorySystemEditor Win64 Development ^
|
||||||
|
-Project="<path>\InventorySystem.uproject" -WaitMutex
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in-editor: Tools → Item Ecosystem → **Validate Item Data**, or `ItemEco.Validate`
|
||||||
|
in PIE.
|
||||||
25
Source/ItemCore/ItemCore.Build.cs
Normal file
25
Source/ItemCore/ItemCore.Build.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemCore : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemCore(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"NetCore"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Source/ItemCore/Private/ItemCoreModule.cpp
Normal file
22
Source/ItemCore/Private/ItemCoreModule.cpp
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemCoreModule.h"
|
||||||
|
#include "ItemCoreLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemCore);
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "FItemCoreModule"
|
||||||
|
|
||||||
|
void FItemCoreModule::StartupModule()
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemCore, Log, TEXT("ItemCore module started."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemCoreModule::ShutdownModule()
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemCore, Log, TEXT("ItemCore module shut down."));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FItemCoreModule, ItemCore)
|
||||||
77
Source/ItemCore/Private/ItemNativeTags.cpp
Normal file
77
Source/ItemCore/Private/ItemNativeTags.cpp
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
|
||||||
|
namespace ItemEcosystemTags
|
||||||
|
{
|
||||||
|
// --- Runtime property tags ---
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Durability, "Property.Durability");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Freshness, "Property.Freshness");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_RotProgress, "Property.RotProgress");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Temperature, "Property.Temperature");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Wetness, "Property.Wetness");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_FuelAmount, "Property.FuelAmount");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_MaxFuel, "Property.MaxFuel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_BatteryCharge, "Property.BatteryCharge");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Contamination, "Property.Contamination");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_NoiseLevel, "Property.NoiseLevel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_SmellIntensity,"Property.SmellIntensity");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_BloodLevel, "Property.BloodLevel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Radiation, "Property.Radiation");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Poison, "Property.Poison");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_CookedLevel, "Property.CookedLevel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_BurnLevel, "Property.BurnLevel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Quality, "Property.Quality");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_Charge, "Property.Charge");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Property_LastUpdateTime,"Property.LastUpdateTime");
|
||||||
|
|
||||||
|
// --- State tags ---
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(State_Broken, "Item.State.Broken");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(State_Rotten, "Item.State.Rotten");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(State_Wet, "Item.State.Wet");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(State_Contaminated, "Item.State.Contaminated");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(State_Empty, "Item.State.Empty");
|
||||||
|
|
||||||
|
// --- Lock reasons ---
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Carrying, "Lock.Carrying");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Using, "Lock.Using");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Transferring, "Lock.Transferring");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Crafting, "Lock.Crafting");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Installing, "Lock.Installing");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Replicating, "Lock.Replicating");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Lock_Placement, "Lock.Placement");
|
||||||
|
|
||||||
|
// --- Action verbs ---
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Pickup_Instant, "Action.Pickup.Instant");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Pickup, "Action.Pickup");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Open, "Action.Open");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Carry, "Action.Carry");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Drop, "Action.Drop");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Equip, "Action.Equip");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Inspect, "Action.Inspect");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Use, "Action.Use");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Install, "Action.Install");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Remove, "Action.Remove");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_Place, "Action.Place");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Action_UseHeldOnTarget, "Action.UseHeldOnTarget");
|
||||||
|
|
||||||
|
// --- Feature tags ---
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Consumable, "Feature.Consumable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Rotting, "Feature.Rotting");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Durability, "Feature.Durability");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Battery, "Feature.Battery");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Fuel, "Feature.Fuel");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Wetness, "Feature.Wetness");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Temperature, "Feature.Temperature");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_NoiseOnDrop, "Feature.NoiseOnDrop");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Smell, "Feature.Smell");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Container, "Feature.Container");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Equippable, "Feature.Equippable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Throwing, "Feature.Throwing");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Breakable, "Feature.Breakable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Flammable, "Feature.Flammable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Placeable, "Feature.Placeable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Installable, "Feature.Installable");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_QuestItem, "Feature.QuestItem");
|
||||||
|
UE_DEFINE_GAMEPLAY_TAG(Feature_Cursed, "Feature.Cursed");
|
||||||
|
}
|
||||||
7
Source/ItemCore/Private/ItemRuntimeProperties.cpp
Normal file
7
Source/ItemCore/Private/ItemRuntimeProperties.cpp
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
// FItemRuntimeProperties now stores its values in replicable TArrays (see header),
|
||||||
|
// so no custom net serializer is required - this translation unit is intentionally
|
||||||
|
// left empty and kept only to preserve the module's file layout.
|
||||||
|
|
||||||
|
#include "ItemRuntimeProperties.h"
|
||||||
9
Source/ItemCore/Public/ItemCoreLog.h
Normal file
9
Source/ItemCore/Public/ItemCoreLog.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
/** Shared log categories for the Item Interaction Ecosystem (section 33). */
|
||||||
|
ITEMCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogItemCore, Log, All);
|
||||||
19
Source/ItemCore/Public/ItemCoreModule.h
Normal file
19
Source/ItemCore/Public/ItemCoreModule.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ItemCore module.
|
||||||
|
* Foundation of the Item Interaction Ecosystem: base item structs, ids,
|
||||||
|
* runtime properties, location/state enums and shared log categories.
|
||||||
|
* Contains zero gameplay logic - only data types every other module shares.
|
||||||
|
*/
|
||||||
|
class FItemCoreModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override;
|
||||||
|
virtual void ShutdownModule() override;
|
||||||
|
};
|
||||||
95
Source/ItemCore/Public/ItemEnums.h
Normal file
95
Source/ItemCore/Public/ItemEnums.h
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "ItemEnums.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where an item instance currently lives (section 5).
|
||||||
|
* Every state transition is validated server-side through the transaction layer.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EItemLocationType : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
World,
|
||||||
|
Inventory,
|
||||||
|
Container,
|
||||||
|
Held,
|
||||||
|
Equipped,
|
||||||
|
PlacementPreview,
|
||||||
|
Installed,
|
||||||
|
NestedContainer
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Per-property replication scope (section 22). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EItemPropertyReplicationPolicy : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
OwnerOnly,
|
||||||
|
Everyone,
|
||||||
|
NearbyOnly,
|
||||||
|
ServerOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
/** How an item is currently carried/equipped on a character (section 11). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EHeldItemMode : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
EquippedFromInventory,
|
||||||
|
CarryAttached,
|
||||||
|
CarryPhysicsHandle,
|
||||||
|
PlacementPreview
|
||||||
|
};
|
||||||
|
|
||||||
|
/** High level interaction mode resolved per option (section 8.5). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EItemInteractionMode : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
|
||||||
|
InstantPickup,
|
||||||
|
OpenLootUI,
|
||||||
|
PickupToInventory,
|
||||||
|
EquipFromWorld,
|
||||||
|
EquipFromInventory,
|
||||||
|
|
||||||
|
CarryAttached,
|
||||||
|
CarryPhysicsHandle,
|
||||||
|
DropHeldItem,
|
||||||
|
|
||||||
|
Inspect,
|
||||||
|
Use,
|
||||||
|
UseHeldOnTarget,
|
||||||
|
Combine,
|
||||||
|
|
||||||
|
StartPlacement,
|
||||||
|
ConfirmPlacement,
|
||||||
|
CancelPlacement,
|
||||||
|
|
||||||
|
InstallIntoWorldSlot,
|
||||||
|
RemoveFromWorldSlot,
|
||||||
|
|
||||||
|
TransferContainerItem,
|
||||||
|
SplitStack,
|
||||||
|
MergeStack
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Container layout / role (section 7.2). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EInventoryContainerType : uint8
|
||||||
|
{
|
||||||
|
SlotList,
|
||||||
|
Grid,
|
||||||
|
Equipment,
|
||||||
|
Hotbar,
|
||||||
|
VehicleCargo,
|
||||||
|
LootBox,
|
||||||
|
KeyRing,
|
||||||
|
Wallet,
|
||||||
|
CraftingInput,
|
||||||
|
CraftingOutput
|
||||||
|
};
|
||||||
92
Source/ItemCore/Public/ItemInstanceData.h
Normal file
92
Source/ItemCore/Public/ItemInstanceData.h
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "ItemRuntimeProperties.h"
|
||||||
|
#include "ItemLockData.h"
|
||||||
|
#include "ItemInstanceData.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime representation of a single item instance (section 4.3).
|
||||||
|
*
|
||||||
|
* Deliberately tiny: it never copies the static FItemDefinitionRow - it only
|
||||||
|
* stores the stable ItemId plus this instance's mutable state. Resolve the
|
||||||
|
* definition through UItemDatabaseSubsystem when you need icons/meshes/flags.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMCORE_API FItemInstanceData
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
/** Globally unique handle for this concrete stack/instance. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
FGuid InstanceId;
|
||||||
|
|
||||||
|
/** Stable design-time id (e.g. "ammo_9mm"). Never a localized string. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
int32 Quantity = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
FGameplayTagContainer StateTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
FItemRuntimeProperties RuntimeProperties;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
FGuid CurrentContainerId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
int32 SlotIndex = INDEX_NONE;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
EItemLocationType LocationType = EItemLocationType::None;
|
||||||
|
|
||||||
|
/** Bumped when the item schema changes; drives save migration (section 26.2). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
|
||||||
|
int32 ItemDataVersion = 1;
|
||||||
|
|
||||||
|
/** Transient server-side lock; not saved (section 20). */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Transient, Category = "Item")
|
||||||
|
FItemLockData Lock;
|
||||||
|
|
||||||
|
FItemInstanceData() = default;
|
||||||
|
|
||||||
|
explicit FItemInstanceData(FName InItemId, int32 InQuantity = 1)
|
||||||
|
: InstanceId(FGuid::NewGuid())
|
||||||
|
, ItemId(InItemId)
|
||||||
|
, Quantity(InQuantity)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValid() const { return !ItemId.IsNone() && InstanceId.IsValid() && Quantity > 0; }
|
||||||
|
|
||||||
|
void EnsureInstanceId()
|
||||||
|
{
|
||||||
|
if (!InstanceId.IsValid())
|
||||||
|
{
|
||||||
|
InstanceId = FGuid::NewGuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two instances can merge into one stack only when they are the same item,
|
||||||
|
* carry identical per-instance state and neither is locked. MaxStack is
|
||||||
|
* enforced by the inventory layer which has access to the definition.
|
||||||
|
*/
|
||||||
|
bool CanStackWith(const FItemInstanceData& Other) const
|
||||||
|
{
|
||||||
|
return ItemId == Other.ItemId
|
||||||
|
&& !Lock.bLocked && !Other.Lock.bLocked
|
||||||
|
&& StateTags == Other.StateTags
|
||||||
|
&& RuntimeProperties == Other.RuntimeProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const FItemInstanceData& Other) const { return InstanceId == Other.InstanceId; }
|
||||||
|
bool operator!=(const FItemInstanceData& Other) const { return InstanceId != Other.InstanceId; }
|
||||||
|
};
|
||||||
42
Source/ItemCore/Public/ItemLockData.h
Normal file
42
Source/ItemCore/Public/ItemLockData.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemLockData.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side soft lock placed on an item while a transaction owns it (section 20).
|
||||||
|
* Prevents dupes / conflicts: a locked item cannot be moved, taken, dropped,
|
||||||
|
* crafted, deleted or stacked by anyone but the locking actor.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMCORE_API FItemLockData
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
|
||||||
|
bool bLocked = false;
|
||||||
|
|
||||||
|
/** Lock.Carrying / Lock.Using / Lock.Transferring / ... */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
|
||||||
|
FGameplayTag LockReason;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
|
||||||
|
TWeakObjectPtr<AActor> LockedBy;
|
||||||
|
|
||||||
|
/** Server world time (seconds) at which the lock auto-expires; 0 = no expiry. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
|
||||||
|
float LockExpireTime = 0.f;
|
||||||
|
|
||||||
|
bool IsLockedBy(const AActor* Actor) const { return bLocked && LockedBy.Get() == Actor; }
|
||||||
|
|
||||||
|
void Clear()
|
||||||
|
{
|
||||||
|
bLocked = false;
|
||||||
|
LockReason = FGameplayTag();
|
||||||
|
LockedBy = nullptr;
|
||||||
|
LockExpireTime = 0.f;
|
||||||
|
}
|
||||||
|
};
|
||||||
88
Source/ItemCore/Public/ItemNativeTags.h
Normal file
88
Source/ItemCore/Public/ItemNativeTags.h
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "NativeGameplayTags.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native gameplay tags shipped with the framework.
|
||||||
|
*
|
||||||
|
* These cover the built-in behaviour the C++ layer reasons about directly
|
||||||
|
* (lock reasons, the common runtime property tags, the default action verbs).
|
||||||
|
* Project / designer specific tags (item types, world sockets, custom features)
|
||||||
|
* are expected to be declared in the project's tag config or DataTables, not here.
|
||||||
|
*/
|
||||||
|
namespace ItemEcosystemTags
|
||||||
|
{
|
||||||
|
// --- Runtime property tags (section 4.4) ---
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Durability);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Freshness);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_RotProgress);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Temperature);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Wetness);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_FuelAmount);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_MaxFuel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BatteryCharge);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Contamination);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_NoiseLevel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_SmellIntensity);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BloodLevel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Radiation);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Poison);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_CookedLevel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BurnLevel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Quality);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Charge);
|
||||||
|
/** Server world-time (seconds) of the last lazy feature update (section 21.4). */
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_LastUpdateTime);
|
||||||
|
|
||||||
|
// --- State tags (section 4.3 StateTags) ---
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Broken);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Rotten);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Wet);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Contaminated);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Empty);
|
||||||
|
|
||||||
|
// --- Lock reasons (section 20) ---
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Carrying);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Using);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Transferring);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Crafting);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Installing);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Replicating);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Placement);
|
||||||
|
|
||||||
|
// --- Action verbs (section 6.4 / 8.x) ---
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Pickup_Instant);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Pickup);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Open);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Carry);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Drop);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Equip);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Inspect);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Use);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Install);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Remove);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Place);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_UseHeldOnTarget);
|
||||||
|
|
||||||
|
// --- Feature tags (section 6.3) ---
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Consumable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Rotting);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Durability);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Battery);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Fuel);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Wetness);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Temperature);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_NoiseOnDrop);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Smell);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Container);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Equippable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Throwing);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Breakable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Flammable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Placeable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Installable);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_QuestItem);
|
||||||
|
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Cursed);
|
||||||
|
}
|
||||||
190
Source/ItemCore/Public/ItemRuntimeProperties.h
Normal file
190
Source/ItemCore/Public/ItemRuntimeProperties.h
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemRuntimeProperties.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag->value pairs (section 4.4).
|
||||||
|
*
|
||||||
|
* The TЗ sketched these as TMaps, but UHT forbids TMap inside replicated structs,
|
||||||
|
* and runtime properties must replicate (they live in FItemInstanceData). So they
|
||||||
|
* are stored as small replicable arrays instead; the typed accessors below hide
|
||||||
|
* that and behave like a map. N is tiny per item, so linear lookup is fine.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FItemFloatProperty
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
FGameplayTag Tag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
float Value = 0.f;
|
||||||
|
|
||||||
|
bool operator==(const FItemFloatProperty& O) const { return Tag == O.Tag && Value == O.Value; }
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FItemIntProperty
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
FGameplayTag Tag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
int32 Value = 0;
|
||||||
|
|
||||||
|
bool operator==(const FItemIntProperty& O) const { return Tag == O.Tag && Value == O.Value; }
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FItemBoolProperty
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
FGameplayTag Tag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
bool Value = false;
|
||||||
|
|
||||||
|
bool operator==(const FItemBoolProperty& O) const { return Tag == O.Tag && Value == O.Value; }
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FItemStringProperty
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
FGameplayTag Tag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
FString Value;
|
||||||
|
|
||||||
|
bool operator==(const FItemStringProperty& O) const { return Tag == O.Tag && Value == O.Value; }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic, per-instance properties keyed by gameplay tag (section 4.4).
|
||||||
|
* Only data differing from the definition defaults should be stored, to keep
|
||||||
|
* replication/save payloads small. Typed accessors fall back when a key is absent.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMCORE_API FItemRuntimeProperties
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
TArray<FItemFloatProperty> FloatProperties;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
TArray<FItemIntProperty> IntProperties;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
TArray<FItemBoolProperty> BoolProperties;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
|
||||||
|
TArray<FItemStringProperty> StringProperties;
|
||||||
|
|
||||||
|
bool IsEmpty() const
|
||||||
|
{
|
||||||
|
return FloatProperties.Num() == 0 && IntProperties.Num() == 0
|
||||||
|
&& BoolProperties.Num() == 0 && StringProperties.Num() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Float ---
|
||||||
|
float GetFloat(const FGameplayTag& Tag, float Fallback = 0.f) const
|
||||||
|
{
|
||||||
|
for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return P.Value; } }
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
bool HasFloat(const FGameplayTag& Tag) const
|
||||||
|
{
|
||||||
|
for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return true; } }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void SetFloat(const FGameplayTag& Tag, float Value)
|
||||||
|
{
|
||||||
|
for (FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
|
||||||
|
FloatProperties.Add({ Tag, Value });
|
||||||
|
}
|
||||||
|
void RemoveFloat(const FGameplayTag& Tag)
|
||||||
|
{
|
||||||
|
FloatProperties.RemoveAll([&Tag](const FItemFloatProperty& P) { return P.Tag == Tag; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Int ---
|
||||||
|
int32 GetInt(const FGameplayTag& Tag, int32 Fallback = 0) const
|
||||||
|
{
|
||||||
|
for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return P.Value; } }
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
bool HasInt(const FGameplayTag& Tag) const
|
||||||
|
{
|
||||||
|
for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return true; } }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void SetInt(const FGameplayTag& Tag, int32 Value)
|
||||||
|
{
|
||||||
|
for (FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
|
||||||
|
IntProperties.Add({ Tag, Value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bool ---
|
||||||
|
bool GetBool(const FGameplayTag& Tag, bool Fallback = false) const
|
||||||
|
{
|
||||||
|
for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return P.Value; } }
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
bool HasBool(const FGameplayTag& Tag) const
|
||||||
|
{
|
||||||
|
for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return true; } }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void SetBool(const FGameplayTag& Tag, bool Value)
|
||||||
|
{
|
||||||
|
for (FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
|
||||||
|
BoolProperties.Add({ Tag, Value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- String ---
|
||||||
|
FString GetString(const FGameplayTag& Tag, const FString& Fallback = FString()) const
|
||||||
|
{
|
||||||
|
for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return P.Value; } }
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
bool HasString(const FGameplayTag& Tag) const
|
||||||
|
{
|
||||||
|
for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return true; } }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void SetString(const FGameplayTag& Tag, const FString& Value)
|
||||||
|
{
|
||||||
|
for (FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
|
||||||
|
StringProperties.Add({ Tag, Value });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const FItemRuntimeProperties& Other) const
|
||||||
|
{
|
||||||
|
return ArrayEqual(FloatProperties, Other.FloatProperties)
|
||||||
|
&& ArrayEqual(IntProperties, Other.IntProperties)
|
||||||
|
&& ArrayEqual(BoolProperties, Other.BoolProperties)
|
||||||
|
&& ArrayEqual(StringProperties, Other.StringProperties);
|
||||||
|
}
|
||||||
|
bool operator!=(const FItemRuntimeProperties& Other) const { return !(*this == Other); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
template<typename T>
|
||||||
|
static bool ArrayEqual(const TArray<T>& A, const TArray<T>& B)
|
||||||
|
{
|
||||||
|
if (A.Num() != B.Num()) { return false; }
|
||||||
|
for (const T& Elem : A) { if (!B.Contains(Elem)) { return false; } }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
26
Source/ItemDatabase/ItemDatabase.Build.cs
Normal file
26
Source/ItemDatabase/ItemDatabase.Build.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemDatabase : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemDatabase(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"ItemCore"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"DeveloperSettings"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Source/ItemDatabase/Private/ItemDatabaseModule.cpp
Normal file
8
Source/ItemDatabase/Private/ItemDatabaseModule.cpp
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemDatabaseLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemDatabase);
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemDatabase)
|
||||||
555
Source/ItemDatabase/Private/ItemDatabaseSubsystem.cpp
Normal file
555
Source/ItemDatabase/Private/ItemDatabaseSubsystem.cpp
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemEcosystemSettings.h"
|
||||||
|
#include "ItemDatabaseLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "Engine/DataTable.h"
|
||||||
|
#include "Engine/GameInstance.h"
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
RebuildCache();
|
||||||
|
|
||||||
|
#if !UE_BUILD_SHIPPING
|
||||||
|
TArray<FItemValidationIssue> Issues;
|
||||||
|
ValidateData(Issues);
|
||||||
|
for (const FItemValidationIssue& Issue : Issues)
|
||||||
|
{
|
||||||
|
if (Issue.bIsError)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDatabase, Error, TEXT("[Validate] %s: %s"), *Issue.ItemId.ToString(), *Issue.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDatabase, Warning, TEXT("[Validate] %s: %s"), *Issue.ItemId.ToString(), *Issue.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::Deinitialize()
|
||||||
|
{
|
||||||
|
Definitions.Empty();
|
||||||
|
PropertiesByItem.Empty();
|
||||||
|
FeaturesByItem.Empty();
|
||||||
|
InteractionsByItem.Empty();
|
||||||
|
Redirects.Empty();
|
||||||
|
ContainerDefs.Empty();
|
||||||
|
WorldSlotDefs.Empty();
|
||||||
|
LootByTable.Empty();
|
||||||
|
RecipesById.Empty();
|
||||||
|
bCacheBuilt = false;
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
UItemDatabaseSubsystem* UItemDatabaseSubsystem::Get(const UObject* WorldContext)
|
||||||
|
{
|
||||||
|
if (!WorldContext)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (const UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr)
|
||||||
|
{
|
||||||
|
if (UGameInstance* GI = World->GetGameInstance())
|
||||||
|
{
|
||||||
|
return GI->GetSubsystem<UItemDatabaseSubsystem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::RebuildCache()
|
||||||
|
{
|
||||||
|
Definitions.Empty();
|
||||||
|
PropertiesByItem.Empty();
|
||||||
|
FeaturesByItem.Empty();
|
||||||
|
InteractionsByItem.Empty();
|
||||||
|
Redirects.Empty();
|
||||||
|
|
||||||
|
const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get();
|
||||||
|
if (!Settings)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDatabase, Warning, TEXT("No ItemEcosystem settings found."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadDefinitions(Settings->ItemsTable.LoadSynchronous());
|
||||||
|
LoadProperties(Settings->PropertiesTable.LoadSynchronous());
|
||||||
|
LoadFeatures(Settings->FeaturesTable.LoadSynchronous());
|
||||||
|
LoadInteractions(Settings->InteractionsTable.LoadSynchronous());
|
||||||
|
LoadRedirects(Settings->RedirectsTable.LoadSynchronous());
|
||||||
|
LoadContainers(Settings->ContainersTable.LoadSynchronous());
|
||||||
|
LoadWorldSlots(Settings->WorldSlotsTable.LoadSynchronous());
|
||||||
|
LoadLoot(Settings->LootTable.LoadSynchronous());
|
||||||
|
LoadRecipes(Settings->RecipesTable.LoadSynchronous());
|
||||||
|
|
||||||
|
bCacheBuilt = true;
|
||||||
|
UE_LOG(LogItemDatabase, Log, TEXT("Item database built: %d items, %d redirects."),
|
||||||
|
Definitions.Num(), Redirects.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadDefinitions(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDatabase, Warning, TEXT("DT_Items not assigned in Project Settings -> Item Ecosystem."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FItemDefinitionRow*> Rows;
|
||||||
|
Table->GetAllRows<FItemDefinitionRow>(TEXT("ItemDatabase.LoadDefinitions"), Rows);
|
||||||
|
for (const FItemDefinitionRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (!Row || Row->ItemId.IsNone())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Definitions.Add(Row->ItemId, *Row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadProperties(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FItemPropertyRow*> Rows;
|
||||||
|
Table->GetAllRows<FItemPropertyRow>(TEXT("ItemDatabase.LoadProperties"), Rows);
|
||||||
|
for (const FItemPropertyRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->ItemId.IsNone())
|
||||||
|
{
|
||||||
|
PropertiesByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadFeatures(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FItemFeatureRow*> Rows;
|
||||||
|
Table->GetAllRows<FItemFeatureRow>(TEXT("ItemDatabase.LoadFeatures"), Rows);
|
||||||
|
for (const FItemFeatureRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->ItemId.IsNone())
|
||||||
|
{
|
||||||
|
FeaturesByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadInteractions(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FItemInteractionRow*> Rows;
|
||||||
|
Table->GetAllRows<FItemInteractionRow>(TEXT("ItemDatabase.LoadInteractions"), Rows);
|
||||||
|
for (const FItemInteractionRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->ItemId.IsNone())
|
||||||
|
{
|
||||||
|
InteractionsByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadRedirects(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FItemRedirectRow*> Rows;
|
||||||
|
Table->GetAllRows<FItemRedirectRow>(TEXT("ItemDatabase.LoadRedirects"), Rows);
|
||||||
|
for (const FItemRedirectRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->OldItemId.IsNone() && !Row->NewItemId.IsNone())
|
||||||
|
{
|
||||||
|
Redirects.Add(Row->OldItemId, Row->NewItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadContainers(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table) { return; }
|
||||||
|
TArray<FContainerDefRow*> Rows;
|
||||||
|
Table->GetAllRows<FContainerDefRow>(TEXT("ItemDatabase.LoadContainers"), Rows);
|
||||||
|
for (const FContainerDefRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && Row->ContainerTag.IsValid()) { ContainerDefs.Add(Row->ContainerTag, *Row); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadWorldSlots(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table) { return; }
|
||||||
|
TArray<FWorldSlotDefRow*> Rows;
|
||||||
|
Table->GetAllRows<FWorldSlotDefRow>(TEXT("ItemDatabase.LoadWorldSlots"), Rows);
|
||||||
|
for (const FWorldSlotDefRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && Row->SlotTag.IsValid()) { WorldSlotDefs.Add(Row->SlotTag, *Row); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadLoot(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table) { return; }
|
||||||
|
TArray<FLootEntryRow*> Rows;
|
||||||
|
Table->GetAllRows<FLootEntryRow>(TEXT("ItemDatabase.LoadLoot"), Rows);
|
||||||
|
for (const FLootEntryRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->TableId.IsNone()) { LootByTable.FindOrAdd(Row->TableId).Add(*Row); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::LoadRecipes(UDataTable* Table)
|
||||||
|
{
|
||||||
|
if (!Table) { return; }
|
||||||
|
TArray<FCraftingRecipeRow*> Rows;
|
||||||
|
Table->GetAllRows<FCraftingRecipeRow>(TEXT("ItemDatabase.LoadRecipes"), Rows);
|
||||||
|
for (const FCraftingRecipeRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row && !Row->RecipeId.IsNone()) { RecipesById.Add(Row->RecipeId, *Row); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EItemPropertyReplicationPolicy UItemDatabaseSubsystem::GetPropertyPolicy(FName ItemId, const FGameplayTag& PropertyTag) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
for (const FItemPropertyRow& R : *Rows)
|
||||||
|
{
|
||||||
|
if (R.PropertyTag == PropertyTag) { return R.ReplicationPolicy; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EItemPropertyReplicationPolicy::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetServerOnlyProperties(FName ItemId, TArray<FGameplayTag>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
for (const FItemPropertyRow& R : *Rows)
|
||||||
|
{
|
||||||
|
if (R.ReplicationPolicy == EItemPropertyReplicationPolicy::ServerOnly) { Out.Add(R.PropertyTag); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDatabaseSubsystem::FindContainerDef(FGameplayTag ContainerTag, FContainerDefRow& OutDef) const
|
||||||
|
{
|
||||||
|
if (const FContainerDefRow* Found = ContainerDefs.Find(ContainerTag)) { OutDef = *Found; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDatabaseSubsystem::FindWorldSlotDef(FGameplayTag SlotTag, FWorldSlotDefRow& OutDef) const
|
||||||
|
{
|
||||||
|
if (const FWorldSlotDefRow* Found = WorldSlotDefs.Find(SlotTag)) { OutDef = *Found; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDatabaseSubsystem::FindRecipe(FName RecipeId, FCraftingRecipeRow& OutRecipe) const
|
||||||
|
{
|
||||||
|
if (const FCraftingRecipeRow* Found = RecipesById.Find(RecipeId)) { OutRecipe = *Found; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetAllRecipes(TArray<FCraftingRecipeRow>& OutRecipes) const
|
||||||
|
{
|
||||||
|
RecipesById.GenerateValueArray(OutRecipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetLootEntries(FName TableId, TArray<FLootEntryRow>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FLootEntryRow>* Found = LootByTable.Find(TableId)) { Out = *Found; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::RollLoot(FName TableId, int32 RollCount, TArray<FItemInstanceData>& OutItems) const
|
||||||
|
{
|
||||||
|
const TArray<FLootEntryRow>* Entries = LootByTable.Find(TableId);
|
||||||
|
if (!Entries || Entries->Num() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float TotalWeight = 0.f;
|
||||||
|
for (const FLootEntryRow& E : *Entries) { TotalWeight += FMath::Max(0.f, E.Weight); }
|
||||||
|
if (TotalWeight <= 0.f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 Roll = 0; Roll < FMath::Max(1, RollCount); ++Roll)
|
||||||
|
{
|
||||||
|
float Pick = FMath::FRandRange(0.f, TotalWeight);
|
||||||
|
for (const FLootEntryRow& E : *Entries)
|
||||||
|
{
|
||||||
|
Pick -= FMath::Max(0.f, E.Weight);
|
||||||
|
if (Pick <= 0.f)
|
||||||
|
{
|
||||||
|
if (FMath::FRand() <= E.Chance && HasItem(E.ItemId))
|
||||||
|
{
|
||||||
|
const int32 Qty = FMath::RandRange(FMath::Max(1, E.MinQuantity), FMath::Max(E.MinQuantity, E.MaxQuantity));
|
||||||
|
OutItems.Add(MakeItemInstance(E.ItemId, Qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FName UItemDatabaseSubsystem::ResolveRedirect(FName ItemId) const
|
||||||
|
{
|
||||||
|
// Follow the chain with a cycle guard.
|
||||||
|
FName Current = ItemId;
|
||||||
|
int32 Guard = 0;
|
||||||
|
while (const FName* Next = Redirects.Find(Current))
|
||||||
|
{
|
||||||
|
Current = *Next;
|
||||||
|
if (++Guard > 64)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDatabase, Error, TEXT("Redirect cycle detected starting at %s."), *ItemId.ToString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FItemDefinitionRow* UItemDatabaseSubsystem::GetItemDefinition(FName ItemId) const
|
||||||
|
{
|
||||||
|
return Definitions.Find(ResolveRedirect(ItemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDatabaseSubsystem::FindItemDefinition(FName ItemId, FItemDefinitionRow& OutDefinition) const
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = GetItemDefinition(ItemId))
|
||||||
|
{
|
||||||
|
OutDefinition = *Def;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDatabaseSubsystem::HasItem(FName ItemId) const
|
||||||
|
{
|
||||||
|
return Definitions.Contains(ResolveRedirect(ItemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetAllItemIds(TArray<FName>& OutItemIds) const
|
||||||
|
{
|
||||||
|
Definitions.GenerateKeyArray(OutItemIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UItemDatabaseSubsystem::GetMaxStack(FName ItemId) const
|
||||||
|
{
|
||||||
|
const FItemDefinitionRow* Def = GetItemDefinition(ItemId);
|
||||||
|
return Def ? FMath::Max(1, Def->MaxStack) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::ApplyDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
for (const FItemPropertyRow& Row : *Rows)
|
||||||
|
{
|
||||||
|
if (Row.PropertyTag.IsValid())
|
||||||
|
{
|
||||||
|
Out.SetFloat(Row.PropertyTag, FMath::Clamp(Row.DefaultValue, Row.Min, Row.Max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::ApplyMissingDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
for (const FItemPropertyRow& Row : *Rows)
|
||||||
|
{
|
||||||
|
if (Row.PropertyTag.IsValid() && !Out.HasFloat(Row.PropertyTag))
|
||||||
|
{
|
||||||
|
Out.SetFloat(Row.PropertyTag, FMath::Clamp(Row.DefaultValue, Row.Min, Row.Max));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::MigrateLoadedItem(FItemInstanceData& Item) const
|
||||||
|
{
|
||||||
|
// 1) Redirect retired ids to their replacement.
|
||||||
|
const FName Resolved = ResolveRedirect(Item.ItemId);
|
||||||
|
if (Resolved != Item.ItemId)
|
||||||
|
{
|
||||||
|
Item.ItemId = Resolved;
|
||||||
|
}
|
||||||
|
// 2) Fill in properties introduced after this item was saved.
|
||||||
|
if (Item.ItemDataVersion < CurrentItemDataVersion)
|
||||||
|
{
|
||||||
|
ApplyMissingDefaultProperties(Item.ItemId, Item.RuntimeProperties);
|
||||||
|
Item.ItemDataVersion = CurrentItemDataVersion;
|
||||||
|
}
|
||||||
|
Item.EnsureInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemInstanceData UItemDatabaseSubsystem::MakeItemInstance(FName ItemId, int32 Quantity) const
|
||||||
|
{
|
||||||
|
const FName Resolved = ResolveRedirect(ItemId);
|
||||||
|
FItemInstanceData Instance(Resolved, FMath::Max(1, Quantity));
|
||||||
|
Instance.LocationType = EItemLocationType::None;
|
||||||
|
ApplyDefaultProperties(Resolved, Instance.RuntimeProperties);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetPropertyRows(FName ItemId, TArray<FItemPropertyRow>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
Out = *Rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetFeatureRows(FName ItemId, TArray<FItemFeatureRow>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
Out = *Rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetFeatureRows(FName ItemId, const FGameplayTag& Feature, TArray<FItemFeatureRow>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
for (const FItemFeatureRow& Row : *Rows)
|
||||||
|
{
|
||||||
|
if (Row.Feature == Feature)
|
||||||
|
{
|
||||||
|
Out.Add(Row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::GetInteractionRows(FName ItemId, TArray<FItemInteractionRow>& Out) const
|
||||||
|
{
|
||||||
|
if (const TArray<FItemInteractionRow>* Rows = InteractionsByItem.Find(ResolveRedirect(ItemId)))
|
||||||
|
{
|
||||||
|
Out = *Rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemDatabaseSubsystem::ValidateData(TArray<FItemValidationIssue>& OutIssues) const
|
||||||
|
{
|
||||||
|
auto AddIssue = [&OutIssues](FName Id, const FString& Msg, bool bError)
|
||||||
|
{
|
||||||
|
FItemValidationIssue Issue;
|
||||||
|
Issue.ItemId = Id;
|
||||||
|
Issue.Message = Msg;
|
||||||
|
Issue.bIsError = bError;
|
||||||
|
OutIssues.Add(Issue);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const TPair<FName, FItemDefinitionRow>& Pair : Definitions)
|
||||||
|
{
|
||||||
|
const FName Id = Pair.Key;
|
||||||
|
const FItemDefinitionRow& Def = Pair.Value;
|
||||||
|
|
||||||
|
if (Def.ItemId.IsNone())
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("ItemId is empty."), true);
|
||||||
|
}
|
||||||
|
if (Def.MaxStack <= 0)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("MaxStack <= 0."), true);
|
||||||
|
}
|
||||||
|
if (Def.GridSize.X <= 0 || Def.GridSize.Y <= 0)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("GridSize must be >= 1 in both axes."), true);
|
||||||
|
}
|
||||||
|
if (Def.bCanInstantPickup && !Def.bCanStoreInInventory)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("CanInstantPickup=true but CanStoreInInventory=false."), true);
|
||||||
|
}
|
||||||
|
if (Def.bCanEquip && Def.EquippedActorClass.IsNull())
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("CanEquip=true but EquippedActorClass is empty."), true);
|
||||||
|
}
|
||||||
|
if (Def.bCanWorldCarry && Def.WorldMesh.IsNull())
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("CanWorldCarry=true but WorldMesh is empty."), true);
|
||||||
|
}
|
||||||
|
if (Def.bCanInstallIntoWorldSlot && Def.CompatibleWorldSlots.IsEmpty())
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("CanInstallIntoWorldSlot=true but CompatibleWorldSlots is empty."), true);
|
||||||
|
}
|
||||||
|
if (Def.bCanPlace && !Def.PlacementRules.bCanPlaceOnGround
|
||||||
|
&& !Def.PlacementRules.bCanPlaceOnWall && !Def.PlacementRules.bCanPlaceOnVehicle
|
||||||
|
&& !Def.PlacementRules.bCanPlaceInWater)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("CanPlace=true but PlacementRules forbid every surface."), true);
|
||||||
|
}
|
||||||
|
if (Def.bIsQuestCritical && Def.bCanDrop)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("QuestCritical item is droppable (CanDrop=true)."), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature <-> property consistency (section 28).
|
||||||
|
auto HasDefaultProperty = [this, Id](const FGameplayTag& Tag) -> bool
|
||||||
|
{
|
||||||
|
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(Id))
|
||||||
|
{
|
||||||
|
for (const FItemPropertyRow& R : *Rows)
|
||||||
|
{
|
||||||
|
if (R.PropertyTag == Tag) { return true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Def.HasFeature(ItemEcosystemTags::Feature_Battery) && !HasDefaultProperty(ItemEcosystemTags::Property_BatteryCharge))
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("Battery feature present but no Property.BatteryCharge default."), false);
|
||||||
|
}
|
||||||
|
if (Def.HasFeature(ItemEcosystemTags::Feature_Fuel) && !HasDefaultProperty(ItemEcosystemTags::Property_FuelAmount))
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("Fuel feature present but no Property.FuelAmount default."), false);
|
||||||
|
}
|
||||||
|
if (Def.HasFeature(ItemEcosystemTags::Feature_Rotting))
|
||||||
|
{
|
||||||
|
bool bHasRottenId = false;
|
||||||
|
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(Id))
|
||||||
|
{
|
||||||
|
for (const FItemFeatureRow& R : *Rows)
|
||||||
|
{
|
||||||
|
if (R.Feature == ItemEcosystemTags::Feature_Rotting && R.Param == FName("RottenItemId") && !R.Value.IsEmpty())
|
||||||
|
{
|
||||||
|
bHasRottenId = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bHasRottenId)
|
||||||
|
{
|
||||||
|
AddIssue(Id, TEXT("Rotting feature present but no RottenItemId param."), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect targets must exist.
|
||||||
|
for (const TPair<FName, FName>& Pair : Redirects)
|
||||||
|
{
|
||||||
|
if (!Definitions.Contains(ResolveRedirect(Pair.Value)))
|
||||||
|
{
|
||||||
|
AddIssue(Pair.Key, FString::Printf(TEXT("Redirect target '%s' not found in DT_Items."), *Pair.Value.ToString()), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/ItemDatabase/Private/ItemEcosystemSettings.cpp
Normal file
13
Source/ItemDatabase/Private/ItemEcosystemSettings.cpp
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemEcosystemSettings.h"
|
||||||
|
|
||||||
|
UItemEcosystemSettings::UItemEcosystemSettings()
|
||||||
|
{
|
||||||
|
CategoryName = FName("Game");
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemEcosystemSettings* UItemEcosystemSettings::Get()
|
||||||
|
{
|
||||||
|
return GetDefault<UItemEcosystemSettings>();
|
||||||
|
}
|
||||||
129
Source/ItemDatabase/Public/ItemAuxiliaryRows.h
Normal file
129
Source/ItemDatabase/Public/ItemAuxiliaryRows.h
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataTable.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "ItemAuxiliaryRows.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DT_ItemProperties row (section 6.2): default runtime property for an item.
|
||||||
|
* Multiple rows share one ItemId, so the row name is arbitrary - the database
|
||||||
|
* indexes these by ItemId.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemPropertyRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
FGameplayTag PropertyTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
float DefaultValue = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
float Min = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
float Max = 1.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
EItemPropertyReplicationPolicy ReplicationPolicy = EItemPropertyReplicationPolicy::OwnerOnly;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||||
|
bool bSave = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DT_ItemFeatures row (section 6.3): one tunable parameter of a feature.
|
||||||
|
* e.g. raw_meat | Rotting | DecayRatePerHour | 0.08
|
||||||
|
* Value is stored as text so it can carry floats, ints or item ids uniformly.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemFeatureRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||||
|
FGameplayTag Feature;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||||
|
FName Param;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||||
|
FString Value;
|
||||||
|
|
||||||
|
float GetFloat(float Fallback = 0.f) const
|
||||||
|
{
|
||||||
|
return Value.IsNumeric() ? FCString::Atof(*Value) : Fallback;
|
||||||
|
}
|
||||||
|
int32 GetInt(int32 Fallback = 0) const
|
||||||
|
{
|
||||||
|
return Value.IsNumeric() ? FCString::Atoi(*Value) : Fallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DT_ItemInteractions row (section 6.4): a data-driven interaction option.
|
||||||
|
* The interaction resolver turns matching rows into FInteractionOptions.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemInteractionRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
/** Context filter, e.g. "World", "Inventory", "Held+Vehicle", "Held+Generator". */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FName Context;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag ActionTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
EItemInteractionMode InteractionMode = EItemInteractionMode::None;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
int32 Priority = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bRequiresHold = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float HoldDuration = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float ActionDuration = 0.f;
|
||||||
|
|
||||||
|
/** Free-form requirement expression resolved by the requirement system (section 18). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FString Requirement;
|
||||||
|
|
||||||
|
/** Result verb, e.g. AddToInventory / HoldWorldItem / InstallIntoSlot / TransferFuel. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FName Result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DT_ItemRedirects row (section 26.1): maps a retired ItemId to its replacement. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemRedirectRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Redirect")
|
||||||
|
FName OldItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Redirect")
|
||||||
|
FName NewItemId;
|
||||||
|
};
|
||||||
121
Source/ItemDatabase/Public/ItemContentRows.h
Normal file
121
Source/ItemDatabase/Public/ItemContentRows.h
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataTable.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "ItemContentRows.generated.h"
|
||||||
|
|
||||||
|
/** DT_Containers row (section 6): a reusable container template. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FContainerDefRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTag ContainerTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
EInventoryContainerType Type = EInventoryContainerType::SlotList;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 Width = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 Height = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 MaxSlots = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
float MaxWeight = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTagContainer AcceptedItemTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTagContainer BlockedItemTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DT_WorldSlots row (section 6/14): documents a world slot's contract. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FWorldSlotDefRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FGameplayTag SlotTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FGameplayTagContainer AcceptedItemTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FName AttachSocketName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DT_LootTables row (section 6): one weighted drop in a named loot table. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FLootEntryRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
FName TableId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
float Weight = 1.f;
|
||||||
|
|
||||||
|
/** 0..1 independent chance that this entry rolls at all. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
float Chance = 1.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
int32 MinQuantity = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||||
|
int32 MaxQuantity = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One required ingredient of a recipe. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FCraftingIngredient
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
int32 Quantity = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DT_CraftingRecipes row (section 6). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FCraftingRecipeRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
FName RecipeId;
|
||||||
|
|
||||||
|
/** Optional station requirement, e.g. Station.Workbench. Empty = craft anywhere. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
FGameplayTag StationTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
TArray<FCraftingIngredient> Inputs;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
FName OutputItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
int32 OutputQuantity = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||||
|
float CraftTime = 0.f;
|
||||||
|
};
|
||||||
8
Source/ItemDatabase/Public/ItemDatabaseLog.h
Normal file
8
Source/ItemDatabase/Public/ItemDatabaseLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMDATABASE_API DECLARE_LOG_CATEGORY_EXTERN(LogItemDatabase, Log, All);
|
||||||
169
Source/ItemDatabase/Public/ItemDatabaseSubsystem.h
Normal file
169
Source/ItemDatabase/Public/ItemDatabaseSubsystem.h
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/GameInstanceSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "ItemAuxiliaryRows.h"
|
||||||
|
#include "ItemContentRows.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemDatabaseSubsystem.generated.h"
|
||||||
|
|
||||||
|
/** One problem found by ValidateData (section 28). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FItemValidationIssue
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||||
|
FString Message;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||||
|
bool bIsError = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authoritative, read-only access point for all static item data (section 35 stage 1).
|
||||||
|
*
|
||||||
|
* Loads the configured data tables once, flattens them into fast lookup maps and
|
||||||
|
* exposes definition/property/feature/interaction queries plus redirect resolution
|
||||||
|
* and a data validator. Lives on the GameInstance so it is available on both
|
||||||
|
* server and clients; the tables themselves are never replicated (section 32).
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMDATABASE_API UItemDatabaseSubsystem : public UGameInstanceSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//~ USubsystem
|
||||||
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
|
||||||
|
/** Convenience accessor from any world context. */
|
||||||
|
static UItemDatabaseSubsystem* Get(const UObject* WorldContext);
|
||||||
|
|
||||||
|
/** (Re)load every configured table and rebuild the lookup caches. */
|
||||||
|
void RebuildCache();
|
||||||
|
|
||||||
|
// --- Definition lookup ---
|
||||||
|
|
||||||
|
/** Returns the definition for an item (after redirects), or null if unknown. C++ fast path. */
|
||||||
|
const FItemDefinitionRow* GetItemDefinition(FName ItemId) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||||
|
bool FindItemDefinition(FName ItemId, FItemDefinitionRow& OutDefinition) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
bool HasItem(FName ItemId) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
void GetAllItemIds(TArray<FName>& OutItemIds) const;
|
||||||
|
|
||||||
|
/** Applies the redirect chain; returns the input unchanged if no redirect exists. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
FName ResolveRedirect(FName ItemId) const;
|
||||||
|
|
||||||
|
// --- Instances ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a fresh instance of ItemId with a new GUID and its default runtime
|
||||||
|
* properties applied. The single correct way to mint an item (server-side).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||||
|
FItemInstanceData MakeItemInstance(FName ItemId, int32 Quantity = 1) const;
|
||||||
|
|
||||||
|
/** Writes the item's default runtime properties into Out (does not clear first). */
|
||||||
|
void ApplyDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const;
|
||||||
|
|
||||||
|
/** Adds only default properties that are missing (save migration, section 26.2). */
|
||||||
|
void ApplyMissingDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const;
|
||||||
|
|
||||||
|
/** Current schema version stamped onto fresh/migrated instances. */
|
||||||
|
static constexpr int32 CurrentItemDataVersion = 1;
|
||||||
|
|
||||||
|
/** Applies redirects + version migration to a loaded instance (section 26). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||||
|
void MigrateLoadedItem(UPARAM(ref) FItemInstanceData& Item) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
int32 GetMaxStack(FName ItemId) const;
|
||||||
|
|
||||||
|
// --- Auxiliary data ---
|
||||||
|
|
||||||
|
void GetPropertyRows(FName ItemId, TArray<FItemPropertyRow>& Out) const;
|
||||||
|
void GetFeatureRows(FName ItemId, TArray<FItemFeatureRow>& Out) const;
|
||||||
|
|
||||||
|
/** Feature rows for one specific feature tag (e.g. all Rotting params of raw_meat). */
|
||||||
|
void GetFeatureRows(FName ItemId, const FGameplayTag& Feature, TArray<FItemFeatureRow>& Out) const;
|
||||||
|
|
||||||
|
void GetInteractionRows(FName ItemId, TArray<FItemInteractionRow>& Out) const;
|
||||||
|
|
||||||
|
/** Per-property replication policy default (section 22), or None if unspecified. */
|
||||||
|
EItemPropertyReplicationPolicy GetPropertyPolicy(FName ItemId, const FGameplayTag& PropertyTag) const;
|
||||||
|
|
||||||
|
/** Property tags on an item that are flagged ServerOnly (never sent to clients). */
|
||||||
|
void GetServerOnlyProperties(FName ItemId, TArray<FGameplayTag>& Out) const;
|
||||||
|
|
||||||
|
// --- Containers / world slots / loot / recipes (section 6) ---
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
bool FindContainerDef(FGameplayTag ContainerTag, FContainerDefRow& OutDef) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
bool FindWorldSlotDef(FGameplayTag SlotTag, FWorldSlotDefRow& OutDef) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
bool FindRecipe(FName RecipeId, FCraftingRecipeRow& OutRecipe) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||||
|
void GetAllRecipes(TArray<FCraftingRecipeRow>& OutRecipes) const;
|
||||||
|
|
||||||
|
void GetLootEntries(FName TableId, TArray<FLootEntryRow>& Out) const;
|
||||||
|
|
||||||
|
/** Rolls a loot table into a list of fresh item instances (server). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||||
|
void RollLoot(FName TableId, int32 RollCount, TArray<FItemInstanceData>& OutItems) const;
|
||||||
|
|
||||||
|
// --- Validation (section 28) ---
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||||
|
void ValidateData(TArray<FItemValidationIssue>& OutIssues) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void LoadDefinitions(UDataTable* Table);
|
||||||
|
void LoadProperties(UDataTable* Table);
|
||||||
|
void LoadFeatures(UDataTable* Table);
|
||||||
|
void LoadInteractions(UDataTable* Table);
|
||||||
|
void LoadRedirects(UDataTable* Table);
|
||||||
|
void LoadContainers(UDataTable* Table);
|
||||||
|
void LoadWorldSlots(UDataTable* Table);
|
||||||
|
void LoadLoot(UDataTable* Table);
|
||||||
|
void LoadRecipes(UDataTable* Table);
|
||||||
|
|
||||||
|
/** ItemId -> definition. */
|
||||||
|
TMap<FName, FItemDefinitionRow> Definitions;
|
||||||
|
|
||||||
|
/** ItemId -> default property rows. */
|
||||||
|
TMap<FName, TArray<FItemPropertyRow>> PropertiesByItem;
|
||||||
|
|
||||||
|
/** ItemId -> feature parameter rows. */
|
||||||
|
TMap<FName, TArray<FItemFeatureRow>> FeaturesByItem;
|
||||||
|
|
||||||
|
/** ItemId -> interaction rows. */
|
||||||
|
TMap<FName, TArray<FItemInteractionRow>> InteractionsByItem;
|
||||||
|
|
||||||
|
/** Old -> new id. */
|
||||||
|
TMap<FName, FName> Redirects;
|
||||||
|
|
||||||
|
TMap<FGameplayTag, FContainerDefRow> ContainerDefs;
|
||||||
|
TMap<FGameplayTag, FWorldSlotDefRow> WorldSlotDefs;
|
||||||
|
TMap<FName, TArray<FLootEntryRow>> LootByTable;
|
||||||
|
TMap<FName, FCraftingRecipeRow> RecipesById;
|
||||||
|
|
||||||
|
bool bCacheBuilt = false;
|
||||||
|
};
|
||||||
112
Source/ItemDatabase/Public/ItemDefinitionRow.h
Normal file
112
Source/ItemDatabase/Public/ItemDefinitionRow.h
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataTable.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemPlacementRules.h"
|
||||||
|
#include "ItemDefinitionRow.generated.h"
|
||||||
|
|
||||||
|
class UTexture2D;
|
||||||
|
class UStaticMesh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static, design-time data for an item type (section 4.2 / 6.1 DT_Items).
|
||||||
|
*
|
||||||
|
* One row per ItemId. Assets are soft references so the database never force-loads
|
||||||
|
* meshes/icons just to answer a flag query (section 33). Never replicated; never
|
||||||
|
* saved - only the ItemId travels in FItemInstanceData.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemDefinitionRow : public FTableRowBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||||
|
FText DisplayName;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity", meta = (MultiLine = true))
|
||||||
|
FText Description;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||||
|
FGameplayTag ItemType;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||||
|
FGameplayTagContainer ItemTags;
|
||||||
|
|
||||||
|
// --- Visuals (soft refs, section 33) ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||||
|
TSoftObjectPtr<UTexture2D> Icon;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||||
|
TSoftObjectPtr<UStaticMesh> WorldMesh;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||||
|
TSoftClassPtr<AActor> PickupActorClass;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||||
|
TSoftClassPtr<AActor> EquippedActorClass;
|
||||||
|
|
||||||
|
// --- Stacking / weight / grid ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack", meta = (ClampMin = "1"))
|
||||||
|
int32 MaxStack = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack", meta = (ClampMin = "0"))
|
||||||
|
float Weight = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack")
|
||||||
|
FIntPoint GridSize = FIntPoint(1, 1);
|
||||||
|
|
||||||
|
// --- Capability flags (section 4.2) ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanStoreInInventory = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanInstantPickup = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanWorldCarry = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanEquip = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanPlace = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanInstallIntoWorldSlot = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bCanDrop = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||||
|
bool bIsQuestCritical = false;
|
||||||
|
|
||||||
|
// --- Interaction defaults ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag DefaultInteraction;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag AlternativeInteraction;
|
||||||
|
|
||||||
|
// --- Slot compatibility ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Slots")
|
||||||
|
FGameplayTagContainer AllowedEquipmentSlots;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Slots")
|
||||||
|
FGameplayTagContainer CompatibleWorldSlots;
|
||||||
|
|
||||||
|
// --- Features ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Features")
|
||||||
|
FGameplayTagContainer ItemFeatures;
|
||||||
|
|
||||||
|
// --- Placement (section 13.3); only meaningful when bCanPlace ---
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement", meta = (EditCondition = "bCanPlace"))
|
||||||
|
FItemPlacementRules PlacementRules;
|
||||||
|
|
||||||
|
bool HasFeature(const FGameplayTag& Feature) const { return ItemFeatures.HasTag(Feature); }
|
||||||
|
};
|
||||||
63
Source/ItemDatabase/Public/ItemEcosystemSettings.h
Normal file
63
Source/ItemDatabase/Public/ItemEcosystemSettings.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DeveloperSettings.h"
|
||||||
|
#include "ItemEcosystemSettings.generated.h"
|
||||||
|
|
||||||
|
class UDataTable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Settings -> Game -> Item Ecosystem.
|
||||||
|
* Designers assign the data tables here; the database subsystem reads them on init.
|
||||||
|
* Keeping the wiring in settings means no code change to repoint tables (section 3).
|
||||||
|
*/
|
||||||
|
UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Item Ecosystem"))
|
||||||
|
class ITEMDATABASE_API UItemEcosystemSettings : public UDeveloperSettings
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UItemEcosystemSettings();
|
||||||
|
|
||||||
|
virtual FName GetCategoryName() const override { return FName("Game"); }
|
||||||
|
|
||||||
|
/** DT_Items - row struct FItemDefinitionRow. The one required table. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> ItemsTable;
|
||||||
|
|
||||||
|
/** DT_ItemProperties - row struct FItemPropertyRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> PropertiesTable;
|
||||||
|
|
||||||
|
/** DT_ItemFeatures - row struct FItemFeatureRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> FeaturesTable;
|
||||||
|
|
||||||
|
/** DT_ItemInteractions - row struct FItemInteractionRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> InteractionsTable;
|
||||||
|
|
||||||
|
/** DT_ItemRedirects - row struct FItemRedirectRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> RedirectsTable;
|
||||||
|
|
||||||
|
/** DT_Containers - row struct FContainerDefRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> ContainersTable;
|
||||||
|
|
||||||
|
/** DT_WorldSlots - row struct FWorldSlotDefRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> WorldSlotsTable;
|
||||||
|
|
||||||
|
/** DT_LootTables - row struct FLootEntryRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> LootTable;
|
||||||
|
|
||||||
|
/** DT_CraftingRecipes - row struct FCraftingRecipeRow. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||||
|
TSoftObjectPtr<UDataTable> RecipesTable;
|
||||||
|
|
||||||
|
static const UItemEcosystemSettings* Get();
|
||||||
|
};
|
||||||
69
Source/ItemDatabase/Public/ItemPlacementRules.h
Normal file
69
Source/ItemDatabase/Public/ItemPlacementRules.h
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemPlacementRules.generated.h"
|
||||||
|
|
||||||
|
/** How a placeable item snaps when previewed/placed (section 13.3). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EItemPlacementMode : uint8
|
||||||
|
{
|
||||||
|
FreeSurface,
|
||||||
|
SnapToGrid,
|
||||||
|
SnapToSocket
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static placement constraints for a placeable item (section 13.3).
|
||||||
|
* Evaluated locally for the ghost preview and re-validated on the server.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMDATABASE_API FItemPlacementRules
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
EItemPlacementMode PlacementMode = EItemPlacementMode::FreeSurface;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
float MaxDistance = 400.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bRequiresFlatSurface = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement", meta = (ClampMin = "0", ClampMax = "90"))
|
||||||
|
float MaxSlopeAngle = 35.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
FName CollisionProfile = FName("BlockAll");
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
FGameplayTagContainer AllowedSurfaceTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
FGameplayTagContainer BlockedSurfaceTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
float SnapToGrid = 0.f;
|
||||||
|
|
||||||
|
/** When PlacementMode == SnapToSocket, only sockets carrying this tag accept the item. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
FGameplayTag SnapToSocketTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
float MinDistanceFromOtherSameItems = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bCanPlaceInWater = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bCanPlaceOnVehicle = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bCanPlaceOnWall = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bCanPlaceOnGround = true;
|
||||||
|
};
|
||||||
25
Source/ItemDebug/ItemDebug.Build.cs
Normal file
25
Source/ItemDebug/ItemDebug.Build.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemDebug : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemDebug(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase",
|
||||||
|
"ItemInventory",
|
||||||
|
"ItemInteraction",
|
||||||
|
"ItemWorld"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
252
Source/ItemDebug/Private/ItemDebugModule.cpp
Normal file
252
Source/ItemDebug/Private/ItemDebugModule.cpp
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemDebugLog.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemFeatureLibrary.h"
|
||||||
|
#include "ItemCraftingLibrary.h"
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemDebug);
|
||||||
|
|
||||||
|
// Implemented as the Item Debugger console toolkit (section 29).
|
||||||
|
namespace ItemDebugCmd
|
||||||
|
{
|
||||||
|
static APawn* GetPlayerPawn(UWorld* World)
|
||||||
|
{
|
||||||
|
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
||||||
|
return PC ? PC->GetPawn() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UInventoryComponent* GetPlayerInventory(UWorld* World)
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPlayerPawn(World);
|
||||||
|
return Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Screen(const FString& Msg, const FColor Color = FColor::Green)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDebug, Log, TEXT("%s"), *Msg);
|
||||||
|
if (GEngine)
|
||||||
|
{
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 4.f, Color, Msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Give(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
if (Args.Num() < 1) { Screen(TEXT("Usage: ItemEco.Give <ItemId> [Qty]"), FColor::Yellow); return; }
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(World);
|
||||||
|
if (!Inv || !DB) { Screen(TEXT("No player inventory / database."), FColor::Red); return; }
|
||||||
|
|
||||||
|
const FName Id(*Args[0]);
|
||||||
|
const int32 Qty = Args.Num() > 1 ? FMath::Max(1, FCString::Atoi(*Args[1])) : 1;
|
||||||
|
if (!DB->HasItem(Id)) { Screen(FString::Printf(TEXT("Unknown item '%s'"), *Id.ToString()), FColor::Red); return; }
|
||||||
|
|
||||||
|
FItemInstanceData Item = DB->MakeItemInstance(Id, Qty);
|
||||||
|
Inv->AddItem(Item);
|
||||||
|
Screen(FString::Printf(TEXT("[Inventory] gave %s x%d"), *Id.ToString(), Qty));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Spawn(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
if (Args.Num() < 1) { Screen(TEXT("Usage: ItemEco.Spawn <ItemId> [Qty]"), FColor::Yellow); return; }
|
||||||
|
APawn* Pawn = GetPlayerPawn(World);
|
||||||
|
if (!Pawn) { Screen(TEXT("No player pawn."), FColor::Red); return; }
|
||||||
|
|
||||||
|
const FName Id(*Args[0]);
|
||||||
|
const int32 Qty = Args.Num() > 1 ? FMath::Max(1, FCString::Atoi(*Args[1])) : 1;
|
||||||
|
const FTransform Xf(Pawn->GetActorRotation(), Pawn->GetActorLocation() + Pawn->GetActorForwardVector() * 200.f + FVector(0, 0, 50.f));
|
||||||
|
if (APickupItemActor::SpawnPickup(World, Id, Qty, Xf))
|
||||||
|
{
|
||||||
|
Screen(FString::Printf(TEXT("Spawned pickup %s x%d"), *Id.ToString(), Qty));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Screen(FString::Printf(TEXT("Failed to spawn '%s'"), *Id.ToString()), FColor::Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Clear(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inv->GetAllItems(Items);
|
||||||
|
for (const FItemInstanceData& It : Items) { Inv->RemoveItem(It.InstanceId); }
|
||||||
|
Screen(FString::Printf(TEXT("Cleared %d items."), Items.Num()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Dump(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inv->GetAllItems(Items);
|
||||||
|
Screen(FString::Printf(TEXT("--- Inventory (%d entries) ---"), Items.Num()), FColor::Cyan);
|
||||||
|
for (const FItemInstanceData& It : Items)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDebug, Log, TEXT(" %s x%d [container %s slot %d]"),
|
||||||
|
*It.ItemId.ToString(), It.Quantity, *It.CurrentContainerId.ToString(), It.SlotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Validate(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(World);
|
||||||
|
if (!DB) { Screen(TEXT("No database."), FColor::Red); return; }
|
||||||
|
TArray<FItemValidationIssue> Issues;
|
||||||
|
DB->ValidateData(Issues);
|
||||||
|
Screen(FString::Printf(TEXT("Validation: %d issue(s)."), Issues.Num()), Issues.Num() ? FColor::Yellow : FColor::Green);
|
||||||
|
for (const FItemValidationIssue& Issue : Issues)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDebug, Warning, TEXT(" [%s] %s: %s"),
|
||||||
|
Issue.bIsError ? TEXT("ERROR") : TEXT("WARN"), *Issue.ItemId.ToString(), *Issue.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutates the first inventory item via the feature library.
|
||||||
|
static void MutateFirst(UWorld* World, TFunctionRef<void(FItemInstanceData&)> Mutator, const TCHAR* What)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inv->GetAllItems(Items);
|
||||||
|
if (Items.Num() == 0) { Screen(TEXT("Inventory empty."), FColor::Yellow); return; }
|
||||||
|
FItemInstanceData Item = Items[0];
|
||||||
|
Mutator(Item);
|
||||||
|
Inv->UpdateItem(Item);
|
||||||
|
Screen(FString::Printf(TEXT("%s %s"), What, *Item.ItemId.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Rot(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inv->GetAllItems(Items);
|
||||||
|
if (Items.Num() == 0) { Screen(TEXT("Inventory empty."), FColor::Yellow); return; }
|
||||||
|
FItemInstanceData Item = Items[0];
|
||||||
|
FName TransformId;
|
||||||
|
UItemFeatureLibrary::ForceRot(World, Item, TransformId);
|
||||||
|
if (!TransformId.IsNone())
|
||||||
|
{
|
||||||
|
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(World);
|
||||||
|
FItemInstanceData NewItem = DB ? DB->MakeItemInstance(TransformId, Item.Quantity) : FItemInstanceData(TransformId, Item.Quantity);
|
||||||
|
Inv->RemoveItem(Item.InstanceId);
|
||||||
|
Inv->AddItem(NewItem);
|
||||||
|
Screen(FString::Printf(TEXT("Rotted -> %s"), *TransformId.ToString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Inv->UpdateItem(Item);
|
||||||
|
Screen(FString::Printf(TEXT("Rotted %s"), *Item.ItemId.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Break(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
MutateFirst(World, [](FItemInstanceData& It) { UItemFeatureLibrary::BreakItem(It); }, TEXT("Broke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainBattery(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
const float Amt = Args.Num() > 0 ? FCString::Atof(*Args[0]) : 0.25f;
|
||||||
|
MutateFirst(World, [Amt](FItemInstanceData& It) { UItemFeatureLibrary::DrainBattery(It, Amt, 1.f); }, TEXT("Drained battery of"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddFuel(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
const float Amt = Args.Num() > 0 ? FCString::Atof(*Args[0]) : 5.f;
|
||||||
|
MutateFirst(World, [Amt](FItemInstanceData& It) { UItemFeatureLibrary::AddFuel(It, Amt, 9999.f); }, TEXT("Added fuel to"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UnlockAll(const TArray<FString>&, UWorld* World)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inv->GetAllItems(Items);
|
||||||
|
int32 Count = 0;
|
||||||
|
for (const FItemInstanceData& It : Items) { if (Inv->ClearItemLock(It.InstanceId)) { ++Count; } }
|
||||||
|
Screen(FString::Printf(TEXT("Unlocked %d items."), Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RollLoot(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
if (Args.Num() < 1) { Screen(TEXT("Usage: ItemEco.RollLoot <TableId> [Rolls]"), FColor::Yellow); return; }
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
const int32 Rolls = Args.Num() > 1 ? FMath::Max(1, FCString::Atoi(*Args[1])) : 5;
|
||||||
|
UItemCraftingLibrary::RollLootIntoInventory(World, Inv, FName(*Args[0]), Rolls);
|
||||||
|
Screen(FString::Printf(TEXT("Rolled loot '%s' x%d"), *Args[0], Rolls));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Craft(const TArray<FString>& Args, UWorld* World)
|
||||||
|
{
|
||||||
|
if (Args.Num() < 1) { Screen(TEXT("Usage: ItemEco.Craft <RecipeId>"), FColor::Yellow); return; }
|
||||||
|
UInventoryComponent* Inv = GetPlayerInventory(World);
|
||||||
|
if (!Inv) { Screen(TEXT("No player inventory."), FColor::Red); return; }
|
||||||
|
const bool bOk = UItemCraftingLibrary::Craft(World, Inv, FName(*Args[0]), FGameplayTag());
|
||||||
|
Screen(FString::Printf(TEXT("Craft '%s': %s"), *Args[0], bOk ? TEXT("OK") : TEXT("FAILED")), bOk ? FColor::Green : FColor::Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Console command registration ---
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdGive(
|
||||||
|
TEXT("ItemEco.Give"), TEXT("Give <ItemId> [Qty] to the local player inventory."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Give));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdSpawn(
|
||||||
|
TEXT("ItemEco.Spawn"), TEXT("Spawn <ItemId> [Qty] as a world pickup in front of the player."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Spawn));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdClear(
|
||||||
|
TEXT("ItemEco.Clear"), TEXT("Clear the local player inventory."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Clear));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdDump(
|
||||||
|
TEXT("ItemEco.Dump"), TEXT("Dump the local player inventory to the log."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Dump));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdValidate(
|
||||||
|
TEXT("ItemEco.Validate"), TEXT("Run data validation on the item database."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Validate));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdRot(
|
||||||
|
TEXT("ItemEco.Rot"), TEXT("Force-rot the first inventory item."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Rot));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdBreak(
|
||||||
|
TEXT("ItemEco.Break"), TEXT("Break the first inventory item."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Break));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdDrain(
|
||||||
|
TEXT("ItemEco.DrainBattery"), TEXT("DrainBattery [Amount] from the first item."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::DrainBattery));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdAddFuel(
|
||||||
|
TEXT("ItemEco.AddFuel"), TEXT("AddFuel [Amount] to the first item."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::AddFuel));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdUnlock(
|
||||||
|
TEXT("ItemEco.UnlockAll"), TEXT("Clear all item locks in the player inventory."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::UnlockAll));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdRollLoot(
|
||||||
|
TEXT("ItemEco.RollLoot"), TEXT("RollLoot <TableId> [Rolls] into the player inventory."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::RollLoot));
|
||||||
|
|
||||||
|
static FAutoConsoleCommandWithWorldAndArgs GCmdCraft(
|
||||||
|
TEXT("ItemEco.Craft"), TEXT("Craft <RecipeId> using the player inventory."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&ItemDebugCmd::Craft));
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemDebug)
|
||||||
83
Source/ItemDebug/Private/ItemDebugSubsystem.cpp
Normal file
83
Source/ItemDebug/Private/ItemDebugSubsystem.cpp
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemDebugSubsystem.h"
|
||||||
|
#include "InteractionTraceComponent.h"
|
||||||
|
#include "InteractionResolver.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "HeldItemComponent.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
|
||||||
|
static TAutoConsoleVariable<int32> CVarItemDebugHUD(
|
||||||
|
TEXT("ItemEco.DebugHUD"), 0,
|
||||||
|
TEXT("Draw the Item Ecosystem on-screen debugger (held item / target / interactions)."),
|
||||||
|
ECVF_Cheat);
|
||||||
|
|
||||||
|
void UItemDebugSubsystem::Tick(float DeltaTime)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaTime);
|
||||||
|
if (CVarItemDebugHUD.GetValueOnGameThread() <= 0 || !GEngine)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
||||||
|
APawn* Pawn = PC ? PC->GetPawn() : nullptr;
|
||||||
|
if (!Pawn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 Key = 9000;
|
||||||
|
auto Line = [&Key](const FString& Text, const FColor Color = FColor::White)
|
||||||
|
{
|
||||||
|
GEngine->AddOnScreenDebugMessage(Key++, 0.f, Color, Text);
|
||||||
|
};
|
||||||
|
|
||||||
|
Line(TEXT("== Item Ecosystem Debug =="), FColor::Cyan);
|
||||||
|
|
||||||
|
if (const UHeldItemComponent* Held = Pawn->FindComponentByClass<UHeldItemComponent>())
|
||||||
|
{
|
||||||
|
FItemInstanceData HeldItem;
|
||||||
|
if (Held->GetHeldItem(HeldItem))
|
||||||
|
{
|
||||||
|
Line(FString::Printf(TEXT("Held: %s"), *HeldItem.ItemId.ToString()), FColor::Yellow);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Line(TEXT("Held: -"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* Target = nullptr;
|
||||||
|
if (const UInteractionTraceComponent* Trace = Pawn->FindComponentByClass<UInteractionTraceComponent>())
|
||||||
|
{
|
||||||
|
Target = Trace->CurrentTarget;
|
||||||
|
}
|
||||||
|
Line(FString::Printf(TEXT("Target: %s"), *GetNameSafe(Target)), FColor::Green);
|
||||||
|
|
||||||
|
if (Target)
|
||||||
|
{
|
||||||
|
TArray<FInteractionOption> Options;
|
||||||
|
UInteractionResolver::GetAvailableInteractions(Pawn, Target, Options);
|
||||||
|
for (const FInteractionOption& Opt : Options)
|
||||||
|
{
|
||||||
|
Line(FString::Printf(TEXT(" [%d] %s%s"),
|
||||||
|
Opt.Priority, *Opt.DisplayText.ToString(),
|
||||||
|
Opt.bIsEnabled ? TEXT("") : TEXT(" (disabled)")),
|
||||||
|
Opt.bIsEnabled ? FColor::White : FColor::Silver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TStatId UItemDebugSubsystem::GetStatId() const
|
||||||
|
{
|
||||||
|
RETURN_QUICK_DECLARE_CYCLE_STAT(UItemDebugSubsystem, STATGROUP_Tickables);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemDebugSubsystem::DoesSupportWorldType(const EWorldType::Type WorldType) const
|
||||||
|
{
|
||||||
|
return WorldType == EWorldType::Game || WorldType == EWorldType::PIE;
|
||||||
|
}
|
||||||
8
Source/ItemDebug/Public/ItemDebugLog.h
Normal file
8
Source/ItemDebug/Public/ItemDebugLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMDEBUG_API DECLARE_LOG_CATEGORY_EXTERN(LogItemDebug, Log, All);
|
||||||
24
Source/ItemDebug/Public/ItemDebugSubsystem.h
Normal file
24
Source/ItemDebug/Public/ItemDebugSubsystem.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "ItemDebugSubsystem.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On-screen runtime debugger (section 29). Toggle with the console variable
|
||||||
|
* `ItemEco.DebugHUD 1`. Draws the local player's held item, current interaction
|
||||||
|
* target and the available interactions each frame. Read-only / client-local.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMDEBUG_API UItemDebugSubsystem : public UTickableWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//~ FTickableGameObject
|
||||||
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
virtual TStatId GetStatId() const override;
|
||||||
|
virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override;
|
||||||
|
};
|
||||||
30
Source/ItemDeveloper/ItemDeveloper.Build.cs
Normal file
30
Source/ItemDeveloper/ItemDeveloper.Build.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemDeveloper : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemDeveloper(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Slate",
|
||||||
|
"SlateCore",
|
||||||
|
"UnrealEd",
|
||||||
|
"ToolMenus"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
158
Source/ItemDeveloper/Private/ItemDeveloperModule.cpp
Normal file
158
Source/ItemDeveloper/Private/ItemDeveloperModule.cpp
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemDeveloperModule.h"
|
||||||
|
#include "ItemEcosystemSettings.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "Engine/DataTable.h"
|
||||||
|
#include "ToolMenus.h"
|
||||||
|
#include "Framework/Notifications/NotificationManager.h"
|
||||||
|
#include "Widgets/Notifications/SNotificationList.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY_STATIC(LogItemDeveloper, Log, All);
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "ItemDeveloper"
|
||||||
|
|
||||||
|
void FItemDeveloperModule::StartupModule()
|
||||||
|
{
|
||||||
|
UToolMenus::RegisterStartupCallback(
|
||||||
|
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FItemDeveloperModule::RegisterMenus));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemDeveloperModule::ShutdownModule()
|
||||||
|
{
|
||||||
|
UToolMenus::UnRegisterStartupCallback(this);
|
||||||
|
UToolMenus::UnregisterOwner(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemDeveloperModule::RegisterMenus()
|
||||||
|
{
|
||||||
|
FToolMenuOwnerScoped OwnerScoped(this);
|
||||||
|
|
||||||
|
UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
|
||||||
|
if (!ToolsMenu)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FToolMenuSection& Section = ToolsMenu->AddSection("ItemEcosystem", LOCTEXT("ItemEcosystemSection", "Item Ecosystem"));
|
||||||
|
Section.AddMenuEntry(
|
||||||
|
"ValidateItemData",
|
||||||
|
LOCTEXT("ValidateItemData", "Validate Item Data"),
|
||||||
|
LOCTEXT("ValidateItemDataTip", "Run data validation over DT_Items (section 28)."),
|
||||||
|
FSlateIcon(),
|
||||||
|
FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ValidateAllItems)));
|
||||||
|
|
||||||
|
Section.AddMenuEntry(
|
||||||
|
"ExportItemsCsv",
|
||||||
|
LOCTEXT("ExportItemsCsv", "Export DT_Items to CSV"),
|
||||||
|
LOCTEXT("ExportItemsCsvTip", "Write DT_Items to Saved/ItemEcosystem/DT_Items.csv (section 27)."),
|
||||||
|
FSlateIcon(),
|
||||||
|
FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ExportItemsToCsv)));
|
||||||
|
|
||||||
|
Section.AddMenuEntry(
|
||||||
|
"ReportMissingAssets",
|
||||||
|
LOCTEXT("ReportMissingAssets", "Report: Items Missing Icon/Mesh"),
|
||||||
|
LOCTEXT("ReportMissingAssetsTip", "List items with no Icon or WorldMesh (section 27)."),
|
||||||
|
FSlateIcon(),
|
||||||
|
FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ReportMissingAssets)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemDeveloperModule::ExportItemsToCsv()
|
||||||
|
{
|
||||||
|
const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get();
|
||||||
|
UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr;
|
||||||
|
if (!ItemsTable)
|
||||||
|
{
|
||||||
|
FNotificationInfo Info(LOCTEXT("NoItemsTable", "DT_Items is not assigned."));
|
||||||
|
Info.ExpireDuration = 5.f;
|
||||||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Csv = ItemsTable->GetTableAsCSV();
|
||||||
|
const FString Path = FPaths::ProjectSavedDir() / TEXT("ItemEcosystem") / TEXT("DT_Items.csv");
|
||||||
|
const bool bOk = FFileHelper::SaveStringToFile(Csv, *Path);
|
||||||
|
|
||||||
|
const FText Msg = bOk
|
||||||
|
? FText::Format(LOCTEXT("ExportOk", "Exported DT_Items to {0}"), FText::FromString(Path))
|
||||||
|
: LOCTEXT("ExportFail", "Failed to write CSV.");
|
||||||
|
FNotificationInfo Info(Msg);
|
||||||
|
Info.ExpireDuration = 6.f;
|
||||||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
UE_LOG(LogItemDeveloper, Log, TEXT("CSV export: %s -> %s"), bOk ? TEXT("OK") : TEXT("FAIL"), *Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemDeveloperModule::ReportMissingAssets()
|
||||||
|
{
|
||||||
|
const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get();
|
||||||
|
UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr;
|
||||||
|
if (!ItemsTable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 Missing = 0;
|
||||||
|
TArray<FItemDefinitionRow*> Rows;
|
||||||
|
ItemsTable->GetAllRows<FItemDefinitionRow>(TEXT("ItemDeveloper.ReportMissing"), Rows);
|
||||||
|
for (const FItemDefinitionRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (!Row) { continue; }
|
||||||
|
if (Row->Icon.IsNull()) { ++Missing; UE_LOG(LogItemDeveloper, Warning, TEXT("%s: no Icon"), *Row->ItemId.ToString()); }
|
||||||
|
if (Row->WorldMesh.IsNull()) { ++Missing; UE_LOG(LogItemDeveloper, Warning, TEXT("%s: no WorldMesh"), *Row->ItemId.ToString()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
FNotificationInfo Info(FText::Format(LOCTEXT("MissingResult", "Asset report: {0} missing reference(s). See Output Log."), FText::AsNumber(Missing)));
|
||||||
|
Info.ExpireDuration = 6.f;
|
||||||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemDeveloperModule::ValidateAllItems()
|
||||||
|
{
|
||||||
|
const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get();
|
||||||
|
UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr;
|
||||||
|
|
||||||
|
int32 Errors = 0;
|
||||||
|
int32 Warnings = 0;
|
||||||
|
|
||||||
|
auto Report = [&](const FName& Id, const FString& Msg, bool bError)
|
||||||
|
{
|
||||||
|
bError ? ++Errors : ++Warnings;
|
||||||
|
UE_LOG(LogItemDeveloper, Warning, TEXT("[%s] %s: %s"), bError ? TEXT("ERROR") : TEXT("WARN"), *Id.ToString(), *Msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ItemsTable)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemDeveloper, Error, TEXT("DT_Items is not assigned in Project Settings -> Item Ecosystem."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TArray<FItemDefinitionRow*> Rows;
|
||||||
|
ItemsTable->GetAllRows<FItemDefinitionRow>(TEXT("ItemDeveloper.Validate"), Rows);
|
||||||
|
for (const FItemDefinitionRow* Row : Rows)
|
||||||
|
{
|
||||||
|
if (!Row) { continue; }
|
||||||
|
const FName Id = Row->ItemId;
|
||||||
|
if (Id.IsNone()) { Report(Id, TEXT("ItemId is empty."), true); }
|
||||||
|
if (Row->MaxStack <= 0) { Report(Id, TEXT("MaxStack <= 0."), true); }
|
||||||
|
if (Row->GridSize.X <= 0 || Row->GridSize.Y <= 0) { Report(Id, TEXT("GridSize must be >= 1."), true); }
|
||||||
|
if (Row->bCanInstantPickup && !Row->bCanStoreInInventory) { Report(Id, TEXT("InstantPickup but not storable."), true); }
|
||||||
|
if (Row->bCanEquip && Row->EquippedActorClass.IsNull()) { Report(Id, TEXT("CanEquip but no EquippedActorClass."), true); }
|
||||||
|
if (Row->bCanWorldCarry && Row->WorldMesh.IsNull()) { Report(Id, TEXT("CanWorldCarry but no WorldMesh."), true); }
|
||||||
|
if (Row->bCanInstallIntoWorldSlot && Row->CompatibleWorldSlots.IsEmpty()) { Report(Id, TEXT("CanInstall but no CompatibleWorldSlots."), true); }
|
||||||
|
if (Row->bIsQuestCritical && Row->bCanDrop) { Report(Id, TEXT("QuestCritical item is droppable."), false); }
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemDeveloper, Log, TEXT("Validated %d item rows."), Rows.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
const FText Summary = FText::Format(
|
||||||
|
LOCTEXT("ValidateResult", "Item validation: {0} error(s), {1} warning(s). See Output Log."),
|
||||||
|
FText::AsNumber(Errors), FText::AsNumber(Warnings));
|
||||||
|
FNotificationInfo Info(Summary);
|
||||||
|
Info.ExpireDuration = 6.f;
|
||||||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FItemDeveloperModule, ItemDeveloper)
|
||||||
25
Source/ItemDeveloper/Public/ItemDeveloperModule.h
Normal file
25
Source/ItemDeveloper/Public/ItemDeveloperModule.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor-only tooling for the Item Ecosystem (section 27-28).
|
||||||
|
* Registers a "Item Ecosystem" menu under Tools with data validation. The full
|
||||||
|
* Item Ecosystem Editor (browser/filters/templates) is intended to be built on
|
||||||
|
* top of this as an Editor Utility Widget driving UItemDatabaseSubsystem.
|
||||||
|
*/
|
||||||
|
class FItemDeveloperModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override;
|
||||||
|
virtual void ShutdownModule() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RegisterMenus();
|
||||||
|
static void ValidateAllItems();
|
||||||
|
static void ExportItemsToCsv();
|
||||||
|
static void ReportMissingAssets();
|
||||||
|
};
|
||||||
28
Source/ItemInteraction/ItemInteraction.Build.cs
Normal file
28
Source/ItemInteraction/ItemInteraction.Build.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemInteraction : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemInteraction(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"NetCore",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase",
|
||||||
|
"ItemInventory"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Source/ItemInteraction/Private/InteractableComponent.cpp
Normal file
9
Source/ItemInteraction/Private/InteractableComponent.cpp
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
|
||||||
|
UInteractableComponent::UInteractableComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
162
Source/ItemInteraction/Private/InteractionAction.cpp
Normal file
162
Source/ItemInteraction/Private/InteractionAction.cpp
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "ItemInteractionLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** Picks the item a requirement should inspect: held first, else the target item. */
|
||||||
|
const FItemInstanceData* ResolveSubjectItem(const FInteractionContext& Ctx)
|
||||||
|
{
|
||||||
|
if (Ctx.bHasHeldItem) { return &Ctx.HeldItem; }
|
||||||
|
if (Ctx.bHasTargetItem) { return &Ctx.TargetItem; }
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ItemHasTag(const UObject* WorldCtx, const FItemInstanceData& Item, const FGameplayTag& Tag)
|
||||||
|
{
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldCtx))
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||||
|
{
|
||||||
|
return Def->ItemTags.HasTag(Tag) || Def->ItemType.MatchesTag(Tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EvaluateRequirement(const FInteractionRequirement& Req, const FInteractionContext& Ctx)
|
||||||
|
{
|
||||||
|
switch (Req.RequirementType)
|
||||||
|
{
|
||||||
|
case EInteractionRequirementType::None:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case EInteractionRequirementType::HasInventorySpace:
|
||||||
|
{
|
||||||
|
if (!Ctx.PlayerInventory) { return false; }
|
||||||
|
const FItemInstanceData* Subject = Ctx.bHasTargetItem ? &Ctx.TargetItem : ResolveSubjectItem(Ctx);
|
||||||
|
if (!Subject) { return true; }
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
Ctx.PlayerInventory->GetContainers(Containers);
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (Ctx.PlayerInventory->CanAcceptItem(*Subject, C.ContainerId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EInteractionRequirementType::HasItemWithTag:
|
||||||
|
{
|
||||||
|
if (!Ctx.PlayerInventory) { return false; }
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Ctx.PlayerInventory->GetAllItems(Items);
|
||||||
|
for (const FItemInstanceData& It : Items)
|
||||||
|
{
|
||||||
|
if (ItemHasTag(Ctx.PlayerInventory.Get(), It, Req.RequiredTag)) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EInteractionRequirementType::HasHeldItemWithTag:
|
||||||
|
return Ctx.bHasHeldItem && ItemHasTag(Ctx.Character.Get(), Ctx.HeldItem, Req.RequiredTag);
|
||||||
|
|
||||||
|
case EInteractionRequirementType::PropertyGreaterThan:
|
||||||
|
{
|
||||||
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
||||||
|
return Subject && Subject->RuntimeProperties.GetFloat(Req.RequiredTag) > Req.RequiredValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EInteractionRequirementType::PropertyLessThan:
|
||||||
|
{
|
||||||
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
||||||
|
return Subject && Subject->RuntimeProperties.GetFloat(Req.RequiredTag) < Req.RequiredValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EInteractionRequirementType::TargetHasFreeSlotTag:
|
||||||
|
// Re-validated against the world slot component at execution time (stage 8).
|
||||||
|
return Ctx.TargetActor != nullptr;
|
||||||
|
|
||||||
|
case EInteractionRequirementType::NotWet:
|
||||||
|
{
|
||||||
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
||||||
|
return !Subject || Subject->RuntimeProperties.GetFloat(ItemEcosystemTags::Property_Wetness) < 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EInteractionRequirementType::NotBroken:
|
||||||
|
{
|
||||||
|
const FItemInstanceData* Subject = ResolveSubjectItem(Ctx);
|
||||||
|
return !Subject || !Subject->StateTags.HasTag(ItemEcosystemTags::State_Broken);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInteractionAction::CanExecute_Implementation(const FInteractionContext& Context, FText& OutDisabledReason) const
|
||||||
|
{
|
||||||
|
for (const FInteractionRequirement& Req : Requirements)
|
||||||
|
{
|
||||||
|
if (!EvaluateRequirement(Req, Context))
|
||||||
|
{
|
||||||
|
OutDisabledReason = Req.FailureText.IsEmpty()
|
||||||
|
? NSLOCTEXT("ItemInteraction", "RequirementFailed", "Requirement not met")
|
||||||
|
: Req.FailureText;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionAction::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
// Base does nothing; concrete actions / the interaction component handle effects.
|
||||||
|
UE_LOG(LogItemInteraction, Verbose, TEXT("UInteractionAction '%s' base Execute (no-op)."), *ActionTag.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
FInteractionOption UInteractionAction::BuildOption(const FInteractionContext& Context) const
|
||||||
|
{
|
||||||
|
FInteractionOption Option;
|
||||||
|
Option.ActionTag = ActionTag;
|
||||||
|
Option.Mode = Mode;
|
||||||
|
Option.DisplayText = DisplayText;
|
||||||
|
Option.Priority = Priority;
|
||||||
|
Option.bRequiresHold = bRequiresHold;
|
||||||
|
Option.HoldDuration = HoldDuration;
|
||||||
|
Option.ActionDuration = Execution.bHasDuration ? Execution.Duration : 0.f;
|
||||||
|
|
||||||
|
FText Reason;
|
||||||
|
Option.bIsEnabled = CanExecute(Context, Reason);
|
||||||
|
Option.DisabledReason = Reason;
|
||||||
|
|
||||||
|
Option.Request.Mode = Mode;
|
||||||
|
Option.Request.ActionTag = ActionTag;
|
||||||
|
Option.Request.TargetActor = Context.TargetActor;
|
||||||
|
if (Context.bHasTargetItem) { Option.Request.ItemInstanceId = Context.TargetItem.InstanceId; }
|
||||||
|
else if (Context.bHasHeldItem) { Option.Request.ItemInstanceId = Context.HeldItem.InstanceId; }
|
||||||
|
return Option;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* UInteractionAction::GetWorld() const
|
||||||
|
{
|
||||||
|
if (HasAllFlags(RF_ClassDefaultObject))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (const UObject* Outer = GetOuter())
|
||||||
|
{
|
||||||
|
return Outer->GetWorld();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
337
Source/ItemInteraction/Private/InteractionResolver.cpp
Normal file
337
Source/ItemInteraction/Private/InteractionResolver.cpp
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "InteractionResolver.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "ItemInteractionLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemAuxiliaryRows.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "ItemInteraction"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
FInteractionOption MakeOption(const FInteractionContext& Ctx, FGameplayTag ActionTag, EItemInteractionMode Mode,
|
||||||
|
const FText& Text, int32 Priority, bool bRequiresHold = false, float HoldDuration = 0.f, float ActionDuration = 0.f)
|
||||||
|
{
|
||||||
|
FInteractionOption Opt;
|
||||||
|
Opt.ActionTag = ActionTag;
|
||||||
|
Opt.Mode = Mode;
|
||||||
|
Opt.DisplayText = Text;
|
||||||
|
Opt.Priority = Priority;
|
||||||
|
Opt.bRequiresHold = bRequiresHold;
|
||||||
|
Opt.HoldDuration = HoldDuration;
|
||||||
|
Opt.ActionDuration = ActionDuration;
|
||||||
|
Opt.bIsEnabled = true;
|
||||||
|
|
||||||
|
Opt.Request.Mode = Mode;
|
||||||
|
Opt.Request.ActionTag = ActionTag;
|
||||||
|
Opt.Request.TargetActor = Ctx.TargetActor;
|
||||||
|
if (Ctx.bHasHeldItem) { Opt.Request.ItemInstanceId = Ctx.HeldItem.InstanceId; }
|
||||||
|
if (Ctx.bHasTargetItem) { Opt.Request.ItemInstanceId = Ctx.TargetItem.InstanceId; }
|
||||||
|
return Opt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a data-driven requirement string like "FuelAmount>0" or
|
||||||
|
* "BatteryCharge>=0.2" against a subject item's runtime properties (section 18).
|
||||||
|
* The left side maps to the Property.<Name> tag. Empty string = always true.
|
||||||
|
*/
|
||||||
|
bool EvaluateRequirementString(const FItemInstanceData& Subject, const FString& Req)
|
||||||
|
{
|
||||||
|
FString Trimmed = Req.TrimStartAndEnd();
|
||||||
|
if (Trimmed.IsEmpty())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the comparison operator (order matters: check 2-char first).
|
||||||
|
const TCHAR* Ops[] = { TEXT(">="), TEXT("<="), TEXT("=="), TEXT(">"), TEXT("<") };
|
||||||
|
FString Op, Left, Right;
|
||||||
|
for (const TCHAR* Candidate : Ops)
|
||||||
|
{
|
||||||
|
int32 Idx;
|
||||||
|
if (Trimmed.FindChar(Candidate[0], Idx))
|
||||||
|
{
|
||||||
|
if (Trimmed.Mid(Idx, FCString::Strlen(Candidate)) == Candidate)
|
||||||
|
{
|
||||||
|
Op = Candidate;
|
||||||
|
Left = Trimmed.Left(Idx).TrimStartAndEnd();
|
||||||
|
Right = Trimmed.Mid(Idx + FCString::Strlen(Candidate)).TrimStartAndEnd();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Op.IsEmpty())
|
||||||
|
{
|
||||||
|
return true; // unparseable -> don't block
|
||||||
|
}
|
||||||
|
|
||||||
|
const FGameplayTag PropTag = FGameplayTag::RequestGameplayTag(FName(*(FString(TEXT("Property.")) + Left)), false);
|
||||||
|
const float Value = Subject.RuntimeProperties.GetFloat(PropTag, 0.f);
|
||||||
|
const float Threshold = FCString::Atof(*Right);
|
||||||
|
|
||||||
|
if (Op == TEXT(">")) { return Value > Threshold; }
|
||||||
|
if (Op == TEXT("<")) { return Value < Threshold; }
|
||||||
|
if (Op == TEXT(">=")) { return Value >= Threshold; }
|
||||||
|
if (Op == TEXT("<=")) { return Value <= Threshold; }
|
||||||
|
if (Op == TEXT("==")) { return FMath::IsNearlyEqual(Value, Threshold); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnyContainerAccepts(UInventoryComponent* Inv, const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
if (!Inv) { return false; }
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
Inv->GetContainers(Containers);
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (Inv->CanAcceptItem(Item, C.ContainerId)) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInteractionResolver::ResolveHeldItem(APawn* Instigator, FItemInstanceData& OutItem)
|
||||||
|
{
|
||||||
|
if (!Instigator) { return false; }
|
||||||
|
|
||||||
|
auto TryObject = [&OutItem](UObject* Obj) -> bool
|
||||||
|
{
|
||||||
|
if (Obj && Obj->Implements<UHeldItemInterface>())
|
||||||
|
{
|
||||||
|
IHeldItemInterface* Held = Cast<IHeldItemInterface>(Obj);
|
||||||
|
if (Held && Held->HasHeldItem())
|
||||||
|
{
|
||||||
|
return Held->GetHeldItem(OutItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (TryObject(Instigator)) { return true; }
|
||||||
|
for (UActorComponent* Comp : Instigator->GetComponents())
|
||||||
|
{
|
||||||
|
if (TryObject(Comp)) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FInteractionContext UInteractionResolver::BuildContext(APawn* Instigator, AActor* TargetActor)
|
||||||
|
{
|
||||||
|
FInteractionContext Ctx;
|
||||||
|
Ctx.TargetActor = TargetActor;
|
||||||
|
Ctx.Character = Cast<ACharacter>(Instigator);
|
||||||
|
if (Instigator)
|
||||||
|
{
|
||||||
|
Ctx.PlayerController = Cast<APlayerController>(Instigator->GetController());
|
||||||
|
Ctx.PlayerInventory = Instigator->FindComponentByClass<UInventoryComponent>();
|
||||||
|
Ctx.bHasHeldItem = ResolveHeldItem(Instigator, Ctx.HeldItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TargetActor)
|
||||||
|
{
|
||||||
|
Ctx.TargetInteractable = TargetActor->FindComponentByClass<UInteractableComponent>();
|
||||||
|
if (Ctx.TargetInteractable)
|
||||||
|
{
|
||||||
|
Ctx.WorldTags = Ctx.TargetInteractable->InteractionTags;
|
||||||
|
}
|
||||||
|
if (TargetActor->Implements<UWorldItemProvider>())
|
||||||
|
{
|
||||||
|
if (IWorldItemProvider* Provider = Cast<IWorldItemProvider>(TargetActor))
|
||||||
|
{
|
||||||
|
Ctx.bHasTargetItem = Provider->GetProvidedItem(Ctx.TargetItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionResolver::GatherCapabilityOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out)
|
||||||
|
{
|
||||||
|
if (!Ctx.bHasTargetItem)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(Ctx.TargetActor);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Ctx.TargetItem.ItemId) : nullptr;
|
||||||
|
if (!Def)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FText Name = Def->DisplayName.IsEmpty() ? FText::FromName(Ctx.TargetItem.ItemId) : Def->DisplayName;
|
||||||
|
|
||||||
|
if (Def->bCanInstantPickup || Def->bCanStoreInInventory)
|
||||||
|
{
|
||||||
|
const EItemInteractionMode Mode = Def->bCanInstantPickup ? EItemInteractionMode::InstantPickup : EItemInteractionMode::PickupToInventory;
|
||||||
|
const FGameplayTag Tag = Def->bCanInstantPickup ? ItemEcosystemTags::Action_Pickup_Instant : ItemEcosystemTags::Action_Pickup;
|
||||||
|
FText Text = Ctx.TargetItem.Quantity > 1
|
||||||
|
? FText::Format(LOCTEXT("PickUpQty", "Pick up {0} x{1}"), Name, FText::AsNumber(Ctx.TargetItem.Quantity))
|
||||||
|
: FText::Format(LOCTEXT("PickUp", "Pick up {0}"), Name);
|
||||||
|
FInteractionOption Opt = MakeOption(Ctx, Tag, Mode, Text, 600);
|
||||||
|
if (!AnyContainerAccepts(Ctx.PlayerInventory, Ctx.TargetItem))
|
||||||
|
{
|
||||||
|
Opt.bIsEnabled = false;
|
||||||
|
Opt.DisabledReason = LOCTEXT("InventoryFull", "Inventory Full");
|
||||||
|
}
|
||||||
|
Out.Add(Opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Def->bCanWorldCarry)
|
||||||
|
{
|
||||||
|
FInteractionOption Opt = MakeOption(Ctx, ItemEcosystemTags::Action_Carry, EItemInteractionMode::CarryAttached,
|
||||||
|
FText::Format(LOCTEXT("Carry", "Carry {0}"), Name), 500);
|
||||||
|
if (Ctx.bHasHeldItem)
|
||||||
|
{
|
||||||
|
Opt.bIsEnabled = false;
|
||||||
|
Opt.DisabledReason = LOCTEXT("HandsFull", "Hands Full");
|
||||||
|
}
|
||||||
|
Out.Add(Opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionResolver::GatherContainerOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out)
|
||||||
|
{
|
||||||
|
if (!Ctx.TargetActor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A target that owns an inventory (and isn't the player themselves) can be opened.
|
||||||
|
UInventoryComponent* TargetInv = Ctx.TargetActor->FindComponentByClass<UInventoryComponent>();
|
||||||
|
if (!TargetInv || TargetInv == Ctx.PlayerInventory)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText Label = (Ctx.TargetInteractable && !Ctx.TargetInteractable->DisplayName.IsEmpty())
|
||||||
|
? FText::Format(LOCTEXT("OpenNamed", "Open {0}"), Ctx.TargetInteractable->DisplayName)
|
||||||
|
: LOCTEXT("Open", "Open");
|
||||||
|
Out.Add(MakeOption(Ctx, ItemEcosystemTags::Action_Open, EItemInteractionMode::OpenLootUI, Label, 700));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionResolver::GatherDataDrivenOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out)
|
||||||
|
{
|
||||||
|
const UObject* WorldCtx = Ctx.TargetActor ? static_cast<const UObject*>(Ctx.TargetActor) : static_cast<const UObject*>(Ctx.Character);
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldCtx);
|
||||||
|
if (!DB)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a "Held+Vehicle" style context, the suffix after '+' must match one of the
|
||||||
|
// target's world tags or its interactable type (loose, case-insensitive).
|
||||||
|
auto TargetMatchesSuffix = [&Ctx](const FString& Suffix) -> bool
|
||||||
|
{
|
||||||
|
if (Suffix.IsEmpty()) { return true; }
|
||||||
|
if (Ctx.TargetInteractable && Ctx.TargetInteractable->InteractableType.IsValid()
|
||||||
|
&& Ctx.TargetInteractable->InteractableType.ToString().Contains(Suffix))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const FGameplayTag& Tag : Ctx.WorldTags)
|
||||||
|
{
|
||||||
|
if (Tag.ToString().Contains(Suffix)) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto AppendRows = [&](const FItemInstanceData& Item, bool bHeldContext)
|
||||||
|
{
|
||||||
|
TArray<FItemInteractionRow> Rows;
|
||||||
|
DB->GetInteractionRows(Item.ItemId, Rows);
|
||||||
|
for (const FItemInteractionRow& Row : Rows)
|
||||||
|
{
|
||||||
|
const FString Context = Row.Context.ToString();
|
||||||
|
const bool bIsHeldRow = Context.StartsWith(TEXT("Held"));
|
||||||
|
if (bHeldContext != bIsHeldRow)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (bIsHeldRow)
|
||||||
|
{
|
||||||
|
FString Suffix;
|
||||||
|
Context.Split(TEXT("+"), nullptr, &Suffix);
|
||||||
|
if (!TargetMatchesSuffix(Suffix)) { continue; }
|
||||||
|
}
|
||||||
|
FInteractionOption Opt = MakeOption(Ctx, Row.ActionTag, Row.InteractionMode,
|
||||||
|
FText::FromName(Row.ActionTag.GetTagName()), Row.Priority,
|
||||||
|
Row.bRequiresHold, Row.HoldDuration, Row.ActionDuration);
|
||||||
|
Opt.Request.ItemInstanceId = Item.InstanceId;
|
||||||
|
Opt.Request.Quantity = Item.Quantity;
|
||||||
|
|
||||||
|
// Data-driven requirement gate (section 18).
|
||||||
|
if (!Row.Requirement.IsEmpty() && !EvaluateRequirementString(Item, Row.Requirement))
|
||||||
|
{
|
||||||
|
Opt.bIsEnabled = false;
|
||||||
|
Opt.DisabledReason = FText::FromString(Row.Requirement);
|
||||||
|
}
|
||||||
|
Out.Add(Opt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Ctx.bHasTargetItem) { AppendRows(Ctx.TargetItem, /*bHeldContext*/ false); }
|
||||||
|
if (Ctx.bHasHeldItem) { AppendRows(Ctx.HeldItem, /*bHeldContext*/ true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionResolver::GatherActionClassOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out)
|
||||||
|
{
|
||||||
|
if (!Ctx.TargetInteractable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const UInteractionAction* Action : Ctx.TargetInteractable->AvailableActions)
|
||||||
|
{
|
||||||
|
if (Action)
|
||||||
|
{
|
||||||
|
Out.Add(Action->BuildOption(Ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionResolver::GetAvailableInteractions(APawn* Instigator, AActor* TargetActor, TArray<FInteractionOption>& OutOptions)
|
||||||
|
{
|
||||||
|
if (!TargetActor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const FInteractionContext Ctx = BuildContext(Instigator, TargetActor);
|
||||||
|
|
||||||
|
GatherActionClassOptions(Ctx, OutOptions);
|
||||||
|
GatherCapabilityOptions(Ctx, OutOptions);
|
||||||
|
GatherContainerOptions(Ctx, OutOptions);
|
||||||
|
GatherDataDrivenOptions(Ctx, OutOptions);
|
||||||
|
|
||||||
|
OutOptions.StableSort([](const FInteractionOption& A, const FInteractionOption& B)
|
||||||
|
{
|
||||||
|
return A.Priority > B.Priority;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInteractionResolver::GetPrimaryInteraction(APawn* Instigator, AActor* TargetActor, FInteractionOption& OutOption)
|
||||||
|
{
|
||||||
|
TArray<FInteractionOption> Options;
|
||||||
|
GetAvailableInteractions(Instigator, TargetActor, Options);
|
||||||
|
for (const FInteractionOption& Opt : Options)
|
||||||
|
{
|
||||||
|
if (Opt.bIsEnabled)
|
||||||
|
{
|
||||||
|
OutOption = Opt;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Options.Num() > 0)
|
||||||
|
{
|
||||||
|
OutOption = Options[0]; // disabled, but lets UI show the reason
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
86
Source/ItemInteraction/Private/InteractionTraceComponent.cpp
Normal file
86
Source/ItemInteraction/Private/InteractionTraceComponent.cpp
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "InteractionTraceComponent.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
UInteractionTraceComponent::UInteractionTraceComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
PrimaryComponentTick.TickInterval = 0.1f; // 10 Hz is plenty for prompts
|
||||||
|
SetIsReplicatedByDefault(false); // purely local
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionTraceComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
TickTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInteractionTraceComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
|
||||||
|
{
|
||||||
|
const APawn* Pawn = Cast<APawn>(GetOwner());
|
||||||
|
const APlayerController* PC = Pawn ? Cast<APlayerController>(Pawn->GetController()) : nullptr;
|
||||||
|
if (!PC || !PC->IsLocalController())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PC->GetPlayerViewPoint(OutLocation, OutRotation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionTraceComponent::TickTrace()
|
||||||
|
{
|
||||||
|
FVector ViewLoc;
|
||||||
|
FRotator ViewRot;
|
||||||
|
if (!GetViewPoint(ViewLoc, ViewRot))
|
||||||
|
{
|
||||||
|
SetTarget(nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector End = ViewLoc + ViewRot.Vector() * TraceDistance;
|
||||||
|
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionTrace), false, GetOwner());
|
||||||
|
Params.bTraceComplex = false;
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
const bool bHit = GetWorld()->SweepSingleByChannel(
|
||||||
|
Hit, ViewLoc, End, FQuat::Identity, TraceChannel,
|
||||||
|
FCollisionShape::MakeSphere(TraceRadius), Params);
|
||||||
|
|
||||||
|
AActor* HitActor = bHit ? Hit.GetActor() : nullptr;
|
||||||
|
UInteractableComponent* Interactable = HitActor ? HitActor->FindComponentByClass<UInteractableComponent>() : nullptr;
|
||||||
|
|
||||||
|
if (Interactable)
|
||||||
|
{
|
||||||
|
// Distance gate against the component's own reach.
|
||||||
|
const float DistSq = FVector::DistSquared(ViewLoc, Hit.ImpactPoint);
|
||||||
|
if (DistSq > FMath::Square(Interactable->InteractionDistance))
|
||||||
|
{
|
||||||
|
Interactable = nullptr;
|
||||||
|
HitActor = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HitActor = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LastHitLocation = Hit.ImpactPoint;
|
||||||
|
LastHitNormal = Hit.ImpactNormal;
|
||||||
|
SetTarget(HitActor, Interactable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionTraceComponent::SetTarget(AActor* NewTarget, UInteractableComponent* NewInteractable)
|
||||||
|
{
|
||||||
|
if (CurrentTarget == NewTarget && CurrentInteractable == NewInteractable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CurrentTarget = NewTarget;
|
||||||
|
CurrentInteractable = NewInteractable;
|
||||||
|
OnTargetChanged.Broadcast(NewTarget, NewInteractable);
|
||||||
|
}
|
||||||
97
Source/ItemInteraction/Private/ItemInteractionActions.cpp
Normal file
97
Source/ItemInteraction/Private/ItemInteractionActions.cpp
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemInteractionActions.h"
|
||||||
|
#include "ItemInteractionLog.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
|
||||||
|
// --- Instant Pickup ---
|
||||||
|
|
||||||
|
UInteractionAction_InstantPickup::UInteractionAction_InstantPickup()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Pickup_Instant;
|
||||||
|
Mode = EItemInteractionMode::InstantPickup;
|
||||||
|
Priority = 600;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "PickUp", "Pick Up");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionAction_InstantPickup::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (!Context.TargetActor || !Context.PlayerInventory || !Context.TargetActor->Implements<UWorldItemProvider>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(Context.TargetActor);
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Provider || !Provider->IsItemAvailable() || !Provider->GetProvidedItem(Item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int32 Before = Item.Quantity;
|
||||||
|
Context.PlayerInventory->AddItem(Item);
|
||||||
|
if (Before - Item.Quantity <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Item.Quantity <= 0) { Provider->NotifyItemTaken(Context.Character.Get()); }
|
||||||
|
else { Provider->UpdateProvidedItem(Item); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Open Loot UI (client-side; no server effect) ---
|
||||||
|
|
||||||
|
UInteractionAction_OpenLootUI::UInteractionAction_OpenLootUI()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Open;
|
||||||
|
Mode = EItemInteractionMode::OpenLootUI;
|
||||||
|
Priority = 700;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Open", "Open");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inspect ---
|
||||||
|
|
||||||
|
UInteractionAction_Inspect::UInteractionAction_Inspect()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Inspect;
|
||||||
|
Mode = EItemInteractionMode::Inspect;
|
||||||
|
Priority = 400;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Inspect", "Inspect");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionAction_Inspect::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Verbose, TEXT("Inspect %s"), *GetNameSafe(Context.TargetActor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Combine / Transfer / Split / Merge ---
|
||||||
|
// These are inventory-UI verbs; their execution runs through the interaction
|
||||||
|
// component's inventory RPCs. The classes exist as authoring units (section 16).
|
||||||
|
|
||||||
|
UInteractionAction_Combine::UInteractionAction_Combine()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::Combine;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Combine", "Combine");
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_TransferContainerItem::UInteractionAction_TransferContainerItem()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::TransferContainerItem;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Transfer", "Transfer");
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_SplitStack::UInteractionAction_SplitStack()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::SplitStack;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Split", "Split Stack");
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_MergeStack::UInteractionAction_MergeStack()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::MergeStack;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemInteraction", "Merge", "Merge Stack");
|
||||||
|
}
|
||||||
520
Source/ItemInteraction/Private/ItemInteractionComponent.cpp
Normal file
520
Source/ItemInteraction/Private/ItemInteractionComponent.cpp
Normal file
@ -0,0 +1,520 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemInteractionComponent.h"
|
||||||
|
#include "InteractionResolver.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "ItemInteractionLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemTransaction.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
UItemInteractionComponent::UItemInteractionComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME_CONDITION(UItemInteractionComponent, ActiveAction, COND_OwnerOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
if (!bHasPending || !GetOwner() || !GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt if the player walked away from the target (section 19).
|
||||||
|
if (PendingRequest.TargetActor && PendingMaxDistanceSq > 0.f)
|
||||||
|
{
|
||||||
|
const float DistSq = FVector::DistSquared(GetOwner()->GetActorLocation(), PendingRequest.TargetActor->GetActorLocation());
|
||||||
|
if (DistSq > PendingMaxDistanceSq)
|
||||||
|
{
|
||||||
|
AbortTimedAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetWorld()->GetTimeSeconds() >= PendingEndTime)
|
||||||
|
{
|
||||||
|
CompleteTimedAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
APawn* UItemInteractionComponent::GetPawn() const
|
||||||
|
{
|
||||||
|
return Cast<APawn>(GetOwner());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::RequestInteraction(const FInteractionOption& Option)
|
||||||
|
{
|
||||||
|
// Opening a container is a purely local UI action: the shared container already
|
||||||
|
// replicates to this client, so no server round-trip is needed (section 23).
|
||||||
|
if (Option.Mode == EItemInteractionMode::OpenLootUI)
|
||||||
|
{
|
||||||
|
OnOpenContainer.Broadcast(Option.Request.TargetActor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FInteractionRequest Request = Option.Request;
|
||||||
|
if (!Request.RequestId.IsValid())
|
||||||
|
{
|
||||||
|
Request.RequestId = FGuid::NewGuid();
|
||||||
|
}
|
||||||
|
SendRequest(Request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::SendRequest(const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
Server_RequestInteraction(Request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_RequestInteraction_Implementation(const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
FInteractionContext Context;
|
||||||
|
FInteractionOption Authorized;
|
||||||
|
if (!AuthorizeRequest(Request, Context, Authorized))
|
||||||
|
{
|
||||||
|
Client_InteractionResult(Request.RequestId, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timed / hold actions run over time with interruption (section 19); instant ones execute now.
|
||||||
|
const bool bTimed = Authorized.ActionDuration > 0.f || Authorized.bRequiresHold;
|
||||||
|
if (bTimed && !bHasPending)
|
||||||
|
{
|
||||||
|
BeginTimedAction(Context, Authorized, Request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bSuccess = ExecuteAuthorized(Context, Authorized, Request);
|
||||||
|
Client_InteractionResult(Request.RequestId, bSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::AuthorizeRequest(const FInteractionRequest& Request, FInteractionContext& OutContext, FInteractionOption& OutOption) const
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPawn();
|
||||||
|
AActor* Target = Request.TargetActor;
|
||||||
|
if (!Pawn || !Target)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: missing pawn/target."), *Request.ActionTag.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance + line of sight gate (section 9.3-9.4).
|
||||||
|
OutContext = UInteractionResolver::BuildContext(Pawn, Target);
|
||||||
|
if (UInteractableComponent* Interactable = OutContext.TargetInteractable)
|
||||||
|
{
|
||||||
|
const float MaxDist = Interactable->InteractionDistance + ServerDistanceTolerance;
|
||||||
|
if (FVector::DistSquared(Pawn->GetActorLocation(), Target->GetActorLocation()) > FMath::Square(MaxDist))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: out of range."), *Request.ActionTag.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Interactable->bIsBusy)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: target busy."), *Request.ActionTag.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-resolve and require the requested option to actually be available + enabled (section 9.8-9.9).
|
||||||
|
TArray<FInteractionOption> Options;
|
||||||
|
UInteractionResolver::GetAvailableInteractions(Pawn, Target, Options);
|
||||||
|
for (const FInteractionOption& Opt : Options)
|
||||||
|
{
|
||||||
|
if (Opt.ActionTag == Request.ActionTag && Opt.Mode == Request.Mode)
|
||||||
|
{
|
||||||
|
if (!Opt.bIsEnabled)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: %s"),
|
||||||
|
*Request.ActionTag.ToString(), *Opt.DisabledReason.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutOption = Opt;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: option not available."), *Request.ActionTag.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::ServerExecuteMode(const FInteractionContext& Context, EItemInteractionMode Mode, FGameplayTag ActionTag)
|
||||||
|
{
|
||||||
|
if (!GetOwner() || !GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FInteractionOption Option;
|
||||||
|
Option.Mode = Mode;
|
||||||
|
Option.ActionTag = ActionTag;
|
||||||
|
|
||||||
|
FInteractionRequest Request;
|
||||||
|
Request.Mode = Mode;
|
||||||
|
Request.ActionTag = ActionTag;
|
||||||
|
Request.TargetActor = Context.TargetActor;
|
||||||
|
if (Context.bHasHeldItem) { Request.ItemInstanceId = Context.HeldItem.InstanceId; }
|
||||||
|
else if (Context.bHasTargetItem) { Request.ItemInstanceId = Context.TargetItem.InstanceId; }
|
||||||
|
|
||||||
|
return ExecuteAuthorized(Context, Option, Request);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
switch (Option.Mode)
|
||||||
|
{
|
||||||
|
case EItemInteractionMode::InstantPickup:
|
||||||
|
case EItemInteractionMode::PickupToInventory:
|
||||||
|
return ExecutePickup(Context);
|
||||||
|
|
||||||
|
case EItemInteractionMode::TransferContainerItem:
|
||||||
|
return ExecuteTransfer(Context, Request);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Anything else routes to a matching action-class on the interactable.
|
||||||
|
// Built-in carry/equip/install/placement modes are implemented in their
|
||||||
|
// respective stages (held/equipment/world-slot/placement modules).
|
||||||
|
if (ExecuteActionClass(Context, Option))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Mode %d has no handler yet."), (int32)Option.Mode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::ExecutePickup(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (!Context.TargetActor || !Context.PlayerInventory || !Context.TargetActor->Implements<UWorldItemProvider>())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(Context.TargetActor);
|
||||||
|
if (!Provider || !Provider->IsItemAvailable())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Provider->GetProvidedItem(Item) || !Item.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Before = Item.Quantity;
|
||||||
|
Context.PlayerInventory->AddItem(Item); // Item.Quantity now holds the leftover
|
||||||
|
const int32 Added = Before - Item.Quantity;
|
||||||
|
if (Added <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogItemInteraction, Log, TEXT("[Interaction] %s picked up %s x%d"),
|
||||||
|
*GetNameSafe(GetOwner()), *Item.ItemId.ToString(), Added);
|
||||||
|
|
||||||
|
if (Item.Quantity <= 0)
|
||||||
|
{
|
||||||
|
Provider->NotifyItemTaken(GetOwner());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Provider->UpdateProvidedItem(Item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::ExecuteActionClass(const FInteractionContext& Context, const FInteractionOption& Option)
|
||||||
|
{
|
||||||
|
if (!Context.TargetInteractable)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (UInteractionAction* Action : Context.TargetInteractable->AvailableActions)
|
||||||
|
{
|
||||||
|
if (Action && Action->ActionTag == Option.ActionTag)
|
||||||
|
{
|
||||||
|
FText Reason;
|
||||||
|
if (!Action->CanExecute(Context, Reason))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: %s"),
|
||||||
|
*Option.ActionTag.ToString(), *Reason.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Action->Execute(Context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cross-inventory transfer (loot) ---
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::ExecuteTransfer(const FInteractionContext& Context, const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
UInventoryComponent* PlayerInv = Context.PlayerInventory;
|
||||||
|
UInventoryComponent* StorageInv = Context.TargetActor ? Context.TargetActor->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!PlayerInv)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which inventory currently holds the item -> source; the other -> dest.
|
||||||
|
FItemInstanceData Item;
|
||||||
|
UInventoryComponent* Source = nullptr;
|
||||||
|
UInventoryComponent* Dest = nullptr;
|
||||||
|
if (PlayerInv->FindItem(Request.ItemInstanceId, Item)) { Source = PlayerInv; Dest = StorageInv; }
|
||||||
|
else if (StorageInv && StorageInv->FindItem(Request.ItemInstanceId, Item)) { Source = StorageInv; Dest = PlayerInv; }
|
||||||
|
|
||||||
|
if (!Source || !Dest || !Item.IsValid() || Item.Lock.bLocked)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the destination container.
|
||||||
|
FGuid DestContainer = Request.TargetContainerId;
|
||||||
|
if (!DestContainer.IsValid() || !Dest->CanAcceptItem(Item, DestContainer))
|
||||||
|
{
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
Dest->GetContainers(Containers);
|
||||||
|
DestContainer = FGuid();
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (Dest->CanAcceptItem(Item, C.ContainerId)) { DestContainer = C.ContainerId; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!DestContainer.IsValid())
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Transfer failed: no space."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic remove-from-source + add-to-dest (rolls back if either fails).
|
||||||
|
FItemTransaction Tx;
|
||||||
|
Tx.Instigator = GetOwner();
|
||||||
|
Tx.Remove(Source, Item.InstanceId).Add(Dest, Item);
|
||||||
|
if (!Tx.Apply())
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Transfer transaction failed."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemInteraction, Log, TEXT("[Inventory] transferred %s x%d"), *Item.ItemId.ToString(), Item.Quantity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inventory UI operation entry points ---
|
||||||
|
|
||||||
|
void UItemInteractionComponent::RequestMoveItem(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot)
|
||||||
|
{
|
||||||
|
Server_MoveItem(StorageActor, InstanceId, ToContainerId, ToSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::RequestSplitStack(FGuid InstanceId, int32 Amount)
|
||||||
|
{
|
||||||
|
Server_SplitStack(InstanceId, Amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::RequestMergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId)
|
||||||
|
{
|
||||||
|
Server_MergeStack(SourceInstanceId, TargetInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::RequestDropFromInventory(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
Server_DropFromInventory(InstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_MoveItem_Implementation(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot)
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPawn();
|
||||||
|
UInventoryComponent* PlayerInv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!PlayerInv)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StorageActor)
|
||||||
|
{
|
||||||
|
// Cross-inventory transfer; reuse the validated transfer path.
|
||||||
|
FInteractionContext Ctx = UInteractionResolver::BuildContext(Pawn, StorageActor);
|
||||||
|
FInteractionRequest Req;
|
||||||
|
Req.ItemInstanceId = InstanceId;
|
||||||
|
Req.TargetContainerId = ToContainerId;
|
||||||
|
ExecuteTransfer(Ctx, Req);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rearrange within the player's own inventory.
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (PlayerInv->FindItem(InstanceId, Item))
|
||||||
|
{
|
||||||
|
PlayerInv->MoveItem(InstanceId, Item.CurrentContainerId, ToContainerId.IsValid() ? ToContainerId : Item.CurrentContainerId, ToSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_SplitStack_Implementation(FGuid InstanceId, int32 Amount)
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPawn();
|
||||||
|
if (UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr)
|
||||||
|
{
|
||||||
|
Inv->SplitStack(InstanceId, Amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_MergeStack_Implementation(FGuid SourceInstanceId, FGuid TargetInstanceId)
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPawn();
|
||||||
|
if (UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr)
|
||||||
|
{
|
||||||
|
Inv->MergeStack(SourceInstanceId, TargetInstanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_DropFromInventory_Implementation(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
HandleDropFromInventory(InstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemInteractionComponent::HandleDropFromInventory(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
// Base cannot spawn a world pickup (that lives in ItemWorld). Overridden there.
|
||||||
|
UE_LOG(LogItemInteraction, Verbose, TEXT("HandleDropFromInventory: no world handler bound (use WorldInteractionComponent)."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Timed / hold actions (section 19) ---
|
||||||
|
|
||||||
|
void UItemInteractionComponent::BeginTimedAction(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
bHasPending = true;
|
||||||
|
bPendingHold = Option.bRequiresHold;
|
||||||
|
PendingContext = Context;
|
||||||
|
PendingOption = Option;
|
||||||
|
PendingRequest = Request;
|
||||||
|
|
||||||
|
const float Duration = Option.ActionDuration > 0.f ? Option.ActionDuration : Option.HoldDuration;
|
||||||
|
PendingEndTime = GetWorld()->GetTimeSeconds() + FMath::Max(0.05f, Duration);
|
||||||
|
|
||||||
|
// Interrupt radius: the interactable reach + tolerance.
|
||||||
|
float MaxDist = 99999.f;
|
||||||
|
if (Context.TargetInteractable)
|
||||||
|
{
|
||||||
|
MaxDist = Context.TargetInteractable->InteractionDistance + ServerDistanceTolerance;
|
||||||
|
}
|
||||||
|
PendingMaxDistanceSq = FMath::Square(MaxDist);
|
||||||
|
|
||||||
|
LockActionItem(true);
|
||||||
|
|
||||||
|
ActiveAction.bActive = true;
|
||||||
|
ActiveAction.ActionTag = Option.ActionTag;
|
||||||
|
ActiveAction.Duration = Duration;
|
||||||
|
OnActionStarted.Broadcast(Option.ActionTag, Duration); // server-side listeners
|
||||||
|
|
||||||
|
SetComponentTickEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::CompleteTimedAction()
|
||||||
|
{
|
||||||
|
const FInteractionOption Option = PendingOption;
|
||||||
|
const FInteractionContext Context = PendingContext;
|
||||||
|
const FInteractionRequest Request = PendingRequest;
|
||||||
|
|
||||||
|
LockActionItem(false);
|
||||||
|
bHasPending = false;
|
||||||
|
ActiveAction = FActiveInteractionState();
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
|
||||||
|
const bool bSuccess = ExecuteAuthorized(Context, Option, Request);
|
||||||
|
OnActionFinished.Broadcast(Option.ActionTag, bSuccess);
|
||||||
|
Client_InteractionResult(Request.RequestId, bSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::AbortTimedAction()
|
||||||
|
{
|
||||||
|
if (!bHasPending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const FInteractionRequest Request = PendingRequest;
|
||||||
|
const FGameplayTag Tag = PendingOption.ActionTag;
|
||||||
|
|
||||||
|
LockActionItem(false);
|
||||||
|
bHasPending = false;
|
||||||
|
ActiveAction = FActiveInteractionState();
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
|
||||||
|
OnActionFinished.Broadcast(Tag, false);
|
||||||
|
Client_InteractionResult(Request.RequestId, false);
|
||||||
|
UE_LOG(LogItemInteraction, Verbose, TEXT("[Interaction] action %s interrupted."), *Tag.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::LockActionItem(bool bLock)
|
||||||
|
{
|
||||||
|
APawn* Pawn = GetPawn();
|
||||||
|
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!Inv || !PendingRequest.ItemInstanceId.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Inv->FindItem(PendingRequest.ItemInstanceId, Item))
|
||||||
|
{
|
||||||
|
return; // item not in player inventory (e.g. held/world) - nothing to lock here
|
||||||
|
}
|
||||||
|
if (bLock)
|
||||||
|
{
|
||||||
|
Inv->SetItemLock(PendingRequest.ItemInstanceId, ItemEcosystemTags::Lock_Using, GetOwner(), 0.f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Inv->ClearItemLock(PendingRequest.ItemInstanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::ReleaseHold()
|
||||||
|
{
|
||||||
|
Server_ReleaseHold();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Server_ReleaseHold_Implementation()
|
||||||
|
{
|
||||||
|
if (bHasPending && bPendingHold)
|
||||||
|
{
|
||||||
|
AbortTimedAction(); // released before the hold completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::CancelActiveAction()
|
||||||
|
{
|
||||||
|
if (GetOwner() && GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
AbortTimedAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::OnRep_ActiveAction()
|
||||||
|
{
|
||||||
|
if (ActiveAction.bActive)
|
||||||
|
{
|
||||||
|
OnActionStarted.Broadcast(ActiveAction.ActionTag, ActiveAction.Duration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnActionFinished.Broadcast(ActiveAction.ActionTag, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemInteractionComponent::Client_InteractionResult_Implementation(FGuid RequestId, bool bSuccess)
|
||||||
|
{
|
||||||
|
OnInteractionResult.Broadcast(RequestId, bSuccess);
|
||||||
|
}
|
||||||
8
Source/ItemInteraction/Private/ItemInteractionModule.cpp
Normal file
8
Source/ItemInteraction/Private/ItemInteractionModule.cpp
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemInteractionLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemInteraction);
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemInteraction)
|
||||||
48
Source/ItemInteraction/Public/InteractableComponent.h
Normal file
48
Source/ItemInteraction/Public/InteractableComponent.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "InteractableComponent.generated.h"
|
||||||
|
|
||||||
|
class UInteractionAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks an actor as interactable and lists the actions it offers (section 8.1).
|
||||||
|
* Attach to pickups, chests, vehicles, generators, doors, workbenches, corpses,
|
||||||
|
* placed items, quest objects, etc.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMINTERACTION_API UInteractableComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UInteractableComponent();
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag InteractableType;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FText DisplayName;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float InteractionDistance = 250.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bRequiresLineOfSight = true;
|
||||||
|
|
||||||
|
/** Free-form world tags exposed to the interaction context (e.g. Vehicle, Generator). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTagContainer InteractionTags;
|
||||||
|
|
||||||
|
/** Explicit action classes this object offers (in addition to data-driven item rows). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Instanced, Category = "Interaction")
|
||||||
|
TArray<TObjectPtr<UInteractionAction>> AvailableActions;
|
||||||
|
|
||||||
|
/** Server-side soft claim so two players don't interact with the same object at once. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bIsBusy = false;
|
||||||
|
};
|
||||||
65
Source/ItemInteraction/Public/InteractionAction.h
Normal file
65
Source/ItemInteraction/Public/InteractionAction.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "InteractionAction.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single interaction verb, authored as an instanced object on an
|
||||||
|
* InteractableComponent (section 16). This is the Blueprint-extensible path:
|
||||||
|
* built-in modes are also handled directly by the interaction component, but
|
||||||
|
* any custom verb can be added here without C++.
|
||||||
|
*
|
||||||
|
* CanExecute/Execute must be safe to run on the server. CanExecute may also run
|
||||||
|
* on the client for predictive UI, so it must not mutate state.
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract, Blueprintable, EditInlineNew, DefaultToInstanced, CollapseCategories)
|
||||||
|
class ITEMINTERACTION_API UInteractionAction : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
FGameplayTag ActionTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
FText DisplayText;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
EItemInteractionMode Mode = EItemInteractionMode::Use;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
int32 Priority = 300;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
bool bRequiresHold = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
float HoldDuration = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
FInteractionExecutionData Execution;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Action")
|
||||||
|
TArray<FInteractionRequirement> Requirements;
|
||||||
|
|
||||||
|
/** Pure check: are all requirements satisfied? Writes the first failure reason. */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Action")
|
||||||
|
bool CanExecute(const FInteractionContext& Context, FText& OutDisabledReason) const;
|
||||||
|
virtual bool CanExecute_Implementation(const FInteractionContext& Context, FText& OutDisabledReason) const;
|
||||||
|
|
||||||
|
/** Server-authoritative effect. */
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Action")
|
||||||
|
void Execute(const FInteractionContext& Context);
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context);
|
||||||
|
|
||||||
|
/** Turns this action into a UI option for the given context. */
|
||||||
|
FInteractionOption BuildOption(const FInteractionContext& Context) const;
|
||||||
|
|
||||||
|
virtual UWorld* GetWorld() const override;
|
||||||
|
};
|
||||||
44
Source/ItemInteraction/Public/InteractionResolver.h
Normal file
44
Source/ItemInteraction/Public/InteractionResolver.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "InteractionResolver.generated.h"
|
||||||
|
|
||||||
|
class APawn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central, stateless resolver (section 8.3). Given who is interacting and what
|
||||||
|
* they look at, it produces the ordered list of available options. Run on the
|
||||||
|
* client for predictive UI AND re-run on the server to authorize a request, so
|
||||||
|
* it must be deterministic and side-effect free.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMINTERACTION_API UInteractionResolver : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Assembles the interaction context for an instigator/target pair. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction", meta = (WorldContext = "Instigator"))
|
||||||
|
static FInteractionContext BuildContext(APawn* Instigator, AActor* TargetActor);
|
||||||
|
|
||||||
|
/** All options available, highest priority first (section 8.6). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
static void GetAvailableInteractions(APawn* Instigator, AActor* TargetActor, TArray<FInteractionOption>& OutOptions);
|
||||||
|
|
||||||
|
/** Convenience: the single best (highest priority, enabled) option. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
static bool GetPrimaryInteraction(APawn* Instigator, AActor* TargetActor, FInteractionOption& OutOption);
|
||||||
|
|
||||||
|
/** Finds the held item on an instigator via IHeldItemInterface (actor or component). */
|
||||||
|
static bool ResolveHeldItem(APawn* Instigator, FItemInstanceData& OutItem);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void GatherCapabilityOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out);
|
||||||
|
static void GatherContainerOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out);
|
||||||
|
static void GatherDataDrivenOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out);
|
||||||
|
static void GatherActionClassOptions(const FInteractionContext& Ctx, TArray<FInteractionOption>& Out);
|
||||||
|
};
|
||||||
60
Source/ItemInteraction/Public/InteractionTraceComponent.h
Normal file
60
Source/ItemInteraction/Public/InteractionTraceComponent.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "InteractionTraceComponent.generated.h"
|
||||||
|
|
||||||
|
class UInteractableComponent;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionTargetChanged, AActor*, NewTarget, UInteractableComponent*, NewInteractable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local-only aim trace that finds the best interactable under the crosshair
|
||||||
|
* (section 8.2). Runs only on the owning client; the result drives the prompt UI
|
||||||
|
* and is what the player confirms - never trusted by the server.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMINTERACTION_API UInteractionTraceComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UInteractionTraceComponent();
|
||||||
|
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
/** Fires the trace once and updates CurrentTarget. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void TickTrace();
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
float TraceDistance = 300.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
float TraceRadius = 12.f;
|
||||||
|
|
||||||
|
/** Trace channel; defaults to Visibility. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<AActor> CurrentTarget = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<UInteractableComponent> CurrentInteractable = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FVector LastHitLocation = FVector::ZeroVector;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FVector LastHitNormal = FVector::ZeroVector;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnInteractionTargetChanged OnTargetChanged;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
|
||||||
|
void SetTarget(AActor* NewTarget, UInteractableComponent* NewInteractable);
|
||||||
|
};
|
||||||
215
Source/ItemInteraction/Public/InteractionTypes.h
Normal file
215
Source/ItemInteraction/Public/InteractionTypes.h
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "InteractionTypes.generated.h"
|
||||||
|
|
||||||
|
class APlayerController;
|
||||||
|
class ACharacter;
|
||||||
|
class UInteractableComponent;
|
||||||
|
class UInventoryComponent;
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client -> server intent (section 9). The client never mutates state; it sends
|
||||||
|
* this and the server re-validates everything before executing.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINTERACTION_API FInteractionRequest
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGuid RequestId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
EItemInteractionMode Mode = EItemInteractionMode::None;
|
||||||
|
|
||||||
|
/** The verb that produced this request; lets the server pick the matching action. */
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGameplayTag ActionTag;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
TObjectPtr<AActor> TargetActor = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGuid ItemInstanceId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGuid SourceContainerId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGuid TargetContainerId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
int32 TargetSlotIndex = INDEX_NONE;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGameplayTag TargetSlotTag;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FTransform DesiredTransform = FTransform::Identity;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
int32 Quantity = 1;
|
||||||
|
|
||||||
|
/** Last inventory revision the client had; lets the server detect desync. */
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category = "Interaction")
|
||||||
|
int32 ClientRevision = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single interaction the player could perform on the current target (section 8.4).
|
||||||
|
* Built by the resolver, shown by the UI, and carries the ready-to-send Request.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINTERACTION_API FInteractionOption
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag ActionTag;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
EItemInteractionMode Mode = EItemInteractionMode::None;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FText DisplayText;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<UTexture2D> Icon = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
int32 Priority = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bRequiresHold = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float HoldDuration = 0.f;
|
||||||
|
|
||||||
|
/** Duration of the action itself once started (section 19). */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float ActionDuration = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bIsEnabled = true;
|
||||||
|
|
||||||
|
/** Why the option is greyed out, e.g. "Inventory Full", "Requires Wrench". */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FText DisabledReason;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FInteractionRequest Request;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything an action/requirement needs to make a decision (section 17).
|
||||||
|
* Assembled by the resolver on whichever machine is evaluating (client preview
|
||||||
|
* or server validation).
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINTERACTION_API FInteractionContext
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<APlayerController> PlayerController = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<ACharacter> Character = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<AActor> TargetActor = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<UInteractableComponent> TargetInteractable = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
TObjectPtr<UInventoryComponent> PlayerInventory = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bHasHeldItem = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FItemInstanceData HeldItem;
|
||||||
|
|
||||||
|
/** The item the target itself represents (for pickups), if any. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bHasTargetItem = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FItemInstanceData TargetItem;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTagContainer WorldTags;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FVector HitLocation = FVector::ZeroVector;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FVector HitNormal = FVector::ZeroVector;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Requirement type verbs (section 18). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EInteractionRequirementType : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
HasInventorySpace,
|
||||||
|
HasItemWithTag,
|
||||||
|
HasHeldItemWithTag,
|
||||||
|
PropertyGreaterThan,
|
||||||
|
PropertyLessThan,
|
||||||
|
TargetHasFreeSlotTag,
|
||||||
|
NotWet,
|
||||||
|
NotBroken
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One requirement gate on an interaction (section 18). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINTERACTION_API FInteractionRequirement
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
EInteractionRequirementType RequirementType = EInteractionRequirementType::None;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FGameplayTag RequiredTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FName RequiredItemId;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
float RequiredValue = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
FText FailureText;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Timing / interruption data for an action (section 19). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINTERACTION_API FInteractionExecutionData
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
bool bHasDuration = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
float Duration = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
bool bCanBeInterrupted = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
bool bLockItemDuringAction = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
|
||||||
|
bool bLockTargetDuringAction = false;
|
||||||
|
};
|
||||||
72
Source/ItemInteraction/Public/ItemInteractionActions.h
Normal file
72
Source/ItemInteraction/Public/ItemInteractionActions.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "ItemInteractionActions.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concrete interaction actions (section 16). These are the Blueprint-droppable
|
||||||
|
* authoring units: add one to an InteractableComponent's AvailableActions and the
|
||||||
|
* resolver/executor will surface and run it. Built-in modes are also dispatched
|
||||||
|
* directly by the interaction component, so both paths reach the same effect.
|
||||||
|
*/
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Instant Pickup"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_InstantPickup : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_InstantPickup();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Open Loot UI"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_OpenLootUI : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_OpenLootUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Inspect"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_Inspect : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_Inspect();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Combine"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_Combine : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_Combine();
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Transfer Container Item"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_TransferContainerItem : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_TransferContainerItem();
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Split Stack"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_SplitStack : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_SplitStack();
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Merge Stack"))
|
||||||
|
class ITEMINTERACTION_API UInteractionAction_MergeStack : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_MergeStack();
|
||||||
|
};
|
||||||
178
Source/ItemInteraction/Public/ItemInteractionComponent.h
Normal file
178
Source/ItemInteraction/Public/ItemInteractionComponent.h
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "InteractionTypes.h"
|
||||||
|
#include "ItemInteractionComponent.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionResult, FGuid, RequestId, bool, bSuccess);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnOpenContainer, AActor*, ContainerActor);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActionStarted, FGameplayTag, ActionTag, float, Duration);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActionFinished, FGameplayTag, ActionTag, bool, bCompleted);
|
||||||
|
|
||||||
|
/** Replicated snapshot of a long/hold action in progress (section 19), for UI. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FActiveInteractionState
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
bool bActive = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
FGameplayTag ActionTag;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float Duration = 0.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player-side hub that turns a chosen option into a server request and executes
|
||||||
|
* the authorized result (sections 9, 25). Attach to the player pawn alongside
|
||||||
|
* an InventoryComponent and (optionally) an InteractionTraceComponent.
|
||||||
|
*
|
||||||
|
* Flow: UI/input picks an FInteractionOption -> RequestInteraction() -> Server RPC
|
||||||
|
* -> server re-resolves and re-validates -> executes the transaction -> state
|
||||||
|
* replicates back. The client never changes item state itself.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMINTERACTION_API UItemInteractionComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UItemInteractionComponent();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
/** Client entry point: send the chosen option's request to the server. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void RequestInteraction(const FInteractionOption& Option);
|
||||||
|
|
||||||
|
/** Client: signal that a hold action's button was released (cancels if mid-action). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void ReleaseHold();
|
||||||
|
|
||||||
|
/** Server: cancel any action in progress (bind to damage/stun for interruption). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void CancelActiveAction();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
|
const FActiveInteractionState& GetActiveAction() const { return ActiveAction; }
|
||||||
|
|
||||||
|
/** Lower-level entry: send a hand-built request. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void SendRequest(const FInteractionRequest& Request);
|
||||||
|
|
||||||
|
// --- Inventory UI operations (section 23.3). Client calls, server validates. ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move/transfer an item. StorageActor == null -> rearrange within the player's
|
||||||
|
* own inventory. Otherwise transfer between the player and the storage actor's
|
||||||
|
* inventory (loot). ToContainerId == invalid -> first accepting container.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
void RequestMoveItem(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
void RequestSplitStack(FGuid InstanceId, int32 Amount);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
void RequestMergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
void RequestDropFromInventory(FGuid InstanceId);
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnInteractionResult OnInteractionResult;
|
||||||
|
|
||||||
|
/** Fired locally when the player chooses to open a container - UI opens the loot window. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnOpenContainer OnOpenContainer;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnActionStarted OnActionStarted;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||||
|
FOnActionFinished OnActionFinished;
|
||||||
|
|
||||||
|
/** Max slack (cm) added to the interactable's own distance when server-validating. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
|
float ServerDistanceTolerance = 75.f;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_RequestInteraction(const FInteractionRequest& Request);
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_MoveItem(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot);
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_SplitStack(FGuid InstanceId, int32 Amount);
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_MergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId);
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_DropFromInventory(FGuid InstanceId);
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_ReleaseHold();
|
||||||
|
|
||||||
|
UFUNCTION(Client, Reliable)
|
||||||
|
void Client_InteractionResult(FGuid RequestId, bool bSuccess);
|
||||||
|
|
||||||
|
/** Server hook for dropping an item into the world. Overridden in ItemWorld. */
|
||||||
|
virtual bool HandleDropFromInventory(FGuid InstanceId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Server: full validation pipeline (section 9). Returns the authorized option. */
|
||||||
|
bool AuthorizeRequest(const FInteractionRequest& Request, FInteractionContext& OutContext, FInteractionOption& OutOption) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Server: run a mode directly from an already-built context (used by the named
|
||||||
|
* UInteractionAction_* classes so they share the one execution path). Builds a
|
||||||
|
* minimal option/request and dispatches through ExecuteAuthorized (virtual, so
|
||||||
|
* the ItemWorld subclass handles carry/equip/install/use).
|
||||||
|
*/
|
||||||
|
bool ServerExecuteMode(const FInteractionContext& Context, EItemInteractionMode Mode, FGameplayTag ActionTag);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
APawn* GetPawn() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server: run the authorized option's effect. Virtual so ItemWorld can add the
|
||||||
|
* carry/drop/equip/install/use-on-target modes (which need ItemWorld types)
|
||||||
|
* without ItemInteraction depending on ItemWorld. Base handles pickup + transfer.
|
||||||
|
*/
|
||||||
|
virtual bool ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request);
|
||||||
|
|
||||||
|
bool ExecutePickup(const FInteractionContext& Context);
|
||||||
|
bool ExecuteTransfer(const FInteractionContext& Context, const FInteractionRequest& Request);
|
||||||
|
bool ExecuteActionClass(const FInteractionContext& Context, const FInteractionOption& Option);
|
||||||
|
|
||||||
|
// --- Timed / hold action plumbing (section 19) ---
|
||||||
|
void BeginTimedAction(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request);
|
||||||
|
void CompleteTimedAction();
|
||||||
|
void AbortTimedAction();
|
||||||
|
void LockActionItem(bool bLock);
|
||||||
|
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_ActiveAction)
|
||||||
|
FActiveInteractionState ActiveAction;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_ActiveAction();
|
||||||
|
|
||||||
|
// Server-side pending state.
|
||||||
|
bool bHasPending = false;
|
||||||
|
bool bPendingHold = false;
|
||||||
|
float PendingEndTime = 0.f;
|
||||||
|
float PendingMaxDistanceSq = 0.f;
|
||||||
|
FInteractionContext PendingContext;
|
||||||
|
FInteractionOption PendingOption;
|
||||||
|
FInteractionRequest PendingRequest;
|
||||||
|
};
|
||||||
84
Source/ItemInteraction/Public/ItemInteractionInterfaces.h
Normal file
84
Source/ItemInteraction/Public/ItemInteractionInterfaces.h
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Interface.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemInteractionInterfaces.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by world actors that represent a concrete item (e.g. APickupItemActor).
|
||||||
|
* Lets the interaction layer read/take the item without ItemInteraction depending on
|
||||||
|
* ItemWorld - the dependency only flows World -> Interaction.
|
||||||
|
*/
|
||||||
|
UINTERFACE(MinimalAPI, BlueprintType)
|
||||||
|
class UWorldItemProvider : public UInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
};
|
||||||
|
|
||||||
|
class ITEMINTERACTION_API IWorldItemProvider
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Returns the item this actor carries. */
|
||||||
|
virtual bool GetProvidedItem(FItemInstanceData& OutItem) const = 0;
|
||||||
|
|
||||||
|
/** True if the provider is currently locked/claimed by another interaction. */
|
||||||
|
virtual bool IsItemAvailable() const { return true; }
|
||||||
|
|
||||||
|
/** Server: the item was taken into an inventory/hands - tear down the world actor. */
|
||||||
|
virtual void NotifyItemTaken(AActor* Taker) {}
|
||||||
|
|
||||||
|
/** Server: only part of the stack was taken; write back the remaining item. */
|
||||||
|
virtual void UpdateProvidedItem(const FItemInstanceData& RemainingItem) {}
|
||||||
|
|
||||||
|
/** Server: mark this world item as being physically carried by Holder (or released). */
|
||||||
|
virtual void SetCarried(AActor* Holder, bool bCarried) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by whatever tracks the pawn's currently held/carried item
|
||||||
|
* (the character or its UHeldItemComponent). The resolver queries it to build
|
||||||
|
* "held + target" interactions without depending on ItemWorld.
|
||||||
|
*/
|
||||||
|
UINTERFACE(MinimalAPI, BlueprintType)
|
||||||
|
class UHeldItemInterface : public UInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
};
|
||||||
|
|
||||||
|
class ITEMINTERACTION_API IHeldItemInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual bool GetHeldItem(FItemInstanceData& OutItem) const = 0;
|
||||||
|
virtual bool HasHeldItem() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by world objects that react to a held item being used on them
|
||||||
|
* (section 15): pour fuel into a generator, barricade a door, place bait, etc.
|
||||||
|
* Found on the target actor or any of its components.
|
||||||
|
*/
|
||||||
|
UINTERFACE(MinimalAPI, BlueprintType)
|
||||||
|
class UItemUseTarget : public UInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
};
|
||||||
|
|
||||||
|
class ITEMINTERACTION_API IItemUseTarget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Server: apply the held item to this target for the given result verb (e.g.
|
||||||
|
* "TransferFuel"). May mutate HeldItem (reduce fuel/charge). Returns true if it
|
||||||
|
* did something - the caller then writes HeldItem back to the held component.
|
||||||
|
*/
|
||||||
|
virtual bool ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator) = 0;
|
||||||
|
};
|
||||||
8
Source/ItemInteraction/Public/ItemInteractionLog.h
Normal file
8
Source/ItemInteraction/Public/ItemInteractionLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMINTERACTION_API DECLARE_LOG_CATEGORY_EXTERN(LogItemInteraction, Log, All);
|
||||||
27
Source/ItemInventory/ItemInventory.Build.cs
Normal file
27
Source/ItemInventory/ItemInventory.Build.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemInventory : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemInventory(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"NetCore",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
830
Source/ItemInventory/Private/InventoryComponent.cpp
Normal file
830
Source/ItemInventory/Private/InventoryComponent.cpp
Normal file
@ -0,0 +1,830 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
#include "Engine/ActorChannel.h"
|
||||||
|
|
||||||
|
UInventoryComponent::UInventoryComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
bWantsInitializeComponent = true;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
|
||||||
|
if (bOwnerOnlyReplication)
|
||||||
|
{
|
||||||
|
DOREPLIFETIME_CONDITION(UInventoryComponent, ReplicatedItems, COND_OwnerOnly);
|
||||||
|
DOREPLIFETIME_CONDITION(UInventoryComponent, Containers, COND_OwnerOnly);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DOREPLIFETIME(UInventoryComponent, ReplicatedItems);
|
||||||
|
DOREPLIFETIME(UInventoryComponent, Containers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::OnRegister()
|
||||||
|
{
|
||||||
|
Super::OnRegister();
|
||||||
|
ReplicatedItems.OwnerComponent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::InitializeComponent()
|
||||||
|
{
|
||||||
|
Super::InitializeComponent();
|
||||||
|
ReplicatedItems.OwnerComponent = this;
|
||||||
|
|
||||||
|
if (GetOwner() && GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
for (const FInventoryContainer& Template : DefaultContainers)
|
||||||
|
{
|
||||||
|
AddContainer(Template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::HasAuthorityChecked(const TCHAR* Op) const
|
||||||
|
{
|
||||||
|
if (GetOwner() && GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemInventory, Warning, TEXT("%s called without authority on %s - ignored."),
|
||||||
|
Op, *GetNameSafe(GetOwner()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Containers ---
|
||||||
|
|
||||||
|
FGuid UInventoryComponent::AddContainer(FInventoryContainer Container)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("AddContainer")))
|
||||||
|
{
|
||||||
|
return FGuid();
|
||||||
|
}
|
||||||
|
if (!Container.ContainerId.IsValid())
|
||||||
|
{
|
||||||
|
Container.ContainerId = FGuid::NewGuid();
|
||||||
|
}
|
||||||
|
Containers.Add(Container);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return Container.ContainerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
FGuid UInventoryComponent::AddContainerByTag(FGameplayTag ContainerTag)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("AddContainerByTag")))
|
||||||
|
{
|
||||||
|
return FGuid();
|
||||||
|
}
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
FContainerDefRow Def;
|
||||||
|
if (!DB || !DB->FindContainerDef(ContainerTag, Def))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInventory, Warning, TEXT("AddContainerByTag: no DT_Containers row for %s"), *ContainerTag.ToString());
|
||||||
|
return FGuid();
|
||||||
|
}
|
||||||
|
FInventoryContainer Container;
|
||||||
|
Container.ContainerTag = Def.ContainerTag;
|
||||||
|
Container.Type = Def.Type;
|
||||||
|
Container.Width = Def.Width;
|
||||||
|
Container.Height = Def.Height;
|
||||||
|
Container.MaxSlots = Def.MaxSlots;
|
||||||
|
Container.MaxWeight = Def.MaxWeight;
|
||||||
|
Container.AcceptedItemTags = Def.AcceptedItemTags;
|
||||||
|
Container.BlockedItemTags = Def.BlockedItemTags;
|
||||||
|
return AddContainer(Container);
|
||||||
|
}
|
||||||
|
|
||||||
|
FGuid UInventoryComponent::AddNestedContainerForItem(FGuid OwnerItemInstanceId, FGameplayTag ContainerTag)
|
||||||
|
{
|
||||||
|
const FGuid NewId = AddContainerByTag(ContainerTag);
|
||||||
|
if (NewId.IsValid())
|
||||||
|
{
|
||||||
|
if (FInventoryContainer* Container = GetContainerMutable(NewId))
|
||||||
|
{
|
||||||
|
Container->OwnerItemInstanceId = OwnerItemInstanceId;
|
||||||
|
Container->Type = EInventoryContainerType::SlotList; // nested defaults
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::GetContainer(FGuid ContainerId, FInventoryContainer& OutContainer) const
|
||||||
|
{
|
||||||
|
if (const FInventoryContainer* Found = GetContainerConst(ContainerId))
|
||||||
|
{
|
||||||
|
OutContainer = *Found;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FGuid UInventoryComponent::FindContainerByTag(FGameplayTag ContainerTag) const
|
||||||
|
{
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (C.ContainerTag == ContainerTag)
|
||||||
|
{
|
||||||
|
return C.ContainerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
FInventoryContainer* UInventoryComponent::GetContainerMutable(const FGuid& ContainerId)
|
||||||
|
{
|
||||||
|
return Containers.FindByPredicate([&ContainerId](const FInventoryContainer& C) { return C.ContainerId == ContainerId; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const FInventoryContainer* UInventoryComponent::GetContainerConst(const FGuid& ContainerId) const
|
||||||
|
{
|
||||||
|
return Containers.FindByPredicate([&ContainerId](const FInventoryContainer& C) { return C.ContainerId == ContainerId; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Queries ---
|
||||||
|
|
||||||
|
bool UInventoryComponent::IsSlotOccupied(const FGuid& ContainerId, int32 SlotIndex) const
|
||||||
|
{
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId == ContainerId && E.SlotIndex == SlotIndex)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UInventoryComponent::FindFreeSlot(const FGuid& ContainerId) const
|
||||||
|
{
|
||||||
|
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||||
|
if (!Container)
|
||||||
|
{
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 SlotCount = Container->GetSlotCount();
|
||||||
|
if (SlotCount > 0)
|
||||||
|
{
|
||||||
|
for (int32 Slot = 0; Slot < SlotCount; ++Slot)
|
||||||
|
{
|
||||||
|
if (!IsSlotOccupied(ContainerId, Slot))
|
||||||
|
{
|
||||||
|
return Slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INDEX_NONE; // full
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlimited (weight-gated) list: append after the highest used index.
|
||||||
|
int32 MaxUsed = INDEX_NONE;
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId == ContainerId)
|
||||||
|
{
|
||||||
|
MaxUsed = FMath::Max(MaxUsed, E.SlotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MaxUsed + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FIntPoint UInventoryComponent::GetItemGridSize(FName ItemId) const
|
||||||
|
{
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(ItemId))
|
||||||
|
{
|
||||||
|
return FIntPoint(FMath::Max(1, Def->GridSize.X), FMath::Max(1, Def->GridSize.Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FIntPoint(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::IsGridRegionFree(const FInventoryContainer& Container, int32 TopLeft, FIntPoint Size, const FGuid& IgnoreInstance) const
|
||||||
|
{
|
||||||
|
const int32 W = FMath::Max(1, Container.Width);
|
||||||
|
const int32 H = FMath::Max(1, Container.Height);
|
||||||
|
const int32 X0 = TopLeft % W;
|
||||||
|
const int32 Y0 = TopLeft / W;
|
||||||
|
if (X0 + Size.X > W || Y0 + Size.Y > H)
|
||||||
|
{
|
||||||
|
return false; // would overflow the grid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build an occupancy mask from existing entries' footprints.
|
||||||
|
TArray<bool> Occupied;
|
||||||
|
Occupied.Init(false, W * H);
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId != Container.ContainerId || E.Item.InstanceId == IgnoreInstance || E.SlotIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const FIntPoint ES = GetItemGridSize(E.Item.ItemId);
|
||||||
|
const int32 EX = E.SlotIndex % W;
|
||||||
|
const int32 EY = E.SlotIndex / W;
|
||||||
|
for (int32 dy = 0; dy < ES.Y; ++dy)
|
||||||
|
{
|
||||||
|
for (int32 dx = 0; dx < ES.X; ++dx)
|
||||||
|
{
|
||||||
|
const int32 CellX = EX + dx;
|
||||||
|
const int32 CellY = EY + dy;
|
||||||
|
if (CellX < W && CellY < H) { Occupied[CellY * W + CellX] = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 dy = 0; dy < Size.Y; ++dy)
|
||||||
|
{
|
||||||
|
for (int32 dx = 0; dx < Size.X; ++dx)
|
||||||
|
{
|
||||||
|
if (Occupied[(Y0 + dy) * W + (X0 + dx)]) { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UInventoryComponent::FindFreeSlotForItem(const FGuid& ContainerId, const FItemInstanceData& Item) const
|
||||||
|
{
|
||||||
|
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||||
|
if (!Container)
|
||||||
|
{
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
if (Container->Type != EInventoryContainerType::Grid)
|
||||||
|
{
|
||||||
|
return FindFreeSlot(ContainerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 W = FMath::Max(1, Container->Width);
|
||||||
|
const int32 H = FMath::Max(1, Container->Height);
|
||||||
|
const FIntPoint Size = GetItemGridSize(Item.ItemId);
|
||||||
|
for (int32 Cell = 0; Cell < W * H; ++Cell)
|
||||||
|
{
|
||||||
|
if (IsGridRegionFree(*Container, Cell, Size, FGuid()))
|
||||||
|
{
|
||||||
|
return Cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UInventoryComponent::GetContainerWeight(FGuid ContainerId) const
|
||||||
|
{
|
||||||
|
float Total = 0.f;
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId != ContainerId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (DB)
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(E.Item.ItemId))
|
||||||
|
{
|
||||||
|
Total += Def->Weight * E.Item.Quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UInventoryComponent::GetItemCount(FName ItemId) const
|
||||||
|
{
|
||||||
|
int32 Count = 0;
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.Item.ItemId == ItemId)
|
||||||
|
{
|
||||||
|
Count += E.Item.Quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::FindItem(FGuid InstanceId, FItemInstanceData& OutItem) const
|
||||||
|
{
|
||||||
|
if (const FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId))
|
||||||
|
{
|
||||||
|
OutItem = Entry->Item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::GetContainerItems(FGuid ContainerId, TArray<FItemInstanceData>& OutItems) const
|
||||||
|
{
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId == ContainerId)
|
||||||
|
{
|
||||||
|
OutItems.Add(E.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::GetAllItems(TArray<FItemInstanceData>& OutItems) const
|
||||||
|
{
|
||||||
|
OutItems.Reserve(ReplicatedItems.Entries.Num());
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
OutItems.Add(E.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::CanAcceptItem(const FItemInstanceData& Item, FGuid ContainerId) const
|
||||||
|
{
|
||||||
|
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||||
|
if (!Container || !Item.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (!Def)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capability: storage-style containers respect bCanStoreInInventory.
|
||||||
|
const bool bStorageStyle =
|
||||||
|
Container->Type == EInventoryContainerType::SlotList ||
|
||||||
|
Container->Type == EInventoryContainerType::Grid ||
|
||||||
|
Container->Type == EInventoryContainerType::Hotbar;
|
||||||
|
if (bStorageStyle && !Def->bCanStoreInInventory)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag gating.
|
||||||
|
FGameplayTagContainer ItemTags = Def->ItemTags;
|
||||||
|
ItemTags.AddTag(Def->ItemType);
|
||||||
|
if (!Container->AcceptedItemTags.IsEmpty() && !ItemTags.HasAny(Container->AcceptedItemTags))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Container->BlockedItemTags.IsEmpty() && ItemTags.HasAny(Container->BlockedItemTags))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight.
|
||||||
|
if (Container->HasWeightLimit())
|
||||||
|
{
|
||||||
|
const float Projected = GetContainerWeight(ContainerId) + Def->Weight * Item.Quantity;
|
||||||
|
if (Projected > Container->MaxWeight)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space: a free slot, or an existing stack of the same item with headroom.
|
||||||
|
if (Container->HasSlotLimit())
|
||||||
|
{
|
||||||
|
if (FindFreeSlotForItem(ContainerId, Item) != INDEX_NONE)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const int32 MaxStack = FMath::Max(1, Def->MaxStack);
|
||||||
|
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId == ContainerId && E.Item.CanStackWith(Item) && E.Item.Quantity < MaxStack)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mutations ---
|
||||||
|
|
||||||
|
void UInventoryComponent::StackIntoExisting(const FGuid& ContainerId, FItemInstanceData& Item, int32& Remaining)
|
||||||
|
{
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const int32 MaxStack = DB ? DB->GetMaxStack(Item.ItemId) : 1;
|
||||||
|
if (MaxStack <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (Remaining <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (E.ContainerId == ContainerId && E.Item.CanStackWith(Item) && E.Item.Quantity < MaxStack)
|
||||||
|
{
|
||||||
|
const int32 Space = MaxStack - E.Item.Quantity;
|
||||||
|
const int32 Moved = FMath::Min(Space, Remaining);
|
||||||
|
E.Item.Quantity += Moved;
|
||||||
|
Remaining -= Moved;
|
||||||
|
ReplicatedItems.MarkItemDirty(E);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::AddItem(FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("AddItem")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (CanAcceptItem(Item, C.ContainerId))
|
||||||
|
{
|
||||||
|
return AddItemToContainer(Item, C.ContainerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemInventory, Verbose, TEXT("AddItem: no container accepts %s x%d"), *Item.ItemId.ToString(), Item.Quantity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::StripServerOnlyProperties(FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
if (!DB)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FGameplayTag> ServerOnly;
|
||||||
|
DB->GetServerOnlyProperties(Item.ItemId, ServerOnly);
|
||||||
|
if (ServerOnly.Num() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FItemRuntimeProperties& Side = ServerOnlyProps.FindOrAdd(Item.InstanceId);
|
||||||
|
for (const FGameplayTag& Tag : ServerOnly)
|
||||||
|
{
|
||||||
|
if (Item.RuntimeProperties.HasFloat(Tag))
|
||||||
|
{
|
||||||
|
Side.SetFloat(Tag, Item.RuntimeProperties.GetFloat(Tag));
|
||||||
|
Item.RuntimeProperties.RemoveFloat(Tag); // never replicated to clients
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float UInventoryComponent::GetServerOnlyFloat(FGuid InstanceId, FGameplayTag PropertyTag, float Fallback) const
|
||||||
|
{
|
||||||
|
if (const FItemRuntimeProperties* Side = ServerOnlyProps.Find(InstanceId))
|
||||||
|
{
|
||||||
|
return Side->GetFloat(PropertyTag, Fallback);
|
||||||
|
}
|
||||||
|
return Fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::AddItemToContainer(FItemInstanceData& Item, FGuid ContainerId)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("AddItemToContainer")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||||
|
if (!Container || !Item.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item.EnsureInstanceId();
|
||||||
|
StripServerOnlyProperties(Item);
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const int32 MaxStack = DB ? DB->GetMaxStack(Item.ItemId) : 1;
|
||||||
|
|
||||||
|
int32 Remaining = Item.Quantity;
|
||||||
|
|
||||||
|
// 1) Top up compatible existing stacks.
|
||||||
|
StackIntoExisting(ContainerId, Item, Remaining);
|
||||||
|
|
||||||
|
// 2) Place leftovers into fresh slots, one stack at a time.
|
||||||
|
bool bFirstNewStack = true;
|
||||||
|
while (Remaining > 0)
|
||||||
|
{
|
||||||
|
const int32 Slot = FindFreeSlotForItem(ContainerId, Item);
|
||||||
|
if (Slot == INDEX_NONE)
|
||||||
|
{
|
||||||
|
break; // container full
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 ThisStack = FMath::Min(MaxStack, Remaining);
|
||||||
|
FItemInstanceData NewStack = Item;
|
||||||
|
NewStack.Quantity = ThisStack;
|
||||||
|
NewStack.LocationType = EItemLocationType::Inventory;
|
||||||
|
// First fresh stack keeps the incoming instance id; subsequent stacks get new ids.
|
||||||
|
NewStack.InstanceId = bFirstNewStack ? Item.InstanceId : FGuid::NewGuid();
|
||||||
|
bFirstNewStack = false;
|
||||||
|
|
||||||
|
FRepInventoryEntry& Entry = ReplicatedItems.AddOrUpdate(ContainerId, Slot, NewStack);
|
||||||
|
NotifyItemAdded(Entry.Item);
|
||||||
|
|
||||||
|
Remaining -= ThisStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 AddedCount = Item.Quantity - Remaining;
|
||||||
|
Item.Quantity = Remaining; // report leftovers back to caller
|
||||||
|
if (AddedCount > 0)
|
||||||
|
{
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
return Remaining == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::RemoveItem(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("RemoveItem")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData Removed;
|
||||||
|
const bool bFound = FindItem(InstanceId, Removed);
|
||||||
|
if (bFound && Removed.Lock.bLocked)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInventory, Verbose, TEXT("RemoveItem blocked: %s is locked."), *InstanceId.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ReplicatedItems.RemoveByInstanceId(InstanceId))
|
||||||
|
{
|
||||||
|
NotifyItemRemoved(Removed);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::RemoveQuantity(FGuid InstanceId, int32 Amount)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("RemoveQuantity")) || Amount <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||||
|
if (!Entry || Entry->Item.Lock.bLocked)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Amount >= Entry->Item.Quantity)
|
||||||
|
{
|
||||||
|
return RemoveItem(InstanceId);
|
||||||
|
}
|
||||||
|
Entry->Item.Quantity -= Amount;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::MoveItem(FGuid InstanceId, FGuid FromContainerId, FGuid ToContainerId, int32 TargetSlot)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("MoveItem")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||||
|
if (!Entry || Entry->ContainerId != FromContainerId || Entry->Item.Lock.bLocked)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!CanAcceptItem(Entry->Item, ToContainerId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve target slot.
|
||||||
|
if (TargetSlot == INDEX_NONE)
|
||||||
|
{
|
||||||
|
TargetSlot = FindFreeSlot(ToContainerId);
|
||||||
|
if (TargetSlot == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Occupant handling: merge if stackable, else swap.
|
||||||
|
FRepInventoryEntry* Occupant = nullptr;
|
||||||
|
for (FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||||
|
{
|
||||||
|
if (E.ContainerId == ToContainerId && E.SlotIndex == TargetSlot && E.Item.InstanceId != InstanceId)
|
||||||
|
{
|
||||||
|
Occupant = &E;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Occupant)
|
||||||
|
{
|
||||||
|
if (Occupant->Item.CanStackWith(Entry->Item))
|
||||||
|
{
|
||||||
|
return MergeStack(InstanceId, Occupant->Item.InstanceId);
|
||||||
|
}
|
||||||
|
// Swap positions.
|
||||||
|
const FGuid FromC = Entry->ContainerId;
|
||||||
|
const int32 FromS = Entry->SlotIndex;
|
||||||
|
Occupant->ContainerId = FromC;
|
||||||
|
Occupant->SlotIndex = FromS;
|
||||||
|
Occupant->Item.CurrentContainerId = FromC;
|
||||||
|
Occupant->Item.SlotIndex = FromS;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Occupant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry->ContainerId = ToContainerId;
|
||||||
|
Entry->SlotIndex = TargetSlot;
|
||||||
|
Entry->Item.CurrentContainerId = ToContainerId;
|
||||||
|
Entry->Item.SlotIndex = TargetSlot;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::SplitStack(FGuid InstanceId, int32 Amount)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("SplitStack")) || Amount <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||||
|
if (!Entry || Entry->Item.Lock.bLocked || Amount >= Entry->Item.Quantity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FGuid ContainerId = Entry->ContainerId;
|
||||||
|
const int32 FreeSlot = FindFreeSlotForItem(ContainerId, Entry->Item);
|
||||||
|
if (FreeSlot == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry->Item.Quantity -= Amount;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
|
||||||
|
FItemInstanceData NewStack = Entry->Item;
|
||||||
|
NewStack.InstanceId = FGuid::NewGuid();
|
||||||
|
NewStack.Quantity = Amount;
|
||||||
|
FRepInventoryEntry& Added = ReplicatedItems.AddOrUpdate(ContainerId, FreeSlot, NewStack);
|
||||||
|
NotifyItemAdded(Added.Item);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::MergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("MergeStack")) || SourceInstanceId == TargetInstanceId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Source = ReplicatedItems.FindByInstanceId(SourceInstanceId);
|
||||||
|
FRepInventoryEntry* Target = ReplicatedItems.FindByInstanceId(TargetInstanceId);
|
||||||
|
if (!Source || !Target || !Target->Item.CanStackWith(Source->Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const int32 MaxStack = DB ? DB->GetMaxStack(Target->Item.ItemId) : 1;
|
||||||
|
const int32 Space = MaxStack - Target->Item.Quantity;
|
||||||
|
if (Space <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Moved = FMath::Min(Space, Source->Item.Quantity);
|
||||||
|
Target->Item.Quantity += Moved;
|
||||||
|
Source->Item.Quantity -= Moved;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Target);
|
||||||
|
|
||||||
|
if (Source->Item.Quantity <= 0)
|
||||||
|
{
|
||||||
|
const FItemInstanceData Removed = Source->Item;
|
||||||
|
ReplicatedItems.RemoveByInstanceId(SourceInstanceId);
|
||||||
|
NotifyItemRemoved(Removed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReplicatedItems.MarkItemDirty(*Source);
|
||||||
|
}
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::UpdateItem(const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("UpdateItem")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(Item.InstanceId);
|
||||||
|
if (!Entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData Stored = Item;
|
||||||
|
StripServerOnlyProperties(Stored);
|
||||||
|
Entry->Item = Stored;
|
||||||
|
Entry->Item.CurrentContainerId = Entry->ContainerId;
|
||||||
|
Entry->Item.SlotIndex = Entry->SlotIndex;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::SetItemLock(FGuid InstanceId, FGameplayTag Reason, AActor* LockedBy, float ExpireWorldTime)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("SetItemLock")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||||
|
if (!Entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Entry->Item.Lock.bLocked = true;
|
||||||
|
Entry->Item.Lock.LockReason = Reason;
|
||||||
|
Entry->Item.Lock.LockedBy = LockedBy;
|
||||||
|
Entry->Item.Lock.LockExpireTime = ExpireWorldTime;
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UInventoryComponent::ClearItemLock(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("ClearItemLock")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||||
|
if (!Entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Entry->Item.Lock.Clear();
|
||||||
|
ReplicatedItems.MarkItemDirty(*Entry);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::SetItemInSlot(const FItemInstanceData& Item, FGuid ContainerId, int32 SlotIndex)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("SetItemInSlot")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FItemInstanceData Copy = Item;
|
||||||
|
Copy.LocationType = EItemLocationType::Inventory;
|
||||||
|
StripServerOnlyProperties(Copy);
|
||||||
|
ReplicatedItems.AddOrUpdate(ContainerId, SlotIndex, Copy);
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::ClearAllItems()
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("ClearAllItems")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ReplicatedItems.Entries.Reset();
|
||||||
|
ReplicatedItems.MarkArrayDirty();
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::SetContainers(const TArray<FInventoryContainer>& InContainers)
|
||||||
|
{
|
||||||
|
if (!HasAuthorityChecked(TEXT("SetContainers")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Containers = InContainers;
|
||||||
|
NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Notifications ---
|
||||||
|
|
||||||
|
void UInventoryComponent::NotifyInventoryChanged()
|
||||||
|
{
|
||||||
|
OnInventoryChanged.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::NotifyItemAdded(const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
OnItemAdded.Broadcast(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInventoryComponent::NotifyItemRemoved(const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
OnItemRemoved.Broadcast(Item);
|
||||||
|
}
|
||||||
98
Source/ItemInventory/Private/ItemCraftingLibrary.cpp
Normal file
98
Source/ItemInventory/Private/ItemCraftingLibrary.cpp
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemCraftingLibrary.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemContentRows.h"
|
||||||
|
|
||||||
|
bool UItemCraftingLibrary::CanCraft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation)
|
||||||
|
{
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
if (!DB || !Inventory)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FCraftingRecipeRow Recipe;
|
||||||
|
if (!DB->FindRecipe(RecipeId, Recipe))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Recipe.StationTag.IsValid() && !AvailableStation.MatchesTag(Recipe.StationTag))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const FCraftingIngredient& In : Recipe.Inputs)
|
||||||
|
{
|
||||||
|
if (Inventory->GetItemCount(In.ItemId) < In.Quantity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemCraftingLibrary::Craft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation)
|
||||||
|
{
|
||||||
|
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!CanCraft(WorldContext, Inventory, RecipeId, AvailableStation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
FCraftingRecipeRow Recipe;
|
||||||
|
DB->FindRecipe(RecipeId, Recipe);
|
||||||
|
|
||||||
|
// Consume inputs (already verified present).
|
||||||
|
for (const FCraftingIngredient& In : Recipe.Inputs)
|
||||||
|
{
|
||||||
|
if (!ConsumeItem(Inventory, In.ItemId, In.Quantity))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInventory, Error, TEXT("Craft '%s' failed mid-consume for %s."), *RecipeId.ToString(), *In.ItemId.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemInstanceData Output = DB->MakeItemInstance(Recipe.OutputItemId, FMath::Max(1, Recipe.OutputQuantity));
|
||||||
|
Inventory->AddItem(Output);
|
||||||
|
UE_LOG(LogItemInventory, Log, TEXT("[Crafting] crafted %s"), *Recipe.OutputItemId.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemCraftingLibrary::RollLootIntoInventory(const UObject* WorldContext, UInventoryComponent* Inventory, FName TableId, int32 RollCount)
|
||||||
|
{
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
if (!DB || !Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TArray<FItemInstanceData> Loot;
|
||||||
|
DB->RollLoot(TableId, RollCount, Loot);
|
||||||
|
for (FItemInstanceData& Item : Loot)
|
||||||
|
{
|
||||||
|
Inventory->AddItem(Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemCraftingLibrary::ConsumeItem(UInventoryComponent* Inventory, FName ItemId, int32 Count)
|
||||||
|
{
|
||||||
|
int32 Remaining = Count;
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inventory->GetAllItems(Items);
|
||||||
|
for (const FItemInstanceData& Item : Items)
|
||||||
|
{
|
||||||
|
if (Remaining <= 0) { break; }
|
||||||
|
if (Item.ItemId != ItemId || Item.Lock.bLocked) { continue; }
|
||||||
|
|
||||||
|
const int32 Take = FMath::Min(Remaining, Item.Quantity);
|
||||||
|
if (Inventory->RemoveQuantity(Item.InstanceId, Take))
|
||||||
|
{
|
||||||
|
Remaining -= Take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Remaining <= 0;
|
||||||
|
}
|
||||||
165
Source/ItemInventory/Private/ItemFeatureLibrary.cpp
Normal file
165
Source/ItemInventory/Private/ItemFeatureLibrary.cpp
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemFeatureLibrary.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemAuxiliaryRows.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
float GetWorldSeconds(const UObject* WorldContext)
|
||||||
|
{
|
||||||
|
const UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
||||||
|
return World ? World->GetTimeSeconds() : 0.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemFeatureLibrary::UpdateLazyFeatures(const UObject* WorldContext, FItemInstanceData& Item, FName& OutTransformItemId)
|
||||||
|
{
|
||||||
|
OutTransformItemId = NAME_None;
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
if (!DB)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FItemFeatureRow> RottingRows;
|
||||||
|
DB->GetFeatureRows(Item.ItemId, ItemEcosystemTags::Feature_Rotting, RottingRows);
|
||||||
|
if (RottingRows.Num() == 0)
|
||||||
|
{
|
||||||
|
return false; // not perishable
|
||||||
|
}
|
||||||
|
|
||||||
|
float DecayPerHour = 0.f;
|
||||||
|
FName RottenItemId = NAME_None;
|
||||||
|
for (const FItemFeatureRow& Row : RottingRows)
|
||||||
|
{
|
||||||
|
if (Row.Param == FName("DecayRatePerHour")) { DecayPerHour = Row.GetFloat(); }
|
||||||
|
else if (Row.Param == FName("RottenItemId")) { RottenItemId = FName(*Row.Value); }
|
||||||
|
}
|
||||||
|
if (DecayPerHour <= 0.f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Now = GetWorldSeconds(WorldContext);
|
||||||
|
const float Last = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_LastUpdateTime, Now);
|
||||||
|
float Freshness = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_Freshness, 1.f);
|
||||||
|
|
||||||
|
const float ElapsedHours = FMath::Max(0.f, Now - Last) / 3600.f;
|
||||||
|
Freshness = FMath::Clamp(Freshness - DecayPerHour * ElapsedHours, 0.f, 1.f);
|
||||||
|
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Freshness, Freshness);
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_LastUpdateTime, Now);
|
||||||
|
|
||||||
|
if (Freshness <= 0.f)
|
||||||
|
{
|
||||||
|
Item.StateTags.AddTag(ItemEcosystemTags::State_Rotten);
|
||||||
|
if (!RottenItemId.IsNone())
|
||||||
|
{
|
||||||
|
OutTransformItemId = RottenItemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemFeatureLibrary::UpdateInventoryFeatures(const UObject* WorldContext, UInventoryComponent* Inventory)
|
||||||
|
{
|
||||||
|
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
Inventory->GetAllItems(Items);
|
||||||
|
for (FItemInstanceData& Item : Items)
|
||||||
|
{
|
||||||
|
FName TransformId;
|
||||||
|
if (!UpdateLazyFeatures(WorldContext, Item, TransformId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!TransformId.IsNone())
|
||||||
|
{
|
||||||
|
// Transform into the rotten variant, preserving quantity.
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
FItemInstanceData NewItem = DB ? DB->MakeItemInstance(TransformId, Item.Quantity) : FItemInstanceData(TransformId, Item.Quantity);
|
||||||
|
Inventory->RemoveItem(Item.InstanceId);
|
||||||
|
Inventory->AddItem(NewItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Inventory->UpdateItem(Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemFeatureLibrary::ForceRot(const UObject* WorldContext, FItemInstanceData& Item, FName& OutTransformItemId)
|
||||||
|
{
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Freshness, 0.f);
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_LastUpdateTime, GetWorldSeconds(WorldContext));
|
||||||
|
Item.StateTags.AddTag(ItemEcosystemTags::State_Rotten);
|
||||||
|
|
||||||
|
OutTransformItemId = NAME_None;
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext))
|
||||||
|
{
|
||||||
|
TArray<FItemFeatureRow> Rows;
|
||||||
|
DB->GetFeatureRows(Item.ItemId, ItemEcosystemTags::Feature_Rotting, Rows);
|
||||||
|
for (const FItemFeatureRow& Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row.Param == FName("RottenItemId") && !Row.Value.IsEmpty())
|
||||||
|
{
|
||||||
|
OutTransformItemId = FName(*Row.Value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemFeatureLibrary::BreakItem(FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Durability, 0.f);
|
||||||
|
Item.StateTags.AddTag(ItemEcosystemTags::State_Broken);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemFeatureLibrary::DrainBattery(FItemInstanceData& Item, float DrainPerSecond, float Seconds)
|
||||||
|
{
|
||||||
|
const float Charge = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_BatteryCharge, 1.f);
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_BatteryCharge,
|
||||||
|
FMath::Clamp(Charge - DrainPerSecond * Seconds, 0.f, 1.f));
|
||||||
|
}
|
||||||
|
|
||||||
|
float UItemFeatureLibrary::AddFuel(FItemInstanceData& Item, float Amount, float MaxFuel)
|
||||||
|
{
|
||||||
|
const float Current = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||||
|
const float NewValue = FMath::Clamp(Current + Amount, 0.f, MaxFuel);
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, NewValue);
|
||||||
|
return NewValue - Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UItemFeatureLibrary::TransferFuel(FItemInstanceData& From, FItemInstanceData& To, float Amount, float ToMaxFuel)
|
||||||
|
{
|
||||||
|
const float Available = From.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||||
|
const float ToCurrent = To.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||||
|
const float Room = FMath::Max(0.f, ToMaxFuel - ToCurrent);
|
||||||
|
const float Moved = FMath::Min3(Amount, Available, Room);
|
||||||
|
if (Moved <= 0.f)
|
||||||
|
{
|
||||||
|
return 0.f;
|
||||||
|
}
|
||||||
|
From.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, Available - Moved);
|
||||||
|
To.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, ToCurrent + Moved);
|
||||||
|
return Moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemFeatureLibrary::SetWetness(FItemInstanceData& Item, float Wetness)
|
||||||
|
{
|
||||||
|
const float Clamped = FMath::Clamp(Wetness, 0.f, 1.f);
|
||||||
|
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Wetness, Clamped);
|
||||||
|
if (Clamped > 0.5f) { Item.StateTags.AddTag(ItemEcosystemTags::State_Wet); }
|
||||||
|
else { Item.StateTags.RemoveTag(ItemEcosystemTags::State_Wet); }
|
||||||
|
}
|
||||||
8
Source/ItemInventory/Private/ItemInventoryModule.cpp
Normal file
8
Source/ItemInventory/Private/ItemInventoryModule.cpp
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemInventory);
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemInventory)
|
||||||
79
Source/ItemInventory/Private/ItemSaveLibrary.cpp
Normal file
79
Source/ItemInventory/Private/ItemSaveLibrary.cpp
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemSaveLibrary.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
|
||||||
|
FInventorySaveData UItemSaveLibrary::CaptureInventory(UInventoryComponent* Inventory)
|
||||||
|
{
|
||||||
|
FInventorySaveData Data;
|
||||||
|
if (!Inventory)
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
Inventory->GetAllItems(Data.Items);
|
||||||
|
for (FItemInstanceData& Item : Data.Items)
|
||||||
|
{
|
||||||
|
Item.Lock.Clear(); // never persist transient locks (section 26)
|
||||||
|
}
|
||||||
|
Inventory->GetContainers(Data.Containers);
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UItemSaveLibrary::RestoreInventory(const UObject* WorldContext, UInventoryComponent* Inventory, const FInventorySaveData& Data)
|
||||||
|
{
|
||||||
|
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inventory->SetContainers(Data.Containers);
|
||||||
|
Inventory->ClearAllItems();
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
for (const FItemInstanceData& Saved : Data.Items)
|
||||||
|
{
|
||||||
|
FItemInstanceData Item = Saved;
|
||||||
|
if (DB) { DB->MigrateLoadedItem(Item); }
|
||||||
|
Inventory->SetItemInSlot(Item, Item.CurrentContainerId, Item.SlotIndex);
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemInventory, Log, TEXT("[Save] restored %d items into %s"), Data.Items.Num(), *GetNameSafe(Inventory->GetOwner()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemSaveLibrary::SaveInventoryToSlot(UInventoryComponent* Inventory, const FString& SlotName, FName Key)
|
||||||
|
{
|
||||||
|
if (!Inventory)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UItemSaveGame* Save = Cast<UItemSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||||
|
if (!Save)
|
||||||
|
{
|
||||||
|
Save = Cast<UItemSaveGame>(UGameplayStatics::CreateSaveGameObject(UItemSaveGame::StaticClass()));
|
||||||
|
}
|
||||||
|
if (!Save)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Save->Inventories.Add(Key, CaptureInventory(Inventory));
|
||||||
|
return UGameplayStatics::SaveGameToSlot(Save, SlotName, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UItemSaveLibrary::LoadInventoryFromSlot(const UObject* WorldContext, UInventoryComponent* Inventory, const FString& SlotName, FName Key)
|
||||||
|
{
|
||||||
|
UItemSaveGame* Save = Cast<UItemSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||||
|
if (!Save)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const FInventorySaveData* Data = Save->Inventories.Find(Key);
|
||||||
|
if (!Data)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RestoreInventory(WorldContext, Inventory, *Data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
173
Source/ItemInventory/Private/ItemTransaction.cpp
Normal file
173
Source/ItemInventory/Private/ItemTransaction.cpp
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ItemTransaction.h"
|
||||||
|
#include "ItemInventoryLog.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
|
||||||
|
FItemTransaction& FItemTransaction::Add(UInventoryComponent* Inventory, const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
FItemTransactionStep Step;
|
||||||
|
Step.Type = EItemTransactionStepType::AddItem;
|
||||||
|
Step.Inventory = Inventory;
|
||||||
|
Step.Item = Item;
|
||||||
|
Step.Item.EnsureInstanceId();
|
||||||
|
Step.InstanceId = Step.Item.InstanceId;
|
||||||
|
Steps.Add(Step);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemTransaction& FItemTransaction::Remove(UInventoryComponent* Inventory, const FGuid& InstanceId)
|
||||||
|
{
|
||||||
|
FItemTransactionStep Step;
|
||||||
|
Step.Type = EItemTransactionStepType::RemoveItem;
|
||||||
|
Step.Inventory = Inventory;
|
||||||
|
Step.InstanceId = InstanceId;
|
||||||
|
Steps.Add(Step);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemTransaction& FItemTransaction::Update(UInventoryComponent* Inventory, const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
FItemTransactionStep Step;
|
||||||
|
Step.Type = EItemTransactionStepType::UpdateItem;
|
||||||
|
Step.Inventory = Inventory;
|
||||||
|
Step.Item = Item;
|
||||||
|
Step.InstanceId = Item.InstanceId;
|
||||||
|
Steps.Add(Step);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemTransaction& FItemTransaction::Move(UInventoryComponent* Inventory, const FGuid& InstanceId, const FGuid& ToContainerId, int32 ToSlotIndex)
|
||||||
|
{
|
||||||
|
FItemTransactionStep Step;
|
||||||
|
Step.Type = EItemTransactionStepType::MoveItem;
|
||||||
|
Step.Inventory = Inventory;
|
||||||
|
Step.InstanceId = InstanceId;
|
||||||
|
Step.ToContainer = ToContainerId;
|
||||||
|
Step.ToSlot = ToSlotIndex;
|
||||||
|
Steps.Add(Step);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FItemTransaction::Validate() const
|
||||||
|
{
|
||||||
|
for (const FItemTransactionStep& Step : Steps)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (Step.Type)
|
||||||
|
{
|
||||||
|
case EItemTransactionStepType::AddItem:
|
||||||
|
{
|
||||||
|
// At least one container must accept it.
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
Inv->GetContainers(Containers);
|
||||||
|
bool bAccepts = false;
|
||||||
|
for (const FInventoryContainer& C : Containers)
|
||||||
|
{
|
||||||
|
if (Inv->CanAcceptItem(Step.Item, C.ContainerId)) { bAccepts = true; break; }
|
||||||
|
}
|
||||||
|
if (!bAccepts) { return false; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EItemTransactionStepType::RemoveItem:
|
||||||
|
case EItemTransactionStepType::UpdateItem:
|
||||||
|
case EItemTransactionStepType::MoveItem:
|
||||||
|
{
|
||||||
|
FItemInstanceData Found;
|
||||||
|
if (!Inv->FindItem(Step.InstanceId, Found) || Found.Lock.bLocked) { return false; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FItemTransaction::Apply()
|
||||||
|
{
|
||||||
|
if (!Validate())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FItemTransactionStep& Step : Steps)
|
||||||
|
{
|
||||||
|
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||||
|
bool bOk = false;
|
||||||
|
switch (Step.Type)
|
||||||
|
{
|
||||||
|
case EItemTransactionStepType::AddItem:
|
||||||
|
{
|
||||||
|
FItemInstanceData ToAdd = Step.Item;
|
||||||
|
bOk = Inv->AddItem(ToAdd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EItemTransactionStepType::RemoveItem:
|
||||||
|
{
|
||||||
|
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot); // capture for rollback
|
||||||
|
bOk = Inv->RemoveItem(Step.InstanceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EItemTransactionStepType::UpdateItem:
|
||||||
|
{
|
||||||
|
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot);
|
||||||
|
bOk = Inv->UpdateItem(Step.Item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EItemTransactionStepType::MoveItem:
|
||||||
|
{
|
||||||
|
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot);
|
||||||
|
Step.UndoFromContainer = Step.UndoSnapshot.CurrentContainerId;
|
||||||
|
Step.UndoFromSlot = Step.UndoSnapshot.SlotIndex;
|
||||||
|
bOk = Inv->MoveItem(Step.InstanceId, Step.UndoFromContainer, Step.ToContainer, Step.ToSlot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bOk)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemInventory, Warning, TEXT("[Transaction %s] step failed; rolling back."), *TransactionId.ToString());
|
||||||
|
Rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Step.bApplied = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FItemTransaction::Rollback()
|
||||||
|
{
|
||||||
|
// Undo applied steps in reverse order.
|
||||||
|
for (int32 i = Steps.Num() - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
FItemTransactionStep& Step = Steps[i];
|
||||||
|
if (!Step.bApplied)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (Step.Type)
|
||||||
|
{
|
||||||
|
case EItemTransactionStepType::AddItem:
|
||||||
|
Inv->RemoveItem(Step.InstanceId);
|
||||||
|
break;
|
||||||
|
case EItemTransactionStepType::RemoveItem:
|
||||||
|
if (Step.UndoSnapshot.IsValid()) { FItemInstanceData Restore = Step.UndoSnapshot; Inv->AddItem(Restore); }
|
||||||
|
break;
|
||||||
|
case EItemTransactionStepType::UpdateItem:
|
||||||
|
if (Step.UndoSnapshot.IsValid()) { Inv->UpdateItem(Step.UndoSnapshot); }
|
||||||
|
break;
|
||||||
|
case EItemTransactionStepType::MoveItem:
|
||||||
|
Inv->MoveItem(Step.InstanceId, Step.ToContainer, Step.UndoFromContainer, Step.UndoFromSlot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Step.bApplied = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
Source/ItemInventory/Private/ReplicatedInventoryList.cpp
Normal file
103
Source/ItemInventory/Private/ReplicatedInventoryList.cpp
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "ReplicatedInventoryList.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
|
||||||
|
void FReplicatedInventoryList::PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 /*FinalSize*/)
|
||||||
|
{
|
||||||
|
if (!OwnerComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int32 Index : AddedIndices)
|
||||||
|
{
|
||||||
|
if (Entries.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
OwnerComponent->NotifyItemAdded(Entries[Index].Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OwnerComponent->NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FReplicatedInventoryList::PostReplicatedChange(const TArrayView<int32>& /*ChangedIndices*/, int32 /*FinalSize*/)
|
||||||
|
{
|
||||||
|
if (OwnerComponent)
|
||||||
|
{
|
||||||
|
OwnerComponent->NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FReplicatedInventoryList::PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 /*FinalSize*/)
|
||||||
|
{
|
||||||
|
if (!OwnerComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int32 Index : RemovedIndices)
|
||||||
|
{
|
||||||
|
if (Entries.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
OwnerComponent->NotifyItemRemoved(Entries[Index].Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OwnerComponent->NotifyInventoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
FRepInventoryEntry& FReplicatedInventoryList::AddOrUpdate(const FGuid& Container, int32 Slot, const FItemInstanceData& Item)
|
||||||
|
{
|
||||||
|
if (FRepInventoryEntry* Existing = FindByInstanceId(Item.InstanceId))
|
||||||
|
{
|
||||||
|
Existing->ContainerId = Container;
|
||||||
|
Existing->SlotIndex = Slot;
|
||||||
|
Existing->Item = Item;
|
||||||
|
Existing->Item.CurrentContainerId = Container;
|
||||||
|
Existing->Item.SlotIndex = Slot;
|
||||||
|
MarkItemDirty(*Existing);
|
||||||
|
return *Existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
FRepInventoryEntry& New = Entries.Emplace_GetRef(Container, Slot, Item);
|
||||||
|
New.Item.CurrentContainerId = Container;
|
||||||
|
New.Item.SlotIndex = Slot;
|
||||||
|
MarkItemDirty(New);
|
||||||
|
return New;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FReplicatedInventoryList::RemoveByInstanceId(const FGuid& InstanceId)
|
||||||
|
{
|
||||||
|
const int32 Index = Entries.IndexOfByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||||
|
{
|
||||||
|
return E.Item.InstanceId == InstanceId;
|
||||||
|
});
|
||||||
|
if (Index == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Entries.RemoveAt(Index);
|
||||||
|
MarkArrayDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FReplicatedInventoryList::MarkEntryDirtyByInstanceId(const FGuid& InstanceId)
|
||||||
|
{
|
||||||
|
if (FRepInventoryEntry* Entry = FindByInstanceId(InstanceId))
|
||||||
|
{
|
||||||
|
MarkItemDirty(*Entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FRepInventoryEntry* FReplicatedInventoryList::FindByInstanceId(const FGuid& InstanceId)
|
||||||
|
{
|
||||||
|
return Entries.FindByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||||
|
{
|
||||||
|
return E.Item.InstanceId == InstanceId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const FRepInventoryEntry* FReplicatedInventoryList::FindByInstanceId(const FGuid& InstanceId) const
|
||||||
|
{
|
||||||
|
return Entries.FindByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||||
|
{
|
||||||
|
return E.Item.InstanceId == InstanceId;
|
||||||
|
});
|
||||||
|
}
|
||||||
195
Source/ItemInventory/Public/InventoryComponent.h
Normal file
195
Source/ItemInventory/Public/InventoryComponent.h
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "ReplicatedInventoryList.h"
|
||||||
|
#include "InventoryContainer.h"
|
||||||
|
#include "InventoryComponent.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChangedSignature);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemEventSignature, const FItemInstanceData&, Item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns and replicates a set of containers full of items (section 7).
|
||||||
|
*
|
||||||
|
* Server-authoritative: every mutating method must run on the server (the
|
||||||
|
* interaction/transaction layer is the only intended caller). Clients receive
|
||||||
|
* state through the FastArray and react via OnInventoryChanged / OnRep. The UI
|
||||||
|
* never mutates this directly (section 25).
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMINVENTORY_API UInventoryComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UInventoryComponent();
|
||||||
|
|
||||||
|
//~ UActorComponent
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
virtual void InitializeComponent() override;
|
||||||
|
virtual void OnRegister() override;
|
||||||
|
|
||||||
|
// --- Events (UI binds here, never polls - section 32) ---
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||||
|
FOnInventoryChangedSignature OnInventoryChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||||
|
FOnInventoryItemEventSignature OnItemAdded;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||||
|
FOnInventoryItemEventSignature OnItemRemoved;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true (default) inventory replicates only to the owning client (player
|
||||||
|
* inventory, section 25.9). Set false for shared world containers (loot chests,
|
||||||
|
* vehicle cargo) so nearby clients can see the contents. Must be set before the
|
||||||
|
* component registers replication (e.g. in the owning actor's constructor).
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Replication")
|
||||||
|
bool bOwnerOnlyReplication = true;
|
||||||
|
|
||||||
|
// --- Container setup ---
|
||||||
|
|
||||||
|
/** Registers a container. Assigns a new ContainerId if unset. Server side. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
FGuid AddContainer(FInventoryContainer Container);
|
||||||
|
|
||||||
|
/** Registers a container from a DT_Containers template (section 6). Server. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
FGuid AddContainerByTag(FGameplayTag ContainerTag);
|
||||||
|
|
||||||
|
/** Creates a nested container owned by an item that has Feature.Container (section 7). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
FGuid AddNestedContainerForItem(FGuid OwnerItemInstanceId, FGameplayTag ContainerTag);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
bool GetContainer(FGuid ContainerId, FInventoryContainer& OutContainer) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
void GetContainers(TArray<FInventoryContainer>& OutContainers) const { OutContainers = Containers; }
|
||||||
|
|
||||||
|
/** First container whose tag matches, or an invalid guid. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
FGuid FindContainerByTag(FGameplayTag ContainerTag) const;
|
||||||
|
|
||||||
|
// --- Core operations (section 7.1) ---
|
||||||
|
|
||||||
|
/** Adds an item, stacking into existing compatible stacks then free slots. Server. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool AddItem(UPARAM(ref) FItemInstanceData& Item);
|
||||||
|
|
||||||
|
/** Adds an item into a specific container. Server. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool AddItemToContainer(UPARAM(ref) FItemInstanceData& Item, FGuid ContainerId);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool RemoveItem(FGuid InstanceId);
|
||||||
|
|
||||||
|
/** Removes up to Amount from a stack; removes the entry when it hits zero. Server. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool RemoveQuantity(FGuid InstanceId, int32 Amount);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool MoveItem(FGuid InstanceId, FGuid FromContainerId, FGuid ToContainerId, int32 TargetSlot);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool SplitStack(FGuid InstanceId, int32 Amount);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool MergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId);
|
||||||
|
|
||||||
|
// --- Queries ---
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
bool CanAcceptItem(const FItemInstanceData& Item, FGuid ContainerId) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
bool FindItem(FGuid InstanceId, FItemInstanceData& OutItem) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
void GetContainerItems(FGuid ContainerId, TArray<FItemInstanceData>& OutItems) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
void GetAllItems(TArray<FItemInstanceData>& OutItems) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
float GetContainerWeight(FGuid ContainerId) const;
|
||||||
|
|
||||||
|
/** Total quantity of a given ItemId across all containers. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
int32 GetItemCount(FName ItemId) const;
|
||||||
|
|
||||||
|
// --- Internal mutation used by the transaction layer ---
|
||||||
|
|
||||||
|
/** Writes a mutated copy back into the list and replicates it. Server. */
|
||||||
|
bool UpdateItem(const FItemInstanceData& Item);
|
||||||
|
|
||||||
|
/** Server: place a soft lock on an item so it cannot be moved/taken (section 20). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool SetItemLock(FGuid InstanceId, FGameplayTag Reason, AActor* LockedBy, float ExpireWorldTime);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
bool ClearItemLock(FGuid InstanceId);
|
||||||
|
|
||||||
|
/** Reads a ServerOnly property kept off the wire (section 22). Server only. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||||
|
float GetServerOnlyFloat(FGuid InstanceId, FGameplayTag PropertyTag, float Fallback = 0.f) const;
|
||||||
|
|
||||||
|
/** Direct, unchecked write of an item into a slot (used by transactions). Server. */
|
||||||
|
void SetItemInSlot(const FItemInstanceData& Item, FGuid ContainerId, int32 SlotIndex);
|
||||||
|
|
||||||
|
/** Server: remove every item (save load / debug clear). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||||
|
void ClearAllItems();
|
||||||
|
|
||||||
|
/** Server: replace the container set (save load). */
|
||||||
|
void SetContainers(const TArray<FInventoryContainer>& InContainers);
|
||||||
|
|
||||||
|
/** Broadcasts OnInventoryChanged. Called by the FastArray on clients too. */
|
||||||
|
void NotifyInventoryChanged();
|
||||||
|
void NotifyItemAdded(const FItemInstanceData& Item);
|
||||||
|
void NotifyItemRemoved(const FItemInstanceData& Item);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(Replicated)
|
||||||
|
FReplicatedInventoryList ReplicatedItems;
|
||||||
|
|
||||||
|
UPROPERTY(Replicated)
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
|
||||||
|
/** Default containers spawned in InitializeComponent (designer-configurable). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Setup")
|
||||||
|
TArray<FInventoryContainer> DefaultContainers;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool HasAuthorityChecked(const TCHAR* Op) const;
|
||||||
|
|
||||||
|
FInventoryContainer* GetContainerMutable(const FGuid& ContainerId);
|
||||||
|
const FInventoryContainer* GetContainerConst(const FGuid& ContainerId) const;
|
||||||
|
|
||||||
|
/** Lowest free slot index in a slot-limited container, or INDEX_NONE if full/unlimited-ok. */
|
||||||
|
int32 FindFreeSlot(const FGuid& ContainerId) const;
|
||||||
|
|
||||||
|
/** Grid-aware free slot for a specific item (uses its GridSize); falls back to FindFreeSlot. */
|
||||||
|
int32 FindFreeSlotForItem(const FGuid& ContainerId, const FItemInstanceData& Item) const;
|
||||||
|
|
||||||
|
FIntPoint GetItemGridSize(FName ItemId) const;
|
||||||
|
|
||||||
|
/** True if the WxH rectangle at TopLeft is inside the grid and unoccupied (ignoring IgnoreInstance). */
|
||||||
|
bool IsGridRegionFree(const FInventoryContainer& Container, int32 TopLeft, FIntPoint Size, const FGuid& IgnoreInstance) const;
|
||||||
|
|
||||||
|
bool IsSlotOccupied(const FGuid& ContainerId, int32 SlotIndex) const;
|
||||||
|
|
||||||
|
/** Tries to top up existing stacks of the same item; reduces Remaining accordingly. */
|
||||||
|
void StackIntoExisting(const FGuid& ContainerId, FItemInstanceData& Item, int32& Remaining);
|
||||||
|
|
||||||
|
/** Moves an item's ServerOnly properties into the server-side side channel (section 22). */
|
||||||
|
void StripServerOnlyProperties(FItemInstanceData& Item);
|
||||||
|
|
||||||
|
/** Server-only properties never replicated to clients, keyed by instance id. */
|
||||||
|
TMap<FGuid, FItemRuntimeProperties> ServerOnlyProps;
|
||||||
|
};
|
||||||
74
Source/ItemInventory/Public/InventoryContainer.h
Normal file
74
Source/ItemInventory/Public/InventoryContainer.h
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "InventoryContainer.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata describing one container owned by an inventory (section 7.2).
|
||||||
|
*
|
||||||
|
* NOTE (design deviation from the TЗ sketch): the actual item entries do NOT live
|
||||||
|
* inside the container struct. They live in the component's single
|
||||||
|
* FReplicatedInventoryList, tagged with this ContainerId. A FastArraySerializer
|
||||||
|
* only delta-replicates correctly as a direct UPROPERTY, so nesting one inside a
|
||||||
|
* replicated TArray<FInventoryContainer> would silently fall back to full-array
|
||||||
|
* replication. Keeping containers as lightweight metadata fixes that (section 32).
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINVENTORY_API FInventoryContainer
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGuid ContainerId;
|
||||||
|
|
||||||
|
/** Logical identity, e.g. Container.Backpack / Container.Hotbar / Container.Vehicle.Cargo. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTag ContainerTag;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
EInventoryContainerType Type = EInventoryContainerType::SlotList;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 Width = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 Height = 0;
|
||||||
|
|
||||||
|
/** Used by SlotList/Equipment/Hotbar; for Grid types capacity is Width*Height. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
int32 MaxSlots = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
float MaxWeight = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTagContainer AcceptedItemTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||||
|
FGameplayTagContainer BlockedItemTags;
|
||||||
|
|
||||||
|
/** Set when this container lives inside another item (a backpack/pouch item) - section 7. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Container")
|
||||||
|
FGuid OwnerItemInstanceId;
|
||||||
|
|
||||||
|
FInventoryContainer() = default;
|
||||||
|
|
||||||
|
bool IsNested() const { return OwnerItemInstanceId.IsValid(); }
|
||||||
|
|
||||||
|
/** Number of addressable slots in this container (0 = unlimited weight-gated list). */
|
||||||
|
int32 GetSlotCount() const
|
||||||
|
{
|
||||||
|
if (Type == EInventoryContainerType::Grid)
|
||||||
|
{
|
||||||
|
return FMath::Max(0, Width) * FMath::Max(0, Height);
|
||||||
|
}
|
||||||
|
return FMath::Max(0, MaxSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasSlotLimit() const { return GetSlotCount() > 0; }
|
||||||
|
bool HasWeightLimit() const { return MaxWeight > 0.f; }
|
||||||
|
};
|
||||||
38
Source/ItemInventory/Public/ItemCraftingLibrary.h
Normal file
38
Source/ItemInventory/Public/ItemCraftingLibrary.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemCraftingLibrary.generated.h"
|
||||||
|
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crafting + loot helpers (section 6). All server-authoritative: crafting consumes
|
||||||
|
* inputs and produces the output through the inventory's validated operations;
|
||||||
|
* loot rolls fresh instances from a loot table into a container.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMINVENTORY_API UItemCraftingLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** True if Inventory holds every ingredient (and the station requirement is met). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Crafting", meta = (WorldContext = "WorldContext"))
|
||||||
|
static bool CanCraft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation);
|
||||||
|
|
||||||
|
/** Server: consume the recipe inputs and add its output. Returns true on success. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Crafting", meta = (WorldContext = "WorldContext"))
|
||||||
|
static bool Craft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation);
|
||||||
|
|
||||||
|
/** Server: roll a loot table and add the results into an inventory. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Loot", meta = (WorldContext = "WorldContext"))
|
||||||
|
static void RollLootIntoInventory(const UObject* WorldContext, UInventoryComponent* Inventory, FName TableId, int32 RollCount);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Removes Count of ItemId across stacks. Returns true if the full amount was removed. */
|
||||||
|
static bool ConsumeItem(UInventoryComponent* Inventory, FName ItemId, int32 Count);
|
||||||
|
};
|
||||||
56
Source/ItemInventory/Public/ItemFeatureLibrary.h
Normal file
56
Source/ItemInventory/Public/ItemFeatureLibrary.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemFeatureLibrary.generated.h"
|
||||||
|
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature processing (section 21). Everything is LAZY: nothing ticks per item.
|
||||||
|
* Call these on events - container open, save load, interaction, world-time pulse.
|
||||||
|
* State (freshness, last-update time, charge, fuel) lives in the item's runtime
|
||||||
|
* properties so it travels with the instance.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMINVENTORY_API UItemFeatureLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Recomputes time-based features (rotting) for one item up to "now".
|
||||||
|
* Returns true if the item changed. If the item should transform (e.g. raw_meat
|
||||||
|
* -> rotten_meat) OutTransformItemId is set to the replacement id.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||||
|
static bool UpdateLazyFeatures(const UObject* WorldContext, UPARAM(ref) FItemInstanceData& Item, FName& OutTransformItemId);
|
||||||
|
|
||||||
|
/** Server: run lazy updates across a whole inventory, applying any transforms. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||||
|
static void UpdateInventoryFeatures(const UObject* WorldContext, UInventoryComponent* Inventory);
|
||||||
|
|
||||||
|
// --- Direct feature operations (debug tools, gameplay) ---
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||||
|
static void ForceRot(const UObject* WorldContext, UPARAM(ref) FItemInstanceData& Item, FName& OutTransformItemId);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||||
|
static void BreakItem(UPARAM(ref) FItemInstanceData& Item);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||||
|
static void DrainBattery(UPARAM(ref) FItemInstanceData& Item, float DrainPerSecond, float Seconds);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||||
|
static float AddFuel(UPARAM(ref) FItemInstanceData& Item, float Amount, float MaxFuel);
|
||||||
|
|
||||||
|
/** Moves up to Amount of fuel from one item to another, respecting ToMaxFuel. Returns moved. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||||
|
static float TransferFuel(UPARAM(ref) FItemInstanceData& From, UPARAM(ref) FItemInstanceData& To, float Amount, float ToMaxFuel);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||||
|
static void SetWetness(UPARAM(ref) FItemInstanceData& Item, float Wetness);
|
||||||
|
};
|
||||||
8
Source/ItemInventory/Public/ItemInventoryLog.h
Normal file
8
Source/ItemInventory/Public/ItemInventoryLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMINVENTORY_API DECLARE_LOG_CATEGORY_EXTERN(LogItemInventory, Log, All);
|
||||||
34
Source/ItemInventory/Public/ItemSaveGame.h
Normal file
34
Source/ItemInventory/Public/ItemSaveGame.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/SaveGame.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "InventoryContainer.h"
|
||||||
|
#include "ItemSaveGame.generated.h"
|
||||||
|
|
||||||
|
/** Saved snapshot of one inventory (section 26). Only ids/state are kept. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINVENTORY_API FInventorySaveData
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
TArray<FItemInstanceData> Items;
|
||||||
|
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
TArray<FInventoryContainer> Containers;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** SaveGame object holding any number of named inventory snapshots. */
|
||||||
|
UCLASS()
|
||||||
|
class ITEMINVENTORY_API UItemSaveGame : public USaveGame
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Keyed by a stable inventory id chosen by the game (e.g. "Player0", a chest guid). */
|
||||||
|
UPROPERTY(SaveGame)
|
||||||
|
TMap<FName, FInventorySaveData> Inventories;
|
||||||
|
};
|
||||||
38
Source/ItemInventory/Public/ItemSaveLibrary.h
Normal file
38
Source/ItemInventory/Public/ItemSaveLibrary.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "ItemSaveGame.h"
|
||||||
|
#include "ItemSaveLibrary.generated.h"
|
||||||
|
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save / load of inventories with redirect + version migration (section 26).
|
||||||
|
* Stores only ItemId/InstanceId/Quantity/RuntimeProperties/StateTags/container/slot
|
||||||
|
* - never icons, meshes, names or actor pointers (section 26 "do not save").
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMINVENTORY_API UItemSaveLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Snapshots an inventory's items + containers (drops transient locks). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Save")
|
||||||
|
static FInventorySaveData CaptureInventory(UInventoryComponent* Inventory);
|
||||||
|
|
||||||
|
/** Server: rebuilds an inventory from a snapshot, migrating each item. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Save", meta = (WorldContext = "WorldContext"))
|
||||||
|
static void RestoreInventory(const UObject* WorldContext, UInventoryComponent* Inventory, const FInventorySaveData& Data);
|
||||||
|
|
||||||
|
/** Writes one inventory under Key into a save slot (loads/creates the slot file). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Save")
|
||||||
|
static bool SaveInventoryToSlot(UInventoryComponent* Inventory, const FString& SlotName, FName Key);
|
||||||
|
|
||||||
|
/** Server: loads one inventory under Key from a save slot. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|Save", meta = (WorldContext = "WorldContext"))
|
||||||
|
static bool LoadInventoryFromSlot(const UObject* WorldContext, UInventoryComponent* Inventory, const FString& SlotName, FName Key);
|
||||||
|
};
|
||||||
101
Source/ItemInventory/Public/ItemTransaction.h
Normal file
101
Source/ItemInventory/Public/ItemTransaction.h
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemTransaction.generated.h"
|
||||||
|
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/** Kind of mutation a transaction step performs (section 10). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EItemTransactionStepType : uint8
|
||||||
|
{
|
||||||
|
AddItem,
|
||||||
|
RemoveItem,
|
||||||
|
UpdateItem,
|
||||||
|
MoveItem
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One atomic step. Carries enough to apply AND to roll back. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINVENTORY_API FItemTransactionStep
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
EItemTransactionStepType Type = EItemTransactionStepType::AddItem;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<UInventoryComponent> Inventory;
|
||||||
|
|
||||||
|
/** Item payload for AddItem / UpdateItem. */
|
||||||
|
UPROPERTY()
|
||||||
|
FItemInstanceData Item;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGuid InstanceId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGuid ToContainer;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
int32 ToSlot = INDEX_NONE;
|
||||||
|
|
||||||
|
// --- runtime (rollback bookkeeping) ---
|
||||||
|
UPROPERTY()
|
||||||
|
bool bApplied = false;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FItemInstanceData UndoSnapshot;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGuid UndoFromContainer;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
int32 UndoFromSlot = INDEX_NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomic multi-step item operation (section 10). Build steps, then Apply():
|
||||||
|
* if any step fails, every applied step is rolled back so the inventories are
|
||||||
|
* left untouched. The single correct way to do multi-inventory moves.
|
||||||
|
*
|
||||||
|
* Server-only. Operates through UInventoryComponent's authoritative methods.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMINVENTORY_API FItemTransaction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGuid TransactionId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGameplayTag TransactionType;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<AActor> Instigator;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<FItemTransactionStep> Steps;
|
||||||
|
|
||||||
|
FItemTransaction() { TransactionId = FGuid::NewGuid(); }
|
||||||
|
|
||||||
|
// --- Builders ---
|
||||||
|
FItemTransaction& Add(UInventoryComponent* Inventory, const FItemInstanceData& Item);
|
||||||
|
FItemTransaction& Remove(UInventoryComponent* Inventory, const FGuid& InstanceId);
|
||||||
|
FItemTransaction& Update(UInventoryComponent* Inventory, const FItemInstanceData& Item);
|
||||||
|
FItemTransaction& Move(UInventoryComponent* Inventory, const FGuid& InstanceId, const FGuid& ToContainerId, int32 ToSlotIndex);
|
||||||
|
|
||||||
|
/** Non-mutating feasibility check of every step. */
|
||||||
|
bool Validate() const;
|
||||||
|
|
||||||
|
/** Applies all steps; on any failure rolls back and returns false. */
|
||||||
|
bool Apply();
|
||||||
|
|
||||||
|
/** Undoes every applied step in reverse order. */
|
||||||
|
void Rollback();
|
||||||
|
};
|
||||||
84
Source/ItemInventory/Public/ReplicatedInventoryList.h
Normal file
84
Source/ItemInventory/Public/ReplicatedInventoryList.h
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Net/Serialization/FastArraySerializer.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ReplicatedInventoryList.generated.h"
|
||||||
|
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One replicated item slot (section 7.3). The item carries its own ContainerId /
|
||||||
|
* SlotIndex inside FItemInstanceData, but they are mirrored here so the FastArray
|
||||||
|
* delta only needs the entry, and so clients can route OnRep callbacks per slot.
|
||||||
|
*/
|
||||||
|
USTRUCT()
|
||||||
|
struct ITEMINVENTORY_API FRepInventoryEntry : public FFastArraySerializerItem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FGuid ContainerId;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
int32 SlotIndex = INDEX_NONE;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FItemInstanceData Item;
|
||||||
|
|
||||||
|
FRepInventoryEntry() = default;
|
||||||
|
FRepInventoryEntry(const FGuid& InContainer, int32 InSlot, const FItemInstanceData& InItem)
|
||||||
|
: ContainerId(InContainer), SlotIndex(InSlot), Item(InItem) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FastArraySerializer carrying every item the inventory owns across all containers
|
||||||
|
* (section 7.3 / 32). One list per component delta-replicates only changed entries.
|
||||||
|
*/
|
||||||
|
USTRUCT()
|
||||||
|
struct ITEMINVENTORY_API FReplicatedInventoryList : public FFastArraySerializer
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<FRepInventoryEntry> Entries;
|
||||||
|
|
||||||
|
/** Not replicated - set by the owning component so callbacks can broadcast. */
|
||||||
|
UPROPERTY(NotReplicated)
|
||||||
|
TObjectPtr<UInventoryComponent> OwnerComponent = nullptr;
|
||||||
|
|
||||||
|
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
|
||||||
|
{
|
||||||
|
return FFastArraySerializer::FastArrayDeltaSerialize<FRepInventoryEntry, FReplicatedInventoryList>(Entries, DeltaParms, *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
//~ FFastArraySerializer client callbacks
|
||||||
|
void PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 FinalSize);
|
||||||
|
void PostReplicatedChange(const TArrayView<int32>& ChangedIndices, int32 FinalSize);
|
||||||
|
void PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 FinalSize);
|
||||||
|
|
||||||
|
// --- Server-side mutation helpers (mark dirty for delta replication) ---
|
||||||
|
|
||||||
|
/** Adds or replaces the entry occupying (Container, Slot). Returns the live entry. */
|
||||||
|
FRepInventoryEntry& AddOrUpdate(const FGuid& Container, int32 Slot, const FItemInstanceData& Item);
|
||||||
|
|
||||||
|
/** Removes the entry with the given instance id. Returns true if one was removed. */
|
||||||
|
bool RemoveByInstanceId(const FGuid& InstanceId);
|
||||||
|
|
||||||
|
/** Marks an existing entry dirty after its Item was mutated in place. */
|
||||||
|
void MarkEntryDirtyByInstanceId(const FGuid& InstanceId);
|
||||||
|
|
||||||
|
FRepInventoryEntry* FindByInstanceId(const FGuid& InstanceId);
|
||||||
|
const FRepInventoryEntry* FindByInstanceId(const FGuid& InstanceId) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct TStructOpsTypeTraits<FReplicatedInventoryList> : public TStructOpsTypeTraitsBase2<FReplicatedInventoryList>
|
||||||
|
{
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
WithNetDeltaSerializer = true
|
||||||
|
};
|
||||||
|
};
|
||||||
26
Source/ItemPlacement/ItemPlacement.Build.cs
Normal file
26
Source/ItemPlacement/ItemPlacement.Build.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemPlacement : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemPlacement(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"NetCore",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase",
|
||||||
|
"ItemInventory",
|
||||||
|
"ItemInteraction",
|
||||||
|
"ItemWorld"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Source/ItemPlacement/Private/ItemPlacementModule.cpp
Normal file
8
Source/ItemPlacement/Private/ItemPlacementModule.cpp
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemPlacementLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemPlacement);
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemPlacement)
|
||||||
53
Source/ItemPlacement/Private/PlacementInteractionActions.cpp
Normal file
53
Source/ItemPlacement/Private/PlacementInteractionActions.cpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "PlacementInteractionActions.h"
|
||||||
|
#include "PlacementPreviewComponent.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
UPlacementPreviewComponent* GetPlacement(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
return Context.Character ? Context.Character->FindComponentByClass<UPlacementPreviewComponent>() : nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_StartPlacement::UInteractionAction_StartPlacement()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Place;
|
||||||
|
Mode = EItemInteractionMode::StartPlacement;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemPlacement", "Place", "Place");
|
||||||
|
}
|
||||||
|
void UInteractionAction_StartPlacement::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (UPlacementPreviewComponent* P = GetPlacement(Context))
|
||||||
|
{
|
||||||
|
const FGuid Id = Context.bHasTargetItem ? Context.TargetItem.InstanceId
|
||||||
|
: (Context.bHasHeldItem ? Context.HeldItem.InstanceId : FGuid());
|
||||||
|
if (Id.IsValid()) { P->StartPlacement(Id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_ConfirmPlacement::UInteractionAction_ConfirmPlacement()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::ConfirmPlacement;
|
||||||
|
Priority = 1000;
|
||||||
|
DisplayText = NSLOCTEXT("ItemPlacement", "Confirm", "Confirm");
|
||||||
|
}
|
||||||
|
void UInteractionAction_ConfirmPlacement::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (UPlacementPreviewComponent* P = GetPlacement(Context)) { P->ConfirmPlacement(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_CancelPlacement::UInteractionAction_CancelPlacement()
|
||||||
|
{
|
||||||
|
Mode = EItemInteractionMode::CancelPlacement;
|
||||||
|
Priority = 1000;
|
||||||
|
DisplayText = NSLOCTEXT("ItemPlacement", "Cancel", "Cancel");
|
||||||
|
}
|
||||||
|
void UInteractionAction_CancelPlacement::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (UPlacementPreviewComponent* P = GetPlacement(Context)) { P->CancelPlacement(); }
|
||||||
|
}
|
||||||
282
Source/ItemPlacement/Private/PlacementPreviewComponent.cpp
Normal file
282
Source/ItemPlacement/Private/PlacementPreviewComponent.cpp
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "PlacementPreviewComponent.h"
|
||||||
|
#include "ItemPlacementLog.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
|
||||||
|
UPlacementPreviewComponent::UPlacementPreviewComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
||||||
|
SetIsReplicatedByDefault(true); // for the server confirm RPC
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPlacementPreviewComponent::GetViewPoint(FVector& OutLoc, FRotator& OutRot) const
|
||||||
|
{
|
||||||
|
const APawn* Pawn = Cast<APawn>(GetOwner());
|
||||||
|
const APlayerController* PC = Pawn ? Cast<APlayerController>(Pawn->GetController()) : nullptr;
|
||||||
|
if (!PC || !PC->IsLocalController())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PC->GetPlayerViewPoint(OutLoc, OutRot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::StartPlacement(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
APawn* Pawn = Cast<APawn>(GetOwner());
|
||||||
|
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Inv->FindItem(InstanceId, Item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (!Def || !Def->bCanPlace)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemInstanceId = InstanceId;
|
||||||
|
ItemId = Item.ItemId;
|
||||||
|
bIsPlacing = true;
|
||||||
|
PreviewYaw = 0.f;
|
||||||
|
|
||||||
|
// Build the local ghost mesh.
|
||||||
|
if (!Ghost)
|
||||||
|
{
|
||||||
|
Ghost = NewObject<UStaticMeshComponent>(GetOwner(), TEXT("PlacementGhost"));
|
||||||
|
Ghost->SetupAttachment(GetOwner()->GetRootComponent());
|
||||||
|
Ghost->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Ghost->SetCastShadow(false);
|
||||||
|
Ghost->RegisterComponent();
|
||||||
|
}
|
||||||
|
if (!Def->WorldMesh.IsNull())
|
||||||
|
{
|
||||||
|
if (UStaticMesh* M = Def->WorldMesh.LoadSynchronous())
|
||||||
|
{
|
||||||
|
Ghost->SetStaticMesh(M);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ghost->SetVisibility(true);
|
||||||
|
SetComponentTickEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::CancelPlacement()
|
||||||
|
{
|
||||||
|
bIsPlacing = false;
|
||||||
|
bCurrentValid = false;
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
DestroyGhost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::RotatePreview(float DeltaYawDegrees)
|
||||||
|
{
|
||||||
|
PreviewYaw = FMath::Fmod(PreviewYaw + DeltaYawDegrees, 360.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::ConfirmPlacement()
|
||||||
|
{
|
||||||
|
if (!bIsPlacing || !bCurrentValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Server_ConfirmPlacement(ItemInstanceId, PreviewTransform);
|
||||||
|
CancelPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
if (!bIsPlacing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTransform Xf;
|
||||||
|
if (ComputePreviewTransform(Xf))
|
||||||
|
{
|
||||||
|
PreviewTransform = Xf;
|
||||||
|
if (Ghost)
|
||||||
|
{
|
||||||
|
Ghost->SetWorldTransform(Xf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemId) : nullptr;
|
||||||
|
FString Reason;
|
||||||
|
const bool bValid = Def && ValidatePlacement(this, *Def, Xf, GetOwner(), Reason);
|
||||||
|
SetValidity(bValid);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetValidity(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPlacementPreviewComponent::ComputePreviewTransform(FTransform& OutTransform) const
|
||||||
|
{
|
||||||
|
FVector ViewLoc;
|
||||||
|
FRotator ViewRot;
|
||||||
|
if (!GetViewPoint(ViewLoc, ViewRot))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemId) : nullptr;
|
||||||
|
const float MaxDist = Def ? Def->PlacementRules.MaxDistance : 400.f;
|
||||||
|
|
||||||
|
const FVector Start = ViewLoc;
|
||||||
|
const FVector End = ViewLoc + ViewRot.Vector() * MaxDist;
|
||||||
|
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementTrace), false, GetOwner());
|
||||||
|
FHitResult Hit;
|
||||||
|
if (!GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FRotator Rot(0.f, PreviewYaw, 0.f);
|
||||||
|
// Optionally align to surface normal when not requiring a flat surface.
|
||||||
|
if (Def && !Def->PlacementRules.bRequiresFlatSurface)
|
||||||
|
{
|
||||||
|
const FRotator Aligned = FRotationMatrix::MakeFromZX(Hit.ImpactNormal, ViewRot.Vector()).Rotator();
|
||||||
|
Rot = FRotator(Aligned.Pitch, PreviewYaw, Aligned.Roll);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutTransform = FTransform(Rot, Hit.ImpactPoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPlacementPreviewComponent::ValidatePlacement(const UObject* WorldContext, const FItemDefinitionRow& Def,
|
||||||
|
const FTransform& Transform, AActor* Instigator, FString& OutReason)
|
||||||
|
{
|
||||||
|
const FItemPlacementRules& Rules = Def.PlacementRules;
|
||||||
|
UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
OutReason = TEXT("No world");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from the instigator.
|
||||||
|
if (Instigator)
|
||||||
|
{
|
||||||
|
const float DistSq = FVector::DistSquared(Instigator->GetActorLocation(), Transform.GetLocation());
|
||||||
|
if (DistSq > FMath::Square(Rules.MaxDistance + 100.f))
|
||||||
|
{
|
||||||
|
OutReason = TEXT("Too far");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slope: trace down and measure the surface normal.
|
||||||
|
{
|
||||||
|
const FVector Loc = Transform.GetLocation();
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementSlope), false, Instigator);
|
||||||
|
FHitResult Hit;
|
||||||
|
if (World->LineTraceSingleByChannel(Hit, Loc + FVector(0, 0, 50.f), Loc - FVector(0, 0, 100.f), ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
const float SlopeDeg = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(Hit.ImpactNormal, FVector::UpVector)));
|
||||||
|
if (Rules.bCanPlaceOnGround && SlopeDeg > Rules.MaxSlopeAngle)
|
||||||
|
{
|
||||||
|
OutReason = TEXT("Surface too steep");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Rules.bRequiresFlatSurface)
|
||||||
|
{
|
||||||
|
OutReason = TEXT("No surface");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collision: overlap a box at the target; any blocking world geometry rejects.
|
||||||
|
{
|
||||||
|
const FVector Extent(40.f, 40.f, 40.f);
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementOverlap), false, Instigator);
|
||||||
|
FCollisionShape Box = FCollisionShape::MakeBox(Extent);
|
||||||
|
if (World->OverlapBlockingTestByChannel(Transform.GetLocation() + FVector(0, 0, Extent.Z), FQuat::Identity, ECC_WorldStatic, Box, Params))
|
||||||
|
{
|
||||||
|
OutReason = TEXT("Blocked");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutReason.Reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::Server_ConfirmPlacement_Implementation(FGuid InstanceId, FTransform Transform)
|
||||||
|
{
|
||||||
|
APawn* Pawn = Cast<APawn>(GetOwner());
|
||||||
|
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Inv->FindItem(InstanceId, Item) || Item.Lock.bLocked)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (!Def || !Def->bCanPlace)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Reason;
|
||||||
|
if (!ValidatePlacement(this, *Def, Transform, GetOwner(), Reason))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemPlacement, Warning, TEXT("[ServerReject] Placement failed: %s"), *Reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (APickupItemActor::SpawnPickupFromInstance(this, Item, Transform))
|
||||||
|
{
|
||||||
|
Inv->RemoveItem(InstanceId);
|
||||||
|
UE_LOG(LogItemPlacement, Log, TEXT("[Placement] placed %s"), *Item.ItemId.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::SetValidity(bool bNewValid)
|
||||||
|
{
|
||||||
|
if (bCurrentValid != bNewValid)
|
||||||
|
{
|
||||||
|
bCurrentValid = bNewValid;
|
||||||
|
OnPlacementStateChanged.Broadcast(bNewValid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::DestroyGhost()
|
||||||
|
{
|
||||||
|
if (Ghost)
|
||||||
|
{
|
||||||
|
Ghost->DestroyComponent();
|
||||||
|
Ghost = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPlacementPreviewComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
|
{
|
||||||
|
DestroyGhost();
|
||||||
|
Super::EndPlay(EndPlayReason);
|
||||||
|
}
|
||||||
8
Source/ItemPlacement/Public/ItemPlacementLog.h
Normal file
8
Source/ItemPlacement/Public/ItemPlacementLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMPLACEMENT_API DECLARE_LOG_CATEGORY_EXTERN(LogItemPlacement, Log, All);
|
||||||
36
Source/ItemPlacement/Public/PlacementInteractionActions.h
Normal file
36
Source/ItemPlacement/Public/PlacementInteractionActions.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "PlacementInteractionActions.generated.h"
|
||||||
|
|
||||||
|
/** Placement actions (section 16) - drive the player's UPlacementPreviewComponent. */
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Start Placement"))
|
||||||
|
class ITEMPLACEMENT_API UInteractionAction_StartPlacement : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_StartPlacement();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Confirm Placement"))
|
||||||
|
class ITEMPLACEMENT_API UInteractionAction_ConfirmPlacement : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_ConfirmPlacement();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Cancel Placement"))
|
||||||
|
class ITEMPLACEMENT_API UInteractionAction_CancelPlacement : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_CancelPlacement();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
82
Source/ItemPlacement/Public/PlacementPreviewComponent.h
Normal file
82
Source/ItemPlacement/Public/PlacementPreviewComponent.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "ItemPlacementRules.h"
|
||||||
|
#include "PlacementPreviewComponent.generated.h"
|
||||||
|
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
struct FItemDefinitionRow;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlacementStateChanged, bool, bIsValid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drives the local ghost-mesh preview and the server-validated confirm for
|
||||||
|
* placeable items (section 13). The client shows a green/red ghost; the server
|
||||||
|
* re-runs every rule before committing (collision, slope, distance, surface).
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Placement), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMPLACEMENT_API UPlacementPreviewComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPlacementPreviewComponent();
|
||||||
|
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bIsPlacing = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Placement")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Placement")
|
||||||
|
FGuid ItemInstanceId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Placement")
|
||||||
|
FTransform PreviewTransform = FTransform::Identity;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Placement")
|
||||||
|
bool bCurrentValid = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Placement")
|
||||||
|
FOnPlacementStateChanged OnPlacementStateChanged;
|
||||||
|
|
||||||
|
/** Client: begin placing an inventory item. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Placement")
|
||||||
|
void StartPlacement(FGuid InstanceId);
|
||||||
|
|
||||||
|
/** Client: confirm at the current preview transform (sends a server request). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Placement")
|
||||||
|
void ConfirmPlacement();
|
||||||
|
|
||||||
|
/** Client: abort placement. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Placement")
|
||||||
|
void CancelPlacement();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Placement")
|
||||||
|
void RotatePreview(float DeltaYawDegrees);
|
||||||
|
|
||||||
|
/** Pure rule check used by both the client preview and the server (section 13.3). */
|
||||||
|
static bool ValidatePlacement(const UObject* WorldContext, const FItemDefinitionRow& Def,
|
||||||
|
const FTransform& Transform, AActor* Instigator, FString& OutReason);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_ConfirmPlacement(FGuid InstanceId, FTransform Transform);
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<UStaticMeshComponent> Ghost = nullptr;
|
||||||
|
|
||||||
|
float PreviewYaw = 0.f;
|
||||||
|
|
||||||
|
bool ComputePreviewTransform(FTransform& OutTransform) const;
|
||||||
|
void DestroyGhost();
|
||||||
|
void SetValidity(bool bNewValid);
|
||||||
|
bool GetViewPoint(FVector& OutLoc, FRotator& OutRot) const;
|
||||||
|
};
|
||||||
29
Source/ItemWorld/ItemWorld.Build.cs
Normal file
29
Source/ItemWorld/ItemWorld.Build.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class ItemWorld : ModuleRules
|
||||||
|
{
|
||||||
|
public ItemWorld(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
IWYUSupport = IWYUSupport.Full;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags",
|
||||||
|
"NetCore",
|
||||||
|
"ItemCore",
|
||||||
|
"ItemDatabase",
|
||||||
|
"ItemInventory",
|
||||||
|
"ItemInteraction"
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
150
Source/ItemWorld/Private/EquipmentComponent.cpp
Normal file
150
Source/ItemWorld/Private/EquipmentComponent.cpp
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "EquipmentComponent.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
#include "Components/SkeletalMeshComponent.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
UEquipmentComponent::UEquipmentComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UEquipmentComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME(UEquipmentComponent, Equipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UEquipmentComponent::OnUnregister()
|
||||||
|
{
|
||||||
|
if (HasAuthority())
|
||||||
|
{
|
||||||
|
for (const FEquippedItemEntry& Entry : Equipped)
|
||||||
|
{
|
||||||
|
if (Entry.EquippedActor)
|
||||||
|
{
|
||||||
|
Entry.EquippedActor->Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Super::OnUnregister();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UEquipmentComponent::HasAuthority() const
|
||||||
|
{
|
||||||
|
return GetOwner() && GetOwner()->HasAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
USceneComponent* UEquipmentComponent::GetAttachTarget() const
|
||||||
|
{
|
||||||
|
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
|
||||||
|
{
|
||||||
|
return Char->GetMesh();
|
||||||
|
}
|
||||||
|
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FName UEquipmentComponent::ResolveSocket(const FGameplayTag& SlotTag) const
|
||||||
|
{
|
||||||
|
if (const FName* Found = SlotSockets.Find(SlotTag))
|
||||||
|
{
|
||||||
|
return *Found;
|
||||||
|
}
|
||||||
|
return DefaultEquipSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UEquipmentComponent::IsSlotEquipped(FGameplayTag SlotTag) const
|
||||||
|
{
|
||||||
|
return Equipped.ContainsByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UEquipmentComponent::ServerEquip(FGuid InstanceId, FGameplayTag SlotTag)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UInventoryComponent* Inv = GetOwner()->FindComponentByClass<UInventoryComponent>();
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Inv->FindItem(InstanceId, Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (!Def || !Def->bCanEquip)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Slot compatibility (if the item restricts equipment slots).
|
||||||
|
if (!Def->AllowedEquipmentSlots.IsEmpty() && !Def->AllowedEquipmentSlots.HasTag(SlotTag))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the slot first.
|
||||||
|
ServerUnequip(SlotTag);
|
||||||
|
|
||||||
|
FEquippedItemEntry Entry;
|
||||||
|
Entry.SlotTag = SlotTag;
|
||||||
|
Entry.InstanceId = InstanceId;
|
||||||
|
Entry.ItemId = Item.ItemId;
|
||||||
|
|
||||||
|
if (!Def->EquippedActorClass.IsNull())
|
||||||
|
{
|
||||||
|
if (UClass* ActorClass = Def->EquippedActorClass.LoadSynchronous())
|
||||||
|
{
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.Owner = GetOwner();
|
||||||
|
Params.Instigator = Cast<APawn>(GetOwner());
|
||||||
|
AActor* Spawned = GetWorld()->SpawnActor<AActor>(ActorClass, FTransform::Identity, Params);
|
||||||
|
if (Spawned)
|
||||||
|
{
|
||||||
|
Spawned->AttachToComponent(GetAttachTarget(), FAttachmentTransformRules::SnapToTargetIncludingScale, ResolveSocket(SlotTag));
|
||||||
|
Entry.EquippedActor = Spawned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Equipped.Add(Entry);
|
||||||
|
OnEquipmentChanged.Broadcast();
|
||||||
|
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] equipped %s in %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UEquipmentComponent::ServerUnequip(FGameplayTag SlotTag)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int32 Index = Equipped.IndexOfByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
|
||||||
|
if (Index == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Equipped[Index].EquippedActor)
|
||||||
|
{
|
||||||
|
Equipped[Index].EquippedActor->Destroy();
|
||||||
|
}
|
||||||
|
Equipped.RemoveAt(Index);
|
||||||
|
OnEquipmentChanged.Broadcast();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UEquipmentComponent::OnRep_Equipped()
|
||||||
|
{
|
||||||
|
OnEquipmentChanged.Broadcast();
|
||||||
|
}
|
||||||
50
Source/ItemWorld/Private/FuelTankComponent.cpp
Normal file
50
Source/ItemWorld/Private/FuelTankComponent.cpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "FuelTankComponent.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
UFuelTankComponent::UFuelTankComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UFuelTankComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME(UFuelTankComponent, CurrentFuel);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UFuelTankComponent::HasAuthority() const
|
||||||
|
{
|
||||||
|
return GetOwner() && GetOwner()->HasAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UFuelTankComponent::ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || ResultVerb != FName("TransferFuel"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Available = HeldItem.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||||
|
const float Room = FMath::Max(0.f, MaxFuel - CurrentFuel);
|
||||||
|
const float Moved = FMath::Min3(PourAmount, Available, Room);
|
||||||
|
if (Moved <= 0.f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentFuel += Moved;
|
||||||
|
HeldItem.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, Available - Moved);
|
||||||
|
OnFuelChanged.Broadcast(CurrentFuel);
|
||||||
|
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] poured %.1f fuel (now %.1f/%.1f)"), Moved, CurrentFuel, MaxFuel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UFuelTankComponent::OnRep_Fuel()
|
||||||
|
{
|
||||||
|
OnFuelChanged.Broadcast(CurrentFuel);
|
||||||
|
}
|
||||||
336
Source/ItemWorld/Private/HeldItemComponent.cpp
Normal file
336
Source/ItemWorld/Private/HeldItemComponent.cpp
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "HeldItemComponent.h"
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "ItemAuxiliaryRows.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
|
#include "Components/SkeletalMeshComponent.h"
|
||||||
|
#include "Components/PrimitiveComponent.h"
|
||||||
|
#include "PhysicsEngine/PhysicsHandleComponent.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
UHeldItemComponent::UHeldItemComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
if (HeldItem.Mode == EHeldItemMode::CarryPhysicsHandle && HasAuthority())
|
||||||
|
{
|
||||||
|
UpdatePhysicsCarryTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerHoldWorldItemPhysics(AActor* WorldItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Provider || !Provider->IsItemAvailable() || !Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UPrimitiveComponent* Prim = nullptr;
|
||||||
|
if (APickupItemActor* Pickup = Cast<APickupItemActor>(WorldItem))
|
||||||
|
{
|
||||||
|
Prim = Pickup->GetMeshComponent();
|
||||||
|
}
|
||||||
|
if (!Prim)
|
||||||
|
{
|
||||||
|
Prim = Cast<UPrimitiveComponent>(WorldItem->GetRootComponent());
|
||||||
|
}
|
||||||
|
if (!Prim)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim the item but keep physics enabled (unlike attached carry).
|
||||||
|
Provider->SetCarried(GetOwner(), true);
|
||||||
|
Prim->SetSimulatePhysics(true);
|
||||||
|
Prim->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||||
|
|
||||||
|
if (!PhysicsHandle)
|
||||||
|
{
|
||||||
|
PhysicsHandle = NewObject<UPhysicsHandleComponent>(this, TEXT("CarryPhysicsHandle"));
|
||||||
|
PhysicsHandle->RegisterComponent();
|
||||||
|
}
|
||||||
|
PhysicsHandle->GrabComponentAtLocation(Prim, NAME_None, Prim->GetComponentLocation());
|
||||||
|
|
||||||
|
HeldItem.bHasHeldItem = true;
|
||||||
|
HeldItem.InstanceId = Item.InstanceId;
|
||||||
|
HeldItem.ItemId = Item.ItemId;
|
||||||
|
HeldItem.Mode = EHeldItemMode::CarryPhysicsHandle;
|
||||||
|
HeldItem.HeldWorldActor = WorldItem;
|
||||||
|
HeldItem.MoveSpeedMultiplier = 0.9f;
|
||||||
|
HeldItem.Item = Item;
|
||||||
|
HeldItem.Item.LocationType = EItemLocationType::Held;
|
||||||
|
|
||||||
|
ApplyMovementState();
|
||||||
|
SetComponentTickEnabled(true);
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::UpdatePhysicsCarryTarget()
|
||||||
|
{
|
||||||
|
if (!PhysicsHandle || !PhysicsHandle->GetGrabbedComponent() || !GetOwner())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FRotator ViewRot = GetOwner()->GetActorRotation();
|
||||||
|
if (const APawn* Pawn = Cast<APawn>(GetOwner()))
|
||||||
|
{
|
||||||
|
ViewRot = Pawn->GetControlRotation();
|
||||||
|
}
|
||||||
|
const FVector Target = GetOwner()->GetActorLocation() + ViewRot.Vector() * PhysicsHoldDistance + FVector(0, 0, 50.f);
|
||||||
|
PhysicsHandle->SetTargetLocationAndRotation(Target, ViewRot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME(UHeldItemComponent, HeldItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::HasAuthority() const
|
||||||
|
{
|
||||||
|
return GetOwner() && GetOwner()->HasAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::CanHoldItem(const FItemInstanceData& Item) const
|
||||||
|
{
|
||||||
|
if (HeldItem.bHasHeldItem || !Item.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||||
|
{
|
||||||
|
return Def->bCanWorldCarry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerHoldWorldItem(AActor* WorldItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
|
||||||
|
if (!Provider || !Provider->IsItemAvailable())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the world actor to the character's carry socket.
|
||||||
|
USceneComponent* AttachParent = nullptr;
|
||||||
|
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
|
||||||
|
{
|
||||||
|
AttachParent = Char->GetMesh();
|
||||||
|
}
|
||||||
|
if (!AttachParent)
|
||||||
|
{
|
||||||
|
AttachParent = GetOwner()->GetRootComponent();
|
||||||
|
}
|
||||||
|
WorldItem->AttachToComponent(AttachParent, FAttachmentTransformRules::SnapToTargetIncludingScale, CarrySocket);
|
||||||
|
Provider->SetCarried(GetOwner(), true);
|
||||||
|
|
||||||
|
// Resolve carry move-speed multiplier from the feature table.
|
||||||
|
float MoveMult = 1.f;
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
TArray<FItemFeatureRow> Features;
|
||||||
|
DB->GetFeatureRows(Item.ItemId, Features);
|
||||||
|
for (const FItemFeatureRow& Row : Features)
|
||||||
|
{
|
||||||
|
if (Row.Param == FName("MoveSpeedMultiplier"))
|
||||||
|
{
|
||||||
|
MoveMult = Row.GetFloat(1.f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HeldItem.bHasHeldItem = true;
|
||||||
|
HeldItem.InstanceId = Item.InstanceId;
|
||||||
|
HeldItem.ItemId = Item.ItemId;
|
||||||
|
HeldItem.Mode = EHeldItemMode::CarryAttached;
|
||||||
|
HeldItem.HeldWorldActor = WorldItem;
|
||||||
|
HeldItem.HoldSocket = CarrySocket;
|
||||||
|
HeldItem.MoveSpeedMultiplier = MoveMult;
|
||||||
|
HeldItem.bBlocksSprint = MoveMult < 0.9f;
|
||||||
|
HeldItem.Item = Item;
|
||||||
|
HeldItem.Item.LocationType = EItemLocationType::Held;
|
||||||
|
|
||||||
|
ApplyMovementState();
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] %s carried %s"), *GetNameSafe(GetOwner()), *Item.ItemId.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::RequestDrop()
|
||||||
|
{
|
||||||
|
Server_Drop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::Server_Drop_Implementation()
|
||||||
|
{
|
||||||
|
ServerDropHeldItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerDropHeldItem()
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !HeldItem.bHasHeldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Fwd = GetOwner()->GetActorForwardVector();
|
||||||
|
const FTransform DropXf(GetOwner()->GetActorRotation(),
|
||||||
|
GetOwner()->GetActorLocation() + Fwd * 100.f + FVector(0, 0, 20.f));
|
||||||
|
|
||||||
|
if (APickupItemActor* Pickup = Cast<APickupItemActor>(HeldItem.HeldWorldActor))
|
||||||
|
{
|
||||||
|
Pickup->ReleaseToWorld(DropXf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
APickupItemActor::SpawnPickupFromInstance(this, HeldItem.Item, DropXf);
|
||||||
|
if (HeldItem.HeldWorldActor)
|
||||||
|
{
|
||||||
|
HeldItem.HeldWorldActor->Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoiseOnDrop feature: emit a hearing stimulus (section 21.6).
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
TArray<FItemFeatureRow> Noise;
|
||||||
|
DB->GetFeatureRows(HeldItem.ItemId, ItemEcosystemTags::Feature_NoiseOnDrop, Noise);
|
||||||
|
for (const FItemFeatureRow& Row : Noise)
|
||||||
|
{
|
||||||
|
if (Row.Param == FName("Radius"))
|
||||||
|
{
|
||||||
|
const float Radius = Row.GetFloat(0.f);
|
||||||
|
if (Radius > 0.f)
|
||||||
|
{
|
||||||
|
GetOwner()->MakeNoise(1.f, Cast<APawn>(GetOwner()), DropXf.GetLocation(), Radius);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
HeldItem = FHeldItemState();
|
||||||
|
ApplyMovementState();
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerConsumeHeldItem(FItemInstanceData& OutItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !HeldItem.bHasHeldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutItem = HeldItem.Item;
|
||||||
|
if (HeldItem.HeldWorldActor)
|
||||||
|
{
|
||||||
|
HeldItem.HeldWorldActor->Destroy();
|
||||||
|
}
|
||||||
|
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
HeldItem = FHeldItemState();
|
||||||
|
ApplyMovementState();
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerHandOffHeldItem(FItemInstanceData& OutItem, AActor*& OutActor)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !HeldItem.bHasHeldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutItem = HeldItem.Item;
|
||||||
|
OutActor = HeldItem.HeldWorldActor;
|
||||||
|
if (OutActor)
|
||||||
|
{
|
||||||
|
OutActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
|
||||||
|
}
|
||||||
|
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
|
||||||
|
SetComponentTickEnabled(false);
|
||||||
|
HeldItem = FHeldItemState();
|
||||||
|
ApplyMovementState();
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::ServerUpdateHeldItem(const FItemInstanceData& NewItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !HeldItem.bHasHeldItem || NewItem.InstanceId != HeldItem.InstanceId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HeldItem.Item = NewItem;
|
||||||
|
HeldItem.Item.LocationType = EItemLocationType::Held;
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHeldItemComponent::GetHeldItem(FItemInstanceData& OutItem) const
|
||||||
|
{
|
||||||
|
if (!HeldItem.bHasHeldItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutItem = HeldItem.Item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::OnRep_HeldItem(FHeldItemState /*OldState*/)
|
||||||
|
{
|
||||||
|
ApplyMovementState();
|
||||||
|
OnHeldItemChanged.Broadcast(HeldItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeldItemComponent::ApplyMovementState()
|
||||||
|
{
|
||||||
|
ACharacter* Char = Cast<ACharacter>(GetOwner());
|
||||||
|
UCharacterMovementComponent* Move = Char ? Char->GetCharacterMovement() : nullptr;
|
||||||
|
if (!Move)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the unmodified speed once, then re-derive from it each time.
|
||||||
|
if (CachedBaseWalkSpeed <= 0.f)
|
||||||
|
{
|
||||||
|
CachedBaseWalkSpeed = Move->MaxWalkSpeed;
|
||||||
|
}
|
||||||
|
const float Mult = HeldItem.bHasHeldItem ? HeldItem.MoveSpeedMultiplier : 1.f;
|
||||||
|
Move->MaxWalkSpeed = CachedBaseWalkSpeed * Mult;
|
||||||
|
}
|
||||||
8
Source/ItemWorld/Private/ItemWorldModule.cpp
Normal file
8
Source/ItemWorld/Private/ItemWorldModule.cpp
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogItemWorld);
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemWorld)
|
||||||
206
Source/ItemWorld/Private/PickupItemActor.cpp
Normal file
206
Source/ItemWorld/Private/PickupItemActor.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
APickupItemActor::APickupItemActor()
|
||||||
|
{
|
||||||
|
bReplicates = true;
|
||||||
|
SetReplicateMovement(true);
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||||
|
SetRootComponent(Root);
|
||||||
|
|
||||||
|
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||||
|
Mesh->SetupAttachment(Root);
|
||||||
|
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||||
|
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
|
||||||
|
Mesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||||
|
|
||||||
|
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME(APickupItemActor, ItemData);
|
||||||
|
DOREPLIFETIME(APickupItemActor, bIsLocked);
|
||||||
|
DOREPLIFETIME(APickupItemActor, CurrentHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
RefreshVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::InitializeFromItem(const FItemInstanceData& InItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ItemData = InItem;
|
||||||
|
ItemData.EnsureInstanceId();
|
||||||
|
ItemData.LocationType = EItemLocationType::World;
|
||||||
|
RefreshVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
APickupItemActor* APickupItemActor::SpawnPickup(UObject* WorldContext, FName ItemId, int32 Quantity, const FTransform& Transform)
|
||||||
|
{
|
||||||
|
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||||
|
if (!DB || !DB->HasItem(ItemId))
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemWorld, Warning, TEXT("SpawnPickup: unknown item '%s'."), *ItemId.ToString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const FItemInstanceData Item = DB->MakeItemInstance(ItemId, Quantity);
|
||||||
|
return SpawnPickupFromInstance(WorldContext, Item, Transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
APickupItemActor* APickupItemActor::SpawnPickupFromInstance(UObject* WorldContext, const FItemInstanceData& Item, const FTransform& Transform)
|
||||||
|
{
|
||||||
|
UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the item's authored pickup actor class; fall back to the base class.
|
||||||
|
TSubclassOf<AActor> SpawnClass = APickupItemActor::StaticClass();
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext))
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||||
|
{
|
||||||
|
if (!Def->PickupActorClass.IsNull())
|
||||||
|
{
|
||||||
|
if (UClass* Loaded = Def->PickupActorClass.LoadSynchronous())
|
||||||
|
{
|
||||||
|
if (Loaded->IsChildOf(APickupItemActor::StaticClass()))
|
||||||
|
{
|
||||||
|
SpawnClass = Loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||||
|
APickupItemActor* Pickup = World->SpawnActor<APickupItemActor>(SpawnClass, Transform, Params);
|
||||||
|
if (Pickup)
|
||||||
|
{
|
||||||
|
Pickup->InitializeFromItem(Item);
|
||||||
|
}
|
||||||
|
return Pickup;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APickupItemActor::GetProvidedItem(FItemInstanceData& OutItem) const
|
||||||
|
{
|
||||||
|
if (!ItemData.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutItem = ItemData;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APickupItemActor::IsItemAvailable() const
|
||||||
|
{
|
||||||
|
return !bIsLocked && ItemData.IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::NotifyItemTaken(AActor* Taker)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UE_LOG(LogItemWorld, Verbose, TEXT("Pickup %s taken by %s; destroying."), *GetName(), *GetNameSafe(Taker));
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::UpdateProvidedItem(const FItemInstanceData& RemainingItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (RemainingItem.Quantity <= 0)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ItemData = RemainingItem;
|
||||||
|
ItemData.LocationType = EItemLocationType::World;
|
||||||
|
RefreshVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::SetCarried(AActor* Holder, bool bCarried)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bIsLocked = bCarried;
|
||||||
|
CurrentHolder = bCarried ? Holder : nullptr;
|
||||||
|
if (Mesh)
|
||||||
|
{
|
||||||
|
Mesh->SetSimulatePhysics(false);
|
||||||
|
Mesh->SetCollisionEnabled(bCarried ? ECollisionEnabled::NoCollision : ECollisionEnabled::QueryAndPhysics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::ReleaseToWorld(const FTransform& WorldTransform)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
|
||||||
|
SetActorTransform(WorldTransform);
|
||||||
|
bIsLocked = false;
|
||||||
|
CurrentHolder = nullptr;
|
||||||
|
if (Mesh)
|
||||||
|
{
|
||||||
|
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||||
|
}
|
||||||
|
if (ItemData.IsValid())
|
||||||
|
{
|
||||||
|
ItemData.LocationType = EItemLocationType::World;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::OnRep_ItemData()
|
||||||
|
{
|
||||||
|
RefreshVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APickupItemActor::RefreshVisuals()
|
||||||
|
{
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemData.ItemId) : nullptr;
|
||||||
|
if (!Def)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Mesh && !Def->WorldMesh.IsNull())
|
||||||
|
{
|
||||||
|
if (UStaticMesh* LoadedMesh = Def->WorldMesh.LoadSynchronous())
|
||||||
|
{
|
||||||
|
Mesh->SetStaticMesh(LoadedMesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Interactable && Interactable->DisplayName.IsEmpty())
|
||||||
|
{
|
||||||
|
Interactable->DisplayName = Def->DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Source/ItemWorld/Private/StorageContainerActor.cpp
Normal file
21
Source/ItemWorld/Private/StorageContainerActor.cpp
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "StorageContainerActor.h"
|
||||||
|
#include "InteractableComponent.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
|
||||||
|
AStorageContainerActor::AStorageContainerActor()
|
||||||
|
{
|
||||||
|
bReplicates = true;
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||||
|
SetRootComponent(Mesh);
|
||||||
|
|
||||||
|
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
|
||||||
|
|
||||||
|
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
|
||||||
|
// Shared container: visible to whoever opens it, not just an owning client.
|
||||||
|
Storage->bOwnerOnlyReplication = false;
|
||||||
|
}
|
||||||
106
Source/ItemWorld/Private/WorldInteractionActions.cpp
Normal file
106
Source/ItemWorld/Private/WorldInteractionActions.cpp
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "WorldInteractionActions.h"
|
||||||
|
#include "ItemInteractionComponent.h"
|
||||||
|
#include "ItemNativeTags.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool RouteToComponent(const FInteractionContext& Context, EItemInteractionMode Mode, const FGameplayTag& ActionTag)
|
||||||
|
{
|
||||||
|
if (!Context.Character)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (UItemInteractionComponent* Comp = Context.Character->FindComponentByClass<UItemInteractionComponent>())
|
||||||
|
{
|
||||||
|
return Comp->ServerExecuteMode(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_CarryAttached::UInteractionAction_CarryAttached()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Carry;
|
||||||
|
Mode = EItemInteractionMode::CarryAttached;
|
||||||
|
Priority = 500;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Carry", "Carry");
|
||||||
|
}
|
||||||
|
void UInteractionAction_CarryAttached::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_CarryPhysics::UInteractionAction_CarryPhysics()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Carry;
|
||||||
|
Mode = EItemInteractionMode::CarryPhysicsHandle;
|
||||||
|
Priority = 500;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "CarryPhysics", "Pick Up");
|
||||||
|
}
|
||||||
|
void UInteractionAction_CarryPhysics::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_Equip::UInteractionAction_Equip()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Equip;
|
||||||
|
Mode = EItemInteractionMode::EquipFromInventory;
|
||||||
|
Priority = 800;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Equip", "Equip");
|
||||||
|
}
|
||||||
|
void UInteractionAction_Equip::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_DropHeldItem::UInteractionAction_DropHeldItem()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Drop;
|
||||||
|
Mode = EItemInteractionMode::DropHeldItem;
|
||||||
|
Priority = 300;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Drop", "Drop");
|
||||||
|
}
|
||||||
|
void UInteractionAction_DropHeldItem::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_InstallIntoWorldSlot::UInteractionAction_InstallIntoWorldSlot()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Install;
|
||||||
|
Mode = EItemInteractionMode::InstallIntoWorldSlot;
|
||||||
|
Priority = 900;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Install", "Install");
|
||||||
|
}
|
||||||
|
void UInteractionAction_InstallIntoWorldSlot::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_RemoveFromWorldSlot::UInteractionAction_RemoveFromWorldSlot()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_Remove;
|
||||||
|
Mode = EItemInteractionMode::RemoveFromWorldSlot;
|
||||||
|
Priority = 400;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Remove", "Remove");
|
||||||
|
}
|
||||||
|
void UInteractionAction_RemoveFromWorldSlot::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
UInteractionAction_UseHeldOnTarget::UInteractionAction_UseHeldOnTarget()
|
||||||
|
{
|
||||||
|
ActionTag = ItemEcosystemTags::Action_UseHeldOnTarget;
|
||||||
|
Mode = EItemInteractionMode::UseHeldOnTarget;
|
||||||
|
Priority = 800;
|
||||||
|
DisplayText = NSLOCTEXT("ItemWorld", "Use", "Use");
|
||||||
|
}
|
||||||
|
void UInteractionAction_UseHeldOnTarget::Execute_Implementation(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
RouteToComponent(Context, Mode, ActionTag);
|
||||||
|
}
|
||||||
261
Source/ItemWorld/Private/WorldInteractionComponent.cpp
Normal file
261
Source/ItemWorld/Private/WorldInteractionComponent.cpp
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "WorldInteractionComponent.h"
|
||||||
|
#include "HeldItemComponent.h"
|
||||||
|
#include "EquipmentComponent.h"
|
||||||
|
#include "WorldItemSlotComponent.h"
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "InventoryComponent.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "ItemAuxiliaryRows.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
switch (Option.Mode)
|
||||||
|
{
|
||||||
|
case EItemInteractionMode::CarryAttached:
|
||||||
|
return ExecuteCarry(Context, /*bPhysics*/ false);
|
||||||
|
|
||||||
|
case EItemInteractionMode::CarryPhysicsHandle:
|
||||||
|
return ExecuteCarry(Context, /*bPhysics*/ true);
|
||||||
|
|
||||||
|
case EItemInteractionMode::DropHeldItem:
|
||||||
|
return ExecuteDropHeld(Context);
|
||||||
|
|
||||||
|
case EItemInteractionMode::InstallIntoWorldSlot:
|
||||||
|
return ExecuteInstall(Context);
|
||||||
|
|
||||||
|
case EItemInteractionMode::RemoveFromWorldSlot:
|
||||||
|
return ExecuteRemoveFromSlot(Context);
|
||||||
|
|
||||||
|
case EItemInteractionMode::EquipFromInventory:
|
||||||
|
return ExecuteEquip(Context, Request);
|
||||||
|
|
||||||
|
case EItemInteractionMode::UseHeldOnTarget:
|
||||||
|
return ExecuteUseHeldOnTarget(Context, Option);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Super::ExecuteAuthorized(Context, Option, Request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteCarry(const FInteractionContext& Context, bool bPhysics)
|
||||||
|
{
|
||||||
|
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||||
|
if (!Held || !Context.TargetActor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return bPhysics ? Held->ServerHoldWorldItemPhysics(Context.TargetActor)
|
||||||
|
: Held->ServerHoldWorldItem(Context.TargetActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteDropHeld(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||||
|
return Held && Held->ServerDropHeldItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteInstall(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||||
|
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemInstanceData HeldItem;
|
||||||
|
Held->GetHeldItem(HeldItem);
|
||||||
|
|
||||||
|
// Find a compatible, free slot on the target.
|
||||||
|
UWorldItemSlotComponent* Chosen = nullptr;
|
||||||
|
TArray<UWorldItemSlotComponent*> Slots;
|
||||||
|
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
|
||||||
|
for (UWorldItemSlotComponent* Slot : Slots)
|
||||||
|
{
|
||||||
|
if (Slot && Slot->CanInstall(HeldItem))
|
||||||
|
{
|
||||||
|
Chosen = Slot;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Chosen)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Install failed: no compatible free slot."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand the carried actor over to the slot as its visual.
|
||||||
|
FItemInstanceData Item;
|
||||||
|
AActor* Visual = nullptr;
|
||||||
|
if (!Held->ServerHandOffHeldItem(Item, Visual))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Chosen->InstallItem(Item, Visual))
|
||||||
|
{
|
||||||
|
// Roll back: drop the item back to the world so it is not lost.
|
||||||
|
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 100.f);
|
||||||
|
if (APickupItemActor* Pickup = Cast<APickupItemActor>(Visual))
|
||||||
|
{
|
||||||
|
Pickup->ReleaseToWorld(Xf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
|
||||||
|
if (Visual) { Visual->Destroy(); }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteEquip(const FInteractionContext& Context, const FInteractionRequest& Request)
|
||||||
|
{
|
||||||
|
UEquipmentComponent* Equip = GetOwner()->FindComponentByClass<UEquipmentComponent>();
|
||||||
|
if (!Equip)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FGameplayTag SlotTag = Request.TargetSlotTag;
|
||||||
|
if (!SlotTag.IsValid() && Context.PlayerInventory)
|
||||||
|
{
|
||||||
|
// Fall back to the item's first allowed equipment slot.
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (Context.PlayerInventory->FindItem(Request.ItemInstanceId, Item))
|
||||||
|
{
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
||||||
|
{
|
||||||
|
if (!Def->AllowedEquipmentSlots.IsEmpty())
|
||||||
|
{
|
||||||
|
SlotTag = Def->AllowedEquipmentSlots.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Equip->ServerEquip(Request.ItemInstanceId, SlotTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteUseHeldOnTarget(const FInteractionContext& Context, const FInteractionOption& Option)
|
||||||
|
{
|
||||||
|
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
|
||||||
|
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData HeldItem;
|
||||||
|
Held->GetHeldItem(HeldItem);
|
||||||
|
|
||||||
|
// Find the result verb from the data-driven interaction row for this held item.
|
||||||
|
FName ResultVerb = NAME_None;
|
||||||
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
TArray<FItemInteractionRow> Rows;
|
||||||
|
DB->GetInteractionRows(HeldItem.ItemId, Rows);
|
||||||
|
for (const FItemInteractionRow& Row : Rows)
|
||||||
|
{
|
||||||
|
if (Row.ActionTag == Option.ActionTag) { ResultVerb = Row.Result; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ResultVerb.IsNone())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate an IItemUseTarget on the target actor or its components.
|
||||||
|
IItemUseTarget* UseTarget = Cast<IItemUseTarget>(Context.TargetActor);
|
||||||
|
if (!UseTarget)
|
||||||
|
{
|
||||||
|
for (UActorComponent* Comp : Context.TargetActor->GetComponents())
|
||||||
|
{
|
||||||
|
if (IItemUseTarget* AsTarget = Cast<IItemUseTarget>(Comp)) { UseTarget = AsTarget; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!UseTarget)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UseTarget->ReceiveHeldUse(ResultVerb, HeldItem, GetOwner()))
|
||||||
|
{
|
||||||
|
Held->ServerUpdateHeldItem(HeldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::ExecuteRemoveFromSlot(const FInteractionContext& Context)
|
||||||
|
{
|
||||||
|
if (!Context.TargetActor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Find the first occupied slot on the target.
|
||||||
|
UWorldItemSlotComponent* Occupied = nullptr;
|
||||||
|
TArray<UWorldItemSlotComponent*> Slots;
|
||||||
|
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
|
||||||
|
for (UWorldItemSlotComponent* Slot : Slots)
|
||||||
|
{
|
||||||
|
if (Slot && Slot->IsOccupied()) { Occupied = Slot; break; }
|
||||||
|
}
|
||||||
|
if (!Occupied)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Occupied->RemoveItem(Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the player's inventory; otherwise drop it at their feet.
|
||||||
|
UInventoryComponent* Inv = Context.PlayerInventory;
|
||||||
|
if (Inv && Inv->AddItem(Item) && Item.Quantity <= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 30.f));
|
||||||
|
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldInteractionComponent::HandleDropFromInventory(FGuid InstanceId)
|
||||||
|
{
|
||||||
|
APawn* Pawn = Cast<APawn>(GetOwner());
|
||||||
|
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||||
|
if (!Inv)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FItemInstanceData Item;
|
||||||
|
if (!Inv->FindItem(InstanceId, Item) || Item.Lock.bLocked)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (Def && !Def->bCanDrop)
|
||||||
|
{
|
||||||
|
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Drop failed: %s cannot be dropped."), *Item.ItemId.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 20.f));
|
||||||
|
if (APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
|
||||||
|
{
|
||||||
|
Inv->RemoveItem(InstanceId);
|
||||||
|
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] dropped %s"), *Item.ItemId.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
129
Source/ItemWorld/Private/WorldItemSlotComponent.cpp
Normal file
129
Source/ItemWorld/Private/WorldItemSlotComponent.cpp
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#include "WorldItemSlotComponent.h"
|
||||||
|
#include "PickupItemActor.h"
|
||||||
|
#include "ItemWorldLog.h"
|
||||||
|
#include "ItemDatabaseSubsystem.h"
|
||||||
|
#include "ItemDefinitionRow.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
UWorldItemSlotComponent::UWorldItemSlotComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
SetIsReplicatedByDefault(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWorldItemSlotComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
DOREPLIFETIME(UWorldItemSlotComponent, InstalledItem);
|
||||||
|
DOREPLIFETIME(UWorldItemSlotComponent, InstalledVisual);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldItemSlotComponent::HasAuthority() const
|
||||||
|
{
|
||||||
|
return GetOwner() && GetOwner()->HasAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
USceneComponent* UWorldItemSlotComponent::GetAttachTarget() const
|
||||||
|
{
|
||||||
|
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldItemSlotComponent::CanInstall(const FItemInstanceData& Item) const
|
||||||
|
{
|
||||||
|
if (InstalledItem.bHasItem || !Item.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||||
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||||
|
if (!Def || !Def->bCanInstallIntoWorldSlot)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The item must declare this slot as compatible.
|
||||||
|
if (!Def->CompatibleWorldSlots.HasTag(SlotTag))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Optional extra tag gate.
|
||||||
|
if (!AcceptedItemTags.IsEmpty())
|
||||||
|
{
|
||||||
|
FGameplayTagContainer ItemTags = Def->ItemTags;
|
||||||
|
ItemTags.AddTag(Def->ItemType);
|
||||||
|
if (!ItemTags.HasAny(AcceptedItemTags))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldItemSlotComponent::InstallItem(const FItemInstanceData& Item, AActor* ProvidedVisual)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !CanInstall(Item))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstalledItem.bHasItem = true;
|
||||||
|
InstalledItem.Item = Item;
|
||||||
|
InstalledItem.Item.LocationType = EItemLocationType::Installed;
|
||||||
|
|
||||||
|
if (ProvidedVisual)
|
||||||
|
{
|
||||||
|
// Reuse the carried actor as the visual.
|
||||||
|
if (USceneComponent* Target = GetAttachTarget())
|
||||||
|
{
|
||||||
|
ProvidedVisual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
|
||||||
|
}
|
||||||
|
if (IWorldItemProvider* Provider = Cast<IWorldItemProvider>(ProvidedVisual))
|
||||||
|
{
|
||||||
|
Provider->SetCarried(GetOwner(), true); // keep collision off, locked
|
||||||
|
}
|
||||||
|
InstalledVisual = ProvidedVisual;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Spawn a fresh visual pickup and attach it.
|
||||||
|
const FTransform Xf = GetAttachTarget() ? GetAttachTarget()->GetComponentTransform() : GetOwner()->GetActorTransform();
|
||||||
|
if (APickupItemActor* Visual = APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
|
||||||
|
{
|
||||||
|
Visual->SetCarried(GetOwner(), true);
|
||||||
|
if (USceneComponent* Target = GetAttachTarget())
|
||||||
|
{
|
||||||
|
Visual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
|
||||||
|
}
|
||||||
|
InstalledVisual = Visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnSlotChanged.Broadcast();
|
||||||
|
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] installed %s into %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWorldItemSlotComponent::RemoveItem(FItemInstanceData& OutItem)
|
||||||
|
{
|
||||||
|
if (!HasAuthority() || !InstalledItem.bHasItem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutItem = InstalledItem.Item;
|
||||||
|
|
||||||
|
if (InstalledVisual)
|
||||||
|
{
|
||||||
|
InstalledVisual->Destroy();
|
||||||
|
InstalledVisual = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstalledItem = FWorldSlotInstalledState();
|
||||||
|
OnSlotChanged.Broadcast();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWorldItemSlotComponent::OnRep_InstalledItem()
|
||||||
|
{
|
||||||
|
OnSlotChanged.Broadcast();
|
||||||
|
}
|
||||||
85
Source/ItemWorld/Public/EquipmentComponent.h
Normal file
85
Source/ItemWorld/Public/EquipmentComponent.h
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "EquipmentComponent.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One equipped slot (section 11.2 Equipped). The item itself stays in the
|
||||||
|
* inventory; only the visual actor is spawned and replicated.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FEquippedItemEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
|
||||||
|
FGameplayTag SlotTag;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
|
||||||
|
FGuid InstanceId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
|
||||||
|
TObjectPtr<AActor> EquippedActor = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEquipmentChanged);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns/attaches the visual actors for equipped items (section 7, 11.2, 31.5).
|
||||||
|
*
|
||||||
|
* Equip keeps the item in the inventory and only manages the EquippedActorClass
|
||||||
|
* visual - distinct from carry (UHeldItemComponent), which physically removes the
|
||||||
|
* item from storage. Owner-only item truth, everyone sees the visual (section 22/25).
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMWORLD_API UEquipmentComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UEquipmentComponent();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
virtual void OnUnregister() override;
|
||||||
|
|
||||||
|
/** Socket per slot tag; falls back to DefaultEquipSocket. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Equipment")
|
||||||
|
TMap<FGameplayTag, FName> SlotSockets;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Equipment")
|
||||||
|
FName DefaultEquipSocket = FName("hand_rSocket");
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Equipment")
|
||||||
|
FOnEquipmentChanged OnEquipmentChanged;
|
||||||
|
|
||||||
|
/** Server: equip an inventory item into a slot (Inventory -> Equipped). */
|
||||||
|
bool ServerEquip(FGuid InstanceId, FGameplayTag SlotTag);
|
||||||
|
|
||||||
|
/** Server: remove the equipped visual from a slot. */
|
||||||
|
bool ServerUnequip(FGameplayTag SlotTag);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Equipment")
|
||||||
|
bool IsSlotEquipped(FGameplayTag SlotTag) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Equipment")
|
||||||
|
void GetEquippedEntries(TArray<FEquippedItemEntry>& OutEntries) const { OutEntries = Equipped; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_Equipped)
|
||||||
|
TArray<FEquippedItemEntry> Equipped;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_Equipped();
|
||||||
|
|
||||||
|
FName ResolveSocket(const FGameplayTag& SlotTag) const;
|
||||||
|
USceneComponent* GetAttachTarget() const;
|
||||||
|
bool HasAuthority() const;
|
||||||
|
};
|
||||||
48
Source/ItemWorld/Public/FuelTankComponent.h
Normal file
48
Source/ItemWorld/Public/FuelTankComponent.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "FuelTankComponent.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFuelChanged, float, NewFuel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fuel reservoir on a world object (generator, vehicle) that accepts fuel poured
|
||||||
|
* from a held canister (section 15, 31.4). Implements IItemUseTarget for the
|
||||||
|
* "TransferFuel" result verb.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMWORLD_API UFuelTankComponent : public UActorComponent, public IItemUseTarget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFuelTankComponent();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fuel")
|
||||||
|
float MaxFuel = 40.f;
|
||||||
|
|
||||||
|
/** Max litres poured per single use action. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fuel")
|
||||||
|
float PourAmount = 10.f;
|
||||||
|
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_Fuel, BlueprintReadOnly, Category = "Fuel")
|
||||||
|
float CurrentFuel = 0.f;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Fuel")
|
||||||
|
FOnFuelChanged OnFuelChanged;
|
||||||
|
|
||||||
|
//~ IItemUseTarget
|
||||||
|
virtual bool ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_Fuel();
|
||||||
|
|
||||||
|
bool HasAuthority() const;
|
||||||
|
};
|
||||||
151
Source/ItemWorld/Public/HeldItemComponent.h
Normal file
151
Source/ItemWorld/Public/HeldItemComponent.h
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemEnums.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "HeldItemComponent.generated.h"
|
||||||
|
|
||||||
|
class APickupItemActor;
|
||||||
|
|
||||||
|
/** Replicated snapshot of what the character is currently holding (section 11.1). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct ITEMWORLD_API FHeldItemState
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
bool bHasHeldItem = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
FGuid InstanceId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
FName ItemId;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
EHeldItemMode Mode = EHeldItemMode::None;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
TObjectPtr<AActor> HeldWorldActor = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
FName HoldSocket = NAME_None;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
float MoveSpeedMultiplier = 1.f;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
bool bBlocksSprint = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
bool bBlocksClimb = false;
|
||||||
|
|
||||||
|
/** Snapshot of the carried item (so we can drop/install it without the actor). */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Held")
|
||||||
|
FItemInstanceData Item;
|
||||||
|
};
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHeldItemChanged, const FHeldItemState&, NewState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Physically carries one bulky item attached to the character (section 11.2-11.3).
|
||||||
|
*
|
||||||
|
* This is the "two-handed carry" path for batteries, canisters, wheels, crates,
|
||||||
|
* corpses, etc. - distinct from equipment (which keeps the item in the inventory
|
||||||
|
* and only spawns a visual). Server-authoritative; visuals/penalties apply on all
|
||||||
|
* clients via OnRep. Implements IHeldItemInterface so the resolver can build
|
||||||
|
* held+target interactions.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMWORLD_API UHeldItemComponent : public UActorComponent, public IHeldItemInterface
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UHeldItemComponent();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held")
|
||||||
|
FName CarrySocket = FName("CarrySocket");
|
||||||
|
|
||||||
|
/** Hold distance in front of the view for physics-handle carry (section 11.4). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held")
|
||||||
|
float PhysicsHoldDistance = 200.f;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Held")
|
||||||
|
FOnHeldItemChanged OnHeldItemChanged;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Held")
|
||||||
|
bool HasHeldItemBP() const { return HeldItem.bHasHeldItem; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Held")
|
||||||
|
const FHeldItemState& GetHeldItemState() const { return HeldItem; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Held")
|
||||||
|
bool CanHoldItem(const FItemInstanceData& Item) const;
|
||||||
|
|
||||||
|
/** Client entry: ask the server to drop the carried item (bind to the Drop input). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Held")
|
||||||
|
void RequestDrop();
|
||||||
|
|
||||||
|
// --- Server entry points (section 11.1) ---
|
||||||
|
|
||||||
|
/** Server: pick up and carry a world item actor attached to a socket (World -> Held). */
|
||||||
|
bool ServerHoldWorldItem(AActor* WorldItem);
|
||||||
|
|
||||||
|
/** Server: carry a world item via a physics handle instead of a fixed socket (section 11.4). */
|
||||||
|
bool ServerHoldWorldItemPhysics(AActor* WorldItem);
|
||||||
|
|
||||||
|
/** Server: drop the carried item back into the world (Held -> World). */
|
||||||
|
bool ServerDropHeldItem();
|
||||||
|
|
||||||
|
/** Server: relinquish the carried item without spawning a drop (e.g. on install). */
|
||||||
|
bool ServerConsumeHeldItem(FItemInstanceData& OutItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server: hand off the carried item AND its actor to a new owner (e.g. a world
|
||||||
|
* slot) without destroying the actor. Clears the held state. Returns false if
|
||||||
|
* nothing was held.
|
||||||
|
*/
|
||||||
|
bool ServerHandOffHeldItem(FItemInstanceData& OutItem, AActor*& OutActor);
|
||||||
|
|
||||||
|
/** Server: update the carried item's data in place (e.g. after pouring out fuel). */
|
||||||
|
bool ServerUpdateHeldItem(const FItemInstanceData& NewItem);
|
||||||
|
|
||||||
|
//~ IHeldItemInterface
|
||||||
|
virtual bool GetHeldItem(FItemInstanceData& OutItem) const override;
|
||||||
|
virtual bool HasHeldItem() const override { return HeldItem.bHasHeldItem; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_HeldItem)
|
||||||
|
FHeldItemState HeldItem;
|
||||||
|
|
||||||
|
UFUNCTION(Server, Reliable)
|
||||||
|
void Server_Drop();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_HeldItem(FHeldItemState OldState);
|
||||||
|
|
||||||
|
/** Applies/removes the movement-speed penalty on the owning character. */
|
||||||
|
void ApplyMovementState();
|
||||||
|
|
||||||
|
bool HasAuthority() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Owning character's unmodified MaxWalkSpeed, captured the first time we modify it. */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
float CachedBaseWalkSpeed = 0.f;
|
||||||
|
|
||||||
|
/** Created on demand for physics-handle carry. */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<class UPhysicsHandleComponent> PhysicsHandle = nullptr;
|
||||||
|
|
||||||
|
void UpdatePhysicsCarryTarget();
|
||||||
|
};
|
||||||
8
Source/ItemWorld/Public/ItemWorldLog.h
Normal file
8
Source/ItemWorld/Public/ItemWorldLog.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Logging/LogMacros.h"
|
||||||
|
|
||||||
|
ITEMWORLD_API DECLARE_LOG_CATEGORY_EXTERN(LogItemWorld, Log, All);
|
||||||
82
Source/ItemWorld/Public/PickupItemActor.h
Normal file
82
Source/ItemWorld/Public/PickupItemActor.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "ItemInteractionInterfaces.h"
|
||||||
|
#include "PickupItemActor.generated.h"
|
||||||
|
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
class UInteractableComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The world representation of an item lying on the ground / a surface (section 12).
|
||||||
|
*
|
||||||
|
* One actor per world item only (never one per inventory item - section 32/34).
|
||||||
|
* Implements IWorldItemProvider so the interaction layer can pick it up, carry it
|
||||||
|
* or take part of its stack without ItemWorld <-> ItemInteraction coupling.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMWORLD_API APickupItemActor : public AActor, public IWorldItemProvider
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APickupItemActor();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
/** Server: stamp this pickup with an item and refresh its visuals. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|World")
|
||||||
|
void InitializeFromItem(const FItemInstanceData& InItem);
|
||||||
|
|
||||||
|
/** Server convenience: spawn a fresh pickup for ItemId at a transform. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Item|World", meta = (WorldContext = "WorldContext"))
|
||||||
|
static APickupItemActor* SpawnPickup(UObject* WorldContext, FName ItemId, int32 Quantity, const FTransform& Transform);
|
||||||
|
|
||||||
|
/** Server convenience: spawn a pickup carrying an existing instance (e.g. on drop). */
|
||||||
|
static APickupItemActor* SpawnPickupFromInstance(UObject* WorldContext, const FItemInstanceData& Item, const FTransform& Transform);
|
||||||
|
|
||||||
|
//~ IWorldItemProvider
|
||||||
|
virtual bool GetProvidedItem(FItemInstanceData& OutItem) const override;
|
||||||
|
virtual bool IsItemAvailable() const override;
|
||||||
|
virtual void NotifyItemTaken(AActor* Taker) override;
|
||||||
|
virtual void UpdateProvidedItem(const FItemInstanceData& RemainingItem) override;
|
||||||
|
virtual void SetCarried(AActor* Holder, bool bCarried) override;
|
||||||
|
|
||||||
|
/** Server: drop this carried actor back into the world at a transform. */
|
||||||
|
void ReleaseToWorld(const FTransform& WorldTransform);
|
||||||
|
|
||||||
|
UStaticMeshComponent* GetMeshComponent() const { return Mesh; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Item|World")
|
||||||
|
const FItemInstanceData& GetItemData() const { return ItemData; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
TObjectPtr<USceneComponent> Root;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
TObjectPtr<UInteractableComponent> Interactable;
|
||||||
|
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_ItemData, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
FItemInstanceData ItemData;
|
||||||
|
|
||||||
|
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
bool bIsLocked = false;
|
||||||
|
|
||||||
|
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Item|World")
|
||||||
|
TObjectPtr<AActor> CurrentHolder = nullptr;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_ItemData();
|
||||||
|
|
||||||
|
/** Resolves the definition and applies WorldMesh + interactable display name. */
|
||||||
|
void RefreshVisuals();
|
||||||
|
};
|
||||||
38
Source/ItemWorld/Public/StorageContainerActor.h
Normal file
38
Source/ItemWorld/Public/StorageContainerActor.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "StorageContainerActor.generated.h"
|
||||||
|
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
class UInteractableComponent;
|
||||||
|
class UInventoryComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lootable world container (chest, crate, fridge, vehicle cargo, corpse) (section 5).
|
||||||
|
* Its inventory is shared-replicated so any nearby player who opens it sees the
|
||||||
|
* contents; transfers go through the validated server path (section 31.2).
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class ITEMWORLD_API AStorageContainerActor : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AStorageContainerActor();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Storage")
|
||||||
|
UInventoryComponent* GetStorageInventory() const { return Storage; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||||
|
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||||
|
TObjectPtr<UInteractableComponent> Interactable;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
|
||||||
|
TObjectPtr<UInventoryComponent> Storage;
|
||||||
|
};
|
||||||
76
Source/ItemWorld/Public/WorldInteractionActions.h
Normal file
76
Source/ItemWorld/Public/WorldInteractionActions.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "InteractionAction.h"
|
||||||
|
#include "WorldInteractionActions.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World interaction actions (section 16). Each routes through the player's
|
||||||
|
* UItemInteractionComponent::ServerExecuteMode so it shares the one validated
|
||||||
|
* execution path (carry/equip/install/use live in UWorldInteractionComponent).
|
||||||
|
*/
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Carry (Attached)"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_CarryAttached : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_CarryAttached();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Carry (Physics)"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_CarryPhysics : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_CarryPhysics();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Equip"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_Equip : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_Equip();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Drop Held Item"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_DropHeldItem : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_DropHeldItem();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Install Into World Slot"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_InstallIntoWorldSlot : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_InstallIntoWorldSlot();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Remove From World Slot"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_RemoveFromWorldSlot : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_RemoveFromWorldSlot();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(meta = (DisplayName = "Action: Use Held On Target"))
|
||||||
|
class ITEMWORLD_API UInteractionAction_UseHeldOnTarget : public UInteractionAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
public:
|
||||||
|
UInteractionAction_UseHeldOnTarget();
|
||||||
|
virtual void Execute_Implementation(const FInteractionContext& Context) override;
|
||||||
|
};
|
||||||
31
Source/ItemWorld/Public/WorldInteractionComponent.h
Normal file
31
Source/ItemWorld/Public/WorldInteractionComponent.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "ItemInteractionComponent.h"
|
||||||
|
#include "WorldInteractionComponent.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interaction executor to put on a player pawn in a project that uses the
|
||||||
|
* world modules. Extends the base with the modes that need ItemWorld types:
|
||||||
|
* carry, drop-held, equip, install/remove from world slots, and drop-from-inventory
|
||||||
|
* (stages 6-8). Keeps ItemInteraction free of any ItemWorld dependency.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMWORLD_API UWorldInteractionComponent : public UItemInteractionComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request) override;
|
||||||
|
virtual bool HandleDropFromInventory(FGuid InstanceId) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ExecuteCarry(const FInteractionContext& Context, bool bPhysics);
|
||||||
|
bool ExecuteDropHeld(const FInteractionContext& Context);
|
||||||
|
bool ExecuteInstall(const FInteractionContext& Context);
|
||||||
|
bool ExecuteEquip(const FInteractionContext& Context, const FInteractionRequest& Request);
|
||||||
|
bool ExecuteUseHeldOnTarget(const FInteractionContext& Context, const FInteractionOption& Option);
|
||||||
|
bool ExecuteRemoveFromSlot(const FInteractionContext& Context);
|
||||||
|
};
|
||||||
83
Source/ItemWorld/Public/WorldItemSlotComponent.h
Normal file
83
Source/ItemWorld/Public/WorldItemSlotComponent.h
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "ItemInstanceData.h"
|
||||||
|
#include "WorldItemSlotComponent.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWorldSlotChanged);
|
||||||
|
|
||||||
|
/** Replicated state of a world slot's contents (section 14). */
|
||||||
|
USTRUCT()
|
||||||
|
struct FWorldSlotInstalledState
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
bool bHasItem = false;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FItemInstanceData Item;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A socket on a world object that accepts an installed item (section 14):
|
||||||
|
* Vehicle.Socket.Battery, Generator.Socket.Fuel, Door.Socket.BarricadePlank,
|
||||||
|
* Trap.Socket.Bait, etc. One component per slot.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
|
||||||
|
class ITEMWORLD_API UWorldItemSlotComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UWorldItemSlotComponent();
|
||||||
|
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
|
||||||
|
/** This slot's identity; items list it in CompatibleWorldSlots to fit. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FGameplayTag SlotTag;
|
||||||
|
|
||||||
|
/** Optional extra gate; empty = accept anything whose CompatibleWorldSlots has SlotTag. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FGameplayTagContainer AcceptedItemTags;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||||
|
FName AttachSocketName = NAME_None;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "World Slot")
|
||||||
|
FOnWorldSlotChanged OnSlotChanged;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "World Slot")
|
||||||
|
bool IsOccupied() const { return InstalledItem.bHasItem; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "World Slot")
|
||||||
|
bool CanInstall(const FItemInstanceData& Item) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "World Slot")
|
||||||
|
FItemInstanceData GetInstalledItem() const { return InstalledItem.Item; }
|
||||||
|
|
||||||
|
/** Server: install an item. ProvidedVisual (e.g. a carried actor) is reused if given. */
|
||||||
|
bool InstallItem(const FItemInstanceData& Item, AActor* ProvidedVisual = nullptr);
|
||||||
|
|
||||||
|
/** Server: remove and return the installed item. */
|
||||||
|
bool RemoveItem(FItemInstanceData& OutItem);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_InstalledItem)
|
||||||
|
FWorldSlotInstalledState InstalledItem;
|
||||||
|
|
||||||
|
/** Server-spawned visual for the installed item (if not provided externally). */
|
||||||
|
UPROPERTY(Replicated)
|
||||||
|
TObjectPtr<AActor> InstalledVisual = nullptr;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_InstalledItem();
|
||||||
|
|
||||||
|
USceneComponent* GetAttachTarget() const;
|
||||||
|
bool HasAuthority() const;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user