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