public partial class ItemManager : Node { uint _trackingIdCounter = 0; public Dictionary TrackedItems { get; } = []; public override void _Ready() { var items = GetParent().FindChildren("*", "res://objects/Item.cs", owned: false).Cast(); foreach (var item in items) Add(item); } public void Add(Item item) { if (item.Manager != null) throw new ArgumentException( $"Item '{item.GetPath()}' is already part of another scene"); // FIXME: Synchronize items so we can get rid of this temporary fix. // // Generate a random, unused id for this item. // uint id; do { id = Randi(); } while (TrackedItems.ContainsKey(id)); var id = _trackingIdCounter++; TrackedItems.Add(id, item); item.Manager = this; item.TrackingId = id; } public void Remove(Item item) { if (item.Manager != this) throw new ArgumentException( $"Item '{item.GetPath()}' is not part of this scene"); TrackedItems.Remove(item.TrackingId); item.Manager = null; item.TrackingId = 0; } public void TryPickup(Item item) { VerifyManager(item); VerifyAuthority(item, "Item"); RPC.To(item.GetMultiplayerAuthority()).Send(RequestPickup, item.TrackingId); } [Rpc(RpcMode.AnyPeer, CallLocal = true)] void RequestPickup(uint itemTrackingId) { // TODO: Print debug messages instead of just returning? // Pickup will be requested by any player to the owner of the item. // If this message is received by anyone else, ignore it. if (!IsMultiplayerAuthority()) return; // Find the item being requested to be picked up, return if not found. if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return; // Make sure that the item shares its authority with this Manager. if (!item.IsMultiplayerAuthority()) return; var player = Multiplayer.GetRemoteSenderPlayer(); if (player.Pickup.HasItemsHeld) return; // TODO: Check if player is in range. // Have the item be rotated in the player's hands depending on how it's currently rotated. var localBasis = player.Pickup.GlobalBasis.Inverse() * item.GlobalBasis; var localTransform = new Transform3D(localBasis, Vector3.Zero); RPC.ToAll().Send(AcceptPickup, itemTrackingId, player.PeerId, localTransform); } [Rpc(CallLocal = true)] void AcceptPickup(uint itemTrackingId, int playerPeerId, Transform3D localTransform) { var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item"); var player = Game.Players.ByPeerId(playerPeerId); item.Reparent(player.Pickup); item.Transform = localTransform; } public void TryPlace(Item item, Grid grid, Transform3D localTransform) { VerifyManager(item); VerifyAuthority(item, "Item"); RPC.To(item.GetMultiplayerAuthority()).Send(RequestPlace, item.TrackingId, grid.GetPath(), localTransform); } [Rpc(RpcMode.AnyPeer, CallLocal = true)] void RequestPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform) { if (!IsMultiplayerAuthority()) return; if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return; if (!item.IsMultiplayerAuthority()) return; var player = Multiplayer.GetRemoteSenderPlayer(); // Ensure that the item is currently being held by the player. if (item.FindParentOrNull() != player) return; if (GetNodeOrNull(gridPath) is not Grid grid) return; if (!grid.IsMultiplayerAuthority()) return; // TODO: Further verify localTransform. if (!grid.CanPlaceAt(item, localTransform)) return; RPC.ToAll().Send(AcceptPlace, itemTrackingId, gridPath, localTransform); } [Rpc(CallLocal = true)] void AcceptPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform) { var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item"); var grid = VerifyAuthority(this.GetNodeOrThrow(gridPath), "Grid"); item.Reparent(grid); item.Transform = localTransform; } public void TryThrow(Item item) { VerifyManager(item); VerifyAuthority(item, "Item"); RPC.To(item.GetMultiplayerAuthority()).Send(RequestThrow, item.TrackingId); } [Rpc(RpcMode.AnyPeer, CallLocal = true)] void RequestThrow(uint itemTrackingId) { if (!IsMultiplayerAuthority()) return; if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return; if (!item.IsMultiplayerAuthority()) return; var player = Multiplayer.GetRemoteSenderPlayer(); // Ensure that the item is currently being held by the player. if (item.FindParentOrNull() != player) return; RPC.ToAll().Send(AcceptThrow, itemTrackingId); } [Rpc(CallLocal = true)] void AcceptThrow(uint itemTrackingId) { var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item"); var player = item.FindParentOrThrow(); // FIXME: Actually this doesn't work, the item won't have a Manager anymore. D: // Add item to workshop, if player is currently in one, otherwise add to world. var parent = player.EnteredWorkshop?.Objects ?? Game.Instance; item.Reparent(parent, true); // Throw item forward and up a bit. var basis = player.Camera.Camera.GlobalBasis; var direction = -basis.Z + basis.Y; item.ApplyImpulse(direction * 2); } /// /// Verifies that this item is managed by this /// Manager instance, throwing an exception if not. /// void VerifyManager(Item item) { if (item.Manager != this) throw new InvalidOperationException( $"Item manager ({item.Manager?.GetPath().ToString() ?? "null"}) is not this manager ({GetPath()})"); } /// /// Verifies that this node has the same multiplayer authority as this /// Manager instance, throwing an exception if not. Returns the node /// that was passed in. /// T VerifyAuthority(T node, string name) where T : Node { var expected = GetMultiplayerAuthority(); var actual = node.GetMultiplayerAuthority(); if (actual != expected) throw new InvalidOperationException( $"{name} authority ({actual}) is not manager authority ({expected})"); return node; } }