diff --git a/src/Features/Console/Application/Commands/SpawnItemCommand.cs b/src/Features/Console/Application/Commands/SpawnItemCommand.cs index 22988a6..aaf32f7 100644 --- a/src/Features/Console/Application/Commands/SpawnItemCommand.cs +++ b/src/Features/Console/Application/Commands/SpawnItemCommand.cs @@ -15,12 +15,12 @@ public sealed class SpawnItemCommand : ICommand public string Description => "Spawns an item in front of the player."; public IReadOnlyList Args { get; } - public SpawnItemCommand(IItemSpawner spawner, IItemSpawnPoint spawnPoint) + public SpawnItemCommand(IItemSpawner spawner, IItemDefinitionRegistry registry, IItemSpawnPoint spawnPoint) { _spawner = spawner; _spawnPoint = spawnPoint; - string[] definitions = spawner.DefinitionIds + string[] definitions = registry.DefinitionIds .Select(id => id.Value) .OrderBy(id => id, StringComparer.Ordinal) .ToArray(); diff --git a/src/Features/Interactions/Godot/InteractableController.cs b/src/Features/Interactions/Godot/InteractableController.cs index 3aca615..73140f0 100644 --- a/src/Features/Interactions/Godot/InteractableController.cs +++ b/src/Features/Interactions/Godot/InteractableController.cs @@ -11,7 +11,7 @@ public abstract partial class InteractableController : Node3D, IInteractionTarge public const string InteractionGroup = "interactions"; [Export] - public string Key; + public string InstanceKey; [Export] public string PromptText = "Wejdź w interakcję"; @@ -51,7 +51,7 @@ public abstract partial class InteractableController : Node3D, IInteractionTarge public override void _Ready() { - InteractionTargetId = InteractionTargetId.From(this.GetStableInstanceIdFrom(Key)); + InteractionTargetId = InteractionTargetId.From(this.GetStableInstanceIdFrom(InstanceKey)); AddToGroup(InteractionGroup); RegisterWithAvailableRegistry(); } diff --git a/src/Features/Items/Application/ItemInteractionService.cs b/src/Features/Items/Application/ItemInteractionService.cs index c5a976a..d256d64 100644 --- a/src/Features/Items/Application/ItemInteractionService.cs +++ b/src/Features/Items/Application/ItemInteractionService.cs @@ -8,7 +8,7 @@ namespace Game.Features.Items.Application; public sealed class ItemInteractionService( HandsInventory hands, - IItemRepository itemRepository, + IItemInstanceRegistry itemRegistry, IHeldItemPresenter presenter ) : IItemInteractionService { @@ -20,7 +20,7 @@ public sealed class ItemInteractionService( public ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand) { - if (!itemRepository.TryGet(itemId, out IItemInstance item)) + if (!itemRegistry.TryGet(itemId, out IItemInstance item)) { return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found."); } @@ -68,7 +68,7 @@ public sealed class ItemInteractionService( return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty."); } - if (!itemRepository.TryGet(itemId.Value, out IItemInstance item)) + if (!itemRegistry.TryGet(itemId.Value, out IItemInstance item)) { return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found."); } @@ -90,7 +90,7 @@ public sealed class ItemInteractionService( return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty."); } - if (!itemRepository.TryGet(itemId.Value, out IItemInstance item)) + if (!itemRegistry.TryGet(itemId.Value, out IItemInstance item)) { return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found."); } diff --git a/src/Features/Items/Contracts/IItemDefinitionRegistry.cs b/src/Features/Items/Contracts/IItemDefinitionRegistry.cs new file mode 100644 index 0000000..4f35058 --- /dev/null +++ b/src/Features/Items/Contracts/IItemDefinitionRegistry.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Game.Features.Items.Contracts; + +public interface IItemDefinitionRegistry +{ + IReadOnlyCollection DefinitionIds { get; } + bool TryRegister(ItemDefinitionId definitionId, ItemDefinition definition); + bool TryGetDefinition(ItemDefinitionId definitionId, out ItemDefinition definition); +} diff --git a/src/Features/Items/Contracts/IItemDefinitionRegistry.cs.uid b/src/Features/Items/Contracts/IItemDefinitionRegistry.cs.uid new file mode 100644 index 0000000..3bceeaa --- /dev/null +++ b/src/Features/Items/Contracts/IItemDefinitionRegistry.cs.uid @@ -0,0 +1 @@ +uid://kjka28plyqeo diff --git a/src/Features/Items/Contracts/IItemInstanceRegistry.cs b/src/Features/Items/Contracts/IItemInstanceRegistry.cs new file mode 100644 index 0000000..782b569 --- /dev/null +++ b/src/Features/Items/Contracts/IItemInstanceRegistry.cs @@ -0,0 +1,8 @@ +namespace Game.Features.Items.Contracts; + +public interface IItemInstanceRegistry +{ + void Register(ItemInstanceId itemId, IItemInstance item); + void Unregister(ItemInstanceId itemId); + bool TryGet(ItemInstanceId itemId, out IItemInstance item); +} diff --git a/src/Features/Items/Contracts/IItemInstanceRegistry.cs.uid b/src/Features/Items/Contracts/IItemInstanceRegistry.cs.uid new file mode 100644 index 0000000..671ee84 --- /dev/null +++ b/src/Features/Items/Contracts/IItemInstanceRegistry.cs.uid @@ -0,0 +1 @@ +uid://s83yytevpj52 diff --git a/src/Features/Items/Contracts/IItemRepository.cs b/src/Features/Items/Contracts/IItemRepository.cs deleted file mode 100644 index c603ea3..0000000 --- a/src/Features/Items/Contracts/IItemRepository.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Game.Features.Items.Contracts; - -public interface IItemRepository -{ - bool TryGet(ItemInstanceId itemId, out IItemInstance item); -} diff --git a/src/Features/Items/Contracts/IItemRepository.cs.uid b/src/Features/Items/Contracts/IItemRepository.cs.uid deleted file mode 100644 index f2885ce..0000000 --- a/src/Features/Items/Contracts/IItemRepository.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://e7n1m6v4k9r3q diff --git a/src/Features/Items/Contracts/IItemSpawner.cs b/src/Features/Items/Contracts/IItemSpawner.cs index aa95994..044aab8 100644 --- a/src/Features/Items/Contracts/IItemSpawner.cs +++ b/src/Features/Items/Contracts/IItemSpawner.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; using Godot; namespace Game.Features.Items.Contracts; public interface IItemSpawner { - IReadOnlyCollection DefinitionIds { get; } - ItemActionResult TrySpawn(ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId); + ItemActionResult TrySpawn( + ItemDefinitionId definitionId, + Vector3 worldPosition, + out ItemInstanceId itemId + ); } diff --git a/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs index 2ac1b27..bd64e60 100644 --- a/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs +++ b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs @@ -5,7 +5,7 @@ using Godot; namespace Game.Features.Items.Godot.Flashlight; -public partial class FlashlightItemController : WorldItemController +public partial class FlashlightItemController : ItemController { [Export] public SpotLight3D WorldLight; diff --git a/src/Features/Items/Godot/PlayerHeldItemPresenter.cs b/src/Features/Items/Godot/HeldItemPresenter.cs similarity index 68% rename from src/Features/Items/Godot/PlayerHeldItemPresenter.cs rename to src/Features/Items/Godot/HeldItemPresenter.cs index d5f38e8..be14519 100644 --- a/src/Features/Items/Godot/PlayerHeldItemPresenter.cs +++ b/src/Features/Items/Godot/HeldItemPresenter.cs @@ -3,15 +3,15 @@ using Godot; namespace Game.Features.Items.Godot; -public sealed class PlayerHeldItemPresenter( - ItemWorldController itemWorld, +public sealed class HeldItemPresenter( + ItemControllerRegistryController itemWorld, Node3D leftHandAnchor, Node3D rightHandAnchor ) : IHeldItemPresenter { public bool TryShowHeld(IItemInstance item, Hand hand) { - if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller)) + if (!itemWorld.TryGetController(item.Info.Id, out ItemController controller)) { return false; } @@ -23,7 +23,7 @@ public sealed class PlayerHeldItemPresenter( public bool TryShowDropped(IItemInstance item, Vector3 worldPosition) { - if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller)) + if (!itemWorld.TryGetController(item.Info.Id, out ItemController controller)) { return false; } @@ -34,7 +34,7 @@ public sealed class PlayerHeldItemPresenter( public void Refresh(IItemInstance item) { - if (itemWorld.TryGetController(item.Info.Id, out WorldItemController controller)) + if (itemWorld.TryGetController(item.Info.Id, out ItemController controller)) { controller.ApplyState(item); } diff --git a/src/Features/Items/Godot/PlayerHeldItemPresenter.cs.uid b/src/Features/Items/Godot/HeldItemPresenter.cs.uid similarity index 100% rename from src/Features/Items/Godot/PlayerHeldItemPresenter.cs.uid rename to src/Features/Items/Godot/HeldItemPresenter.cs.uid diff --git a/src/Features/Items/Godot/WorldItemController.cs b/src/Features/Items/Godot/ItemController.cs similarity index 67% rename from src/Features/Items/Godot/WorldItemController.cs rename to src/Features/Items/Godot/ItemController.cs index ccd27d2..4d55b6c 100644 --- a/src/Features/Items/Godot/WorldItemController.cs +++ b/src/Features/Items/Godot/ItemController.cs @@ -6,17 +6,30 @@ using Godot; namespace Game.Features.Items.Godot; -public partial class WorldItemController : RigidBody3D, IPickupTarget +public partial class ItemController : RigidBody3D, IPickupTarget { public const string ItemGroup = "world_items"; - [Export] public string Key; - [Export] public string DefinitionId = "item"; - [Export] public string DisplayName = "Item"; - [Export] public HandRequirement HandRequirement = HandRequirement.One; - [Export] public CollisionShape3D Collision; - [Export] public Node3D WorldVisual; - [Export] public PackedScene HeldViewScene; + [Export] + public string InstanceKey; + + [Export] + public string DefinitionId = "item"; + + [Export] + public string DisplayName = "Item"; + + [Export] + public HandRequirement HandRequirement = HandRequirement.One; + + [Export] + public CollisionShape3D Collision; + + [Export] + public Node3D WorldVisual; + + [Export] + public PackedScene HeldViewScene; protected Node3D HeldView { get; private set; } protected bool IsHeld => HeldView is not null; @@ -25,7 +38,7 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget public ItemDefinition Definition { get; private set; } public IItemInstance Item { get; private set; } - private ItemWorldController _registry; + private ItemControllerRegistryController _registry; public override void _Ready() { @@ -41,16 +54,19 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget throw new InvalidOperationException($"{Name}: held view scene is not assigned."); } - Definition = new ItemDefinition(new ItemDefinitionId(DefinitionId), DisplayName, HandRequirement); + Definition = new ItemDefinition( + new ItemDefinitionId(DefinitionId), + DisplayName, + HandRequirement + ); ItemInfo = new ItemInfo( - ItemInstanceId.From(this.GetStableInstanceIdFrom(Key)), + ItemInstanceId.From(this.GetStableInstanceIdFrom(InstanceKey)), Definition.Id, Definition.DisplayName, Definition.HandRequirement ); Item = CreateItemInstance(ItemInfo); AddToGroup(ItemGroup); - RegisterWithAvailableWorld(); ApplyState(Item); } @@ -59,7 +75,7 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget _registry?.Unregister(ItemInfo.Id); } - public void RegisterWith(ItemWorldController registry) + public void RegisterWith(ItemControllerRegistryController registry) { _registry = registry; } @@ -109,29 +125,12 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget OnPresentationChanged(item); } - protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) => new PassiveItemInstance(itemInfo); + protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) => + new PassiveItemInstance(itemInfo); - protected virtual void OnHeldViewCreated(Node3D heldView) - { - } + protected virtual void OnHeldViewCreated(Node3D heldView) { } - protected virtual void OnHeldViewRemoving(Node3D heldView) - { - } + protected virtual void OnHeldViewRemoving(Node3D heldView) { } - protected virtual void OnPresentationChanged(IItemInstance item) - { - } - - private void RegisterWithAvailableWorld() - { - foreach (Node node in GetTree().GetNodesInGroup(ItemWorldController.RegistryGroup)) - { - if (node is ItemWorldController registry) - { - registry.Register(this); - return; - } - } - } + protected virtual void OnPresentationChanged(IItemInstance item) { } } diff --git a/src/Features/Items/Godot/WorldItemController.cs.uid b/src/Features/Items/Godot/ItemController.cs.uid similarity index 100% rename from src/Features/Items/Godot/WorldItemController.cs.uid rename to src/Features/Items/Godot/ItemController.cs.uid diff --git a/src/Features/Items/Godot/ItemControllerRegistryController.cs b/src/Features/Items/Godot/ItemControllerRegistryController.cs new file mode 100644 index 0000000..99e0213 --- /dev/null +++ b/src/Features/Items/Godot/ItemControllerRegistryController.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Game.Features.Items.Contracts; +using Godot; + +namespace Game.Features.Items.Godot; + +public partial class ItemControllerRegistryController : Node +{ + private IItemInstanceRegistry _registry; + private readonly Dictionary _items = []; + + public IReadOnlyCollection Items => _items.Values; + + public void UseRegistry(IItemInstanceRegistry registry) + { + _registry = registry; + } + + public void RegisterSceneItems() + { + foreach (Node node in GetTree().GetNodesInGroup(ItemController.ItemGroup)) + { + if (node is ItemController item) + { + Register(item); + } + } + } + + public void Register(ItemController item) + { + if (_items.TryGetValue(item.ItemInfo.Id, out ItemController existing)) + { + if (ReferenceEquals(existing, item)) + { + return; + } + + throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}."); + } + + _items.Add(item.ItemInfo.Id, item); + _registry.Register(item.ItemInfo.Id, item.Item); + item.RegisterWith(this); + } + + public void Unregister(ItemInstanceId itemId) + { + _items.Remove(itemId); + _registry.Unregister(itemId); + } + + public bool TryGetController(ItemInstanceId itemId, out ItemController controller) + { + return _items.TryGetValue(itemId, out controller); + } +} diff --git a/src/Features/Items/Godot/ItemWorldController.cs.uid b/src/Features/Items/Godot/ItemControllerRegistryController.cs.uid similarity index 100% rename from src/Features/Items/Godot/ItemWorldController.cs.uid rename to src/Features/Items/Godot/ItemControllerRegistryController.cs.uid diff --git a/src/Features/Items/Godot/ItemSceneRegistryController.cs b/src/Features/Items/Godot/ItemSceneRegistryController.cs new file mode 100644 index 0000000..68faef9 --- /dev/null +++ b/src/Features/Items/Godot/ItemSceneRegistryController.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Game.Features.Items.Contracts; +using Game.Godot.Extensions; +using Godot; + +namespace Game.Features.Items.Godot; + +public partial class ItemSceneRegistryController : Node +{ + [Export] + public ItemControllerRegistryController ItemWorld; + + [Export] + public global::Godot.Collections.Array ItemScenes = []; + + private readonly Dictionary _scenes = []; + + private IItemDefinitionRegistry _definitions; + + public void UseRegistry(IItemDefinitionRegistry registry) + { + _definitions = registry; + } + + public override void _Ready() + { + this.AssertRequiredExports(); + } + + public void Initialize() + { + _scenes.Clear(); + + foreach (PackedScene scene in ItemScenes) + { + RegisterItem(scene, throwOnDuplicate: true); + } + + foreach (ItemController item in ItemWorld.Items) + { + string scenePath = item.SceneFilePath; + if (string.IsNullOrWhiteSpace(scenePath)) + { + continue; + } + + RegisterItem(ResourceLoader.Load(scenePath), throwOnDuplicate: false); + } + + if (_scenes.Count == 0) + { + throw new InvalidOperationException("The item spawn catalog is empty."); + } + + foreach (ItemController item in ItemWorld.Items) + { + ValidateAgainstCatalog(item); + } + } + + public bool TryGetScene(ItemDefinitionId definitionId, out PackedScene scene) + { + return _scenes.TryGetValue(definitionId, out scene); + } + + private void RegisterItem(PackedScene itemScene, bool throwOnDuplicate) + { + if (itemScene is null) + { + return; + } + + ItemController prototype = itemScene.Instantiate(); + ItemDefinitionId definitionId = new(prototype.DefinitionId); + ItemDefinition definition = new( + definitionId, + prototype.DisplayName, + prototype.HandRequirement + ); + prototype.Free(); + + if (!_definitions.TryRegister(definitionId, definition)) + { + if (throwOnDuplicate) + { + throw new InvalidOperationException( + $"Duplicate item spawn definition: {definitionId}." + ); + } + + return; + } + + _scenes.Add(definitionId, itemScene); + } + + private void ValidateAgainstCatalog(ItemController item) + { + if (!_definitions.TryGetDefinition(item.Definition.Id, out ItemDefinition canonical)) + { + return; + } + + if (!item.Definition.Equals(canonical)) + { + throw new InvalidOperationException( + $"{item.Name}: item definition drifted from the catalog for '{item.Definition.Id}'." + ); + } + } +} diff --git a/src/Features/Items/Godot/ItemSceneRegistryController.cs.uid b/src/Features/Items/Godot/ItemSceneRegistryController.cs.uid new file mode 100644 index 0000000..a2a0911 --- /dev/null +++ b/src/Features/Items/Godot/ItemSceneRegistryController.cs.uid @@ -0,0 +1 @@ +uid://cft7r2j1f0sav diff --git a/src/Features/Items/Godot/ItemSpawnerController.cs b/src/Features/Items/Godot/ItemSpawnerController.cs index edbcccc..4601eb8 100644 --- a/src/Features/Items/Godot/ItemSpawnerController.cs +++ b/src/Features/Items/Godot/ItemSpawnerController.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Game.Features.Items.Contracts; using Game.Godot.Extensions; using Godot; @@ -9,57 +6,29 @@ namespace Game.Features.Items.Godot; public partial class ItemSpawnerController : Node, IItemSpawner { - [Export] public Node3D ItemsRoot; - [Export] public ItemWorldController ItemWorld; - [Export] public global::Godot.Collections.Array ItemScenes = []; + [Export] + public Node3D ItemsRoot; - private readonly Dictionary _scenes = []; + [Export] + public ItemControllerRegistryController ItemWorld; - public IReadOnlyCollection DefinitionIds => _scenes.Keys.ToArray(); + [Export] + public ItemSceneRegistryController Scenes; public override void _Ready() { this.AssertRequiredExports(); } - public void Initialize() - { - _scenes.Clear(); - - foreach (PackedScene scene in ItemScenes) - { - RegisterScene(scene); - } - - foreach (WorldItemController item in ItemWorld.Items) - { - string scenePath = item.SceneFilePath; - if (string.IsNullOrWhiteSpace(scenePath)) - { - continue; - } - - PackedScene scene = ResourceLoader.Load(scenePath); - if (scene is not null) - { - _scenes.TryAdd(item.ItemInfo.DefinitionId, scene); - } - } - - if (_scenes.Count == 0) - { - throw new InvalidOperationException("The item spawn catalog is empty."); - } - } - public ItemActionResult TrySpawn( ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId - ) { + ) + { itemId = default; - if (!_scenes.TryGetValue(definitionId, out PackedScene scene)) + if (!Scenes.TryGetScene(definitionId, out PackedScene scene)) { return ItemActionResult.Fail( ItemActionStatus.UnknownDefinition, @@ -67,28 +36,11 @@ public partial class ItemSpawnerController : Node, IItemSpawner ); } - WorldItemController item = scene.Instantiate(); + ItemController item = scene.Instantiate(); ItemsRoot.AddChild(item); item.GlobalPosition = worldPosition; ItemWorld.Register(item); itemId = item.ItemInfo.Id; return ItemActionResult.Ok(); } - - private void RegisterScene(PackedScene scene) - { - if (scene is null) - { - return; - } - - WorldItemController prototype = scene.Instantiate(); - ItemDefinitionId definitionId = new(prototype.DefinitionId); - prototype.Free(); - - if (!_scenes.TryAdd(definitionId, scene)) - { - throw new InvalidOperationException($"Duplicate item spawn definition: {definitionId}."); - } - } } diff --git a/src/Features/Items/Godot/ItemWorldController.cs b/src/Features/Items/Godot/ItemWorldController.cs deleted file mode 100644 index 36974d0..0000000 --- a/src/Features/Items/Godot/ItemWorldController.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using Game.Features.Items.Contracts; -using Godot; - -namespace Game.Features.Items.Godot; - -public partial class ItemWorldController : Node, IItemRepository -{ - public const string RegistryGroup = "item_world_registry"; - - private readonly Dictionary _items = []; - - public IReadOnlyCollection Items => _items.Values; - - public override void _EnterTree() - { - AddToGroup(RegistryGroup); - } - - public void RegisterSceneItems() - { - foreach (Node node in GetTree().GetNodesInGroup(WorldItemController.ItemGroup)) - { - if (node is WorldItemController item) - { - Register(item); - } - } - } - - public void Register(WorldItemController item) - { - if (_items.TryGetValue(item.ItemInfo.Id, out WorldItemController existing)) - { - if (ReferenceEquals(existing, item)) - { - return; - } - - throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}."); - } - - _items.Add(item.ItemInfo.Id, item); - item.RegisterWith(this); - } - - public void Unregister(ItemInstanceId itemId) - { - _items.Remove(itemId); - } - - public bool TryGet(ItemInstanceId itemId, out IItemInstance item) - { - if (_items.TryGetValue(itemId, out WorldItemController controller)) - { - item = controller.Item; - return true; - } - - item = null; - return false; - } - - public bool TryGetController(ItemInstanceId itemId, out WorldItemController controller) - { - return _items.TryGetValue(itemId, out controller); - } -} diff --git a/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs b/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs new file mode 100644 index 0000000..2a2448a --- /dev/null +++ b/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Game.Features.Items.Contracts; + +namespace Game.Features.Items.Infrastructure; + +public sealed class ItemDefinitionRegistry : IItemDefinitionRegistry +{ + private readonly Dictionary _definitionsById = []; + + public IReadOnlyCollection DefinitionIds => _definitionsById.Keys.ToArray(); + + public bool TryRegister(ItemDefinitionId definitionId, ItemDefinition definition) => + _definitionsById.TryAdd(definitionId, definition); + + public bool TryGetDefinition(ItemDefinitionId definitionId, out ItemDefinition definition) => + _definitionsById.TryGetValue(definitionId, out definition); +} diff --git a/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs.uid b/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs.uid new file mode 100644 index 0000000..39176b0 --- /dev/null +++ b/src/Features/Items/Infrastructure/ItemDefinitionRegistry.cs.uid @@ -0,0 +1 @@ +uid://72g8eg0lvlui diff --git a/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs b/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs new file mode 100644 index 0000000..9643db3 --- /dev/null +++ b/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Game.Features.Items.Contracts; + +namespace Game.Features.Items.Infrastructure; + +public sealed class ItemInstanceRegistry : IItemInstanceRegistry +{ + private readonly Dictionary _itemsById = []; + + public void Register(ItemInstanceId itemId, IItemInstance item) + { + if (_itemsById.TryGetValue(itemId, out IItemInstance existing)) + { + if (ReferenceEquals(existing, item)) + { + return; + } + + throw new InvalidOperationException($"Duplicate item instance id: {itemId}."); + } + + _itemsById.Add(itemId, item); + } + + public void Unregister(ItemInstanceId itemId) + { + _itemsById.Remove(itemId); + } + + public bool TryGet(ItemInstanceId itemId, out IItemInstance item) => + _itemsById.TryGetValue(itemId, out item); +} diff --git a/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs.uid b/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs.uid new file mode 100644 index 0000000..cfd5446 --- /dev/null +++ b/src/Features/Items/Infrastructure/ItemInstanceRegistry.cs.uid @@ -0,0 +1 @@ +uid://dfbylp383il2v diff --git a/src/Godot/Exceptions/ControllerNotInitializedException.cs.uid b/src/Godot/Exceptions/ControllerNotInitializedException.cs.uid new file mode 100644 index 0000000..76d0c10 --- /dev/null +++ b/src/Godot/Exceptions/ControllerNotInitializedException.cs.uid @@ -0,0 +1 @@ +uid://dj8kxrwjoyfvi diff --git a/src/Godot/GameBootstrap.cs b/src/Godot/GameBootstrap.cs index b12290c..ddf9688 100644 --- a/src/Godot/GameBootstrap.cs +++ b/src/Godot/GameBootstrap.cs @@ -2,14 +2,15 @@ using System; using Game.Features.Console.Application.Commands; using Game.Features.Console.Godot; using Game.Features.Console.Infrastructure; -using Game.Features.Logging.Godot; -using Game.Features.Logging.Infrastructure; using Game.Features.Interactions.Application; using Game.Features.Interactions.Godot; using Game.Features.Interactions.Infrastructure; using Game.Features.Items.Application; using Game.Features.Items.Domain; using Game.Features.Items.Godot; +using Game.Features.Items.Infrastructure; +using Game.Features.Logging.Godot; +using Game.Features.Logging.Infrastructure; using Game.Features.Multiplayer.Godot; using Game.Features.Players.Contracts; using Game.Features.Players.Godot; @@ -21,16 +22,33 @@ namespace Game.Godot; public partial class GameBootstrap : Node { - [Export] public ConsoleController Console; - [Export] public PlayerController Player; - [Export] public PlayerInteractionController PlayerInteraction; - [Export] public ItemWorldController ItemWorld; - [Export] public ItemSpawnerController ItemSpawner; - [Export] public InteractionsRegistryController InteractionWorld; - [Export] public MultiplayerController MultiplayerController; + [Export] + public ConsoleController Console; + + [Export] + public PlayerController Player; + + [Export] + public PlayerInteractionController PlayerInteraction; + + [Export] + public ItemControllerRegistryController ItemWorld; + + [Export] + public ItemSceneRegistryController ItemScenes; + + [Export] + public ItemSpawnerController ItemSpawner; + + [Export] + public InteractionsRegistryController InteractionWorld; + + [Export] + public MultiplayerController MultiplayerController; private FileLogger _fileLogger; private CompositeLogger _logger; + private ItemDefinitionRegistry _itemDefinitions; private void InitializeLogging() { @@ -38,7 +56,7 @@ public partial class GameBootstrap : Node _logger = new CompositeLogger([ new GodotConsoleLogger(), new LogTargetLogger(Console), - _fileLogger + _fileLogger, ]); } @@ -49,7 +67,7 @@ public partial class GameBootstrap : Node registry.Register(new EchoCommand()); registry.Register(new ClearCommand()); registry.Register(new NoclipCommand(Player)); - registry.Register(new SpawnItemCommand(ItemSpawner, PlayerInteraction)); + registry.Register(new SpawnItemCommand(ItemSpawner, _itemDefinitions, PlayerInteraction)); registry.Register(new HostCommand(MultiplayerController)); registry.Register(new JoinCommand(MultiplayerController)); @@ -59,28 +77,40 @@ public partial class GameBootstrap : Node private void InitializeMultiplayer() { MultiplayerController.UsePeerFactory(new EnetMultiplayerPeerFactory()); - MultiplayerController.Connected += () => _logger.Info("Multiplayer", "Connected successfully."); - MultiplayerController.ConnectionFailed += () => _logger.Warning("Multiplayer", "Connection failed."); - MultiplayerController.Disconnected += () => _logger.Warning("Multiplayer", "Server disconnected."); + MultiplayerController.Connected += () => + _logger.Info("Multiplayer", "Connected successfully."); + MultiplayerController.ConnectionFailed += () => + _logger.Warning("Multiplayer", "Connection failed."); + MultiplayerController.Disconnected += () => + _logger.Warning("Multiplayer", "Server disconnected."); MultiplayerController.PeerJoined += id => _logger.Info("Multiplayer", $"Peer {id} joined."); MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left."); } private void InitializeItems() { + ItemInstanceRegistry itemInstanceRegistry = new(); + ItemWorld.UseRegistry(itemInstanceRegistry); ItemWorld.RegisterSceneItems(); - ItemSpawner.Initialize(); + + _itemDefinitions = new ItemDefinitionRegistry(); + ItemScenes.UseRegistry(_itemDefinitions); + ItemScenes.Initialize(); InteractionRegistry interactionRegistry = new(); InteractionWorld.UseRegistry(interactionRegistry); InteractionWorld.RegisterSceneTargets(); - PlayerHeldItemPresenter presenter = new( + HeldItemPresenter presenter = new( ItemWorld, PlayerInteraction.LeftHandAnchor, PlayerInteraction.RightHandAnchor ); - ItemInteractionService itemInteractions = new(new HandsInventory(), ItemWorld, presenter); + ItemInteractionService itemInteractions = new( + new HandsInventory(), + itemInstanceRegistry, + presenter + ); Interact interactions = new(interactionRegistry); PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions); } @@ -90,7 +120,7 @@ public partial class GameBootstrap : Node this.AssertRequiredExports(); MouseCapture.Capture(); - + InitializeLogging(); InitializeItems(); InitializeMultiplayer(); diff --git a/src/Godot/Scenes/GameWorld.tscn b/src/Godot/Scenes/GameWorld.tscn index 5dcf1c0..13b2add 100644 --- a/src/Godot/Scenes/GameWorld.tscn +++ b/src/Godot/Scenes/GameWorld.tscn @@ -6,9 +6,10 @@ [ext_resource type="Script" uid="uid://drctu5y21ey2y" path="res://src/Godot/GameBootstrap.cs" id="3_bootstrap"] [ext_resource type="PackedScene" uid="uid://bk0771dvonxh8" path="res://src/Features/Console/Godot/ConsoleController.tscn" id="4_console"] [ext_resource type="Script" uid="uid://drqx0wiw0sbj1" path="res://src/Features/Multiplayer/Godot/MultiplayerController.cs" id="5_multiplayer"] -[ext_resource type="Script" uid="uid://f8n3b6w1k5r9m" path="res://src/Features/Items/Godot/ItemWorldController.cs" id="6_items"] +[ext_resource type="Script" uid="uid://f8n3b6w1k5r9m" path="res://src/Features/Items/Godot/ItemControllerRegistryController.cs" id="6_items"] [ext_resource type="PackedScene" uid="uid://cv8m3q1y6r4nf" path="res://src/Features/Items/Godot/Flashlight/FlashlightItem.tscn" id="7_flashlight"] [ext_resource type="Script" uid="uid://c3x9m6w2q8r1b" path="res://src/Features/Items/Godot/ItemSpawnerController.cs" id="8_spawner"] +[ext_resource type="Script" path="res://src/Features/Items/Godot/ItemSceneRegistryController.cs" id="8b_catalog"] [ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionsRegistryController.cs" id="9_interactions"] [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"] @@ -22,12 +23,13 @@ background_mode = 2 sky = SubResource("Sky_1kpcd") glow_enabled = true -[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemSpawner", "InteractionWorld", "MultiplayerController")] +[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemScenes", "ItemSpawner", "InteractionWorld", "MultiplayerController")] script = ExtResource("3_bootstrap") Console = NodePath("Console") Player = NodePath("Player") PlayerInteraction = NodePath("Player/PlayerInteraction") ItemWorld = NodePath("ItemWorld") +ItemScenes = NodePath("ItemScenesRegistry") ItemSpawner = NodePath("ItemSpawner") InteractionWorld = NodePath("InteractionWorld") MultiplayerController = NodePath("Multiplayer") @@ -50,11 +52,16 @@ script = ExtResource("5_multiplayer") [node name="ItemWorld" type="Node" parent="."] script = ExtResource("6_items") -[node name="ItemSpawner" type="Node" parent="." node_paths=PackedStringArray("ItemsRoot", "ItemWorld")] +[node name="ItemScenesRegistry" type="Node" parent="." node_paths=PackedStringArray("ItemWorld")] +script = ExtResource("8b_catalog") +ItemWorld = NodePath("../ItemWorld") +ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")]) + +[node name="ItemSpawner" type="Node" parent="." node_paths=PackedStringArray("ItemsRoot", "ItemWorld", "Scenes")] script = ExtResource("8_spawner") ItemsRoot = NodePath("../Entities") ItemWorld = NodePath("../ItemWorld") -ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")]) +Scenes = NodePath("../ItemScenesRegistry") [node name="InteractionWorld" type="Node" parent="."] script = ExtResource("9_interactions")