|
|
|
@ -1,5 +1,6 @@ |
|
|
|
|
public partial class ItemManager : Node |
|
|
|
|
{ |
|
|
|
|
uint _trackingIdCounter = 0; |
|
|
|
|
public Dictionary<uint, Item> TrackedItems { get; } = []; |
|
|
|
|
|
|
|
|
|
public override void _Ready() |
|
|
|
@ -13,8 +14,10 @@ public partial class ItemManager : Node |
|
|
|
|
if (item.Manager != null) throw new ArgumentException( |
|
|
|
|
$"Item '{item.GetPath()}' is already part of another scene"); |
|
|
|
|
|
|
|
|
|
// Generate a random, unused id for this item. |
|
|
|
|
uint id; do { id = Randi(); } while (TrackedItems.ContainsKey(id)); |
|
|
|
|
// 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; |
|
|
|
@ -33,31 +36,147 @@ public partial class ItemManager : Node |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// FIXME: This should be fixed in 4.3 and thus not required anymore? |
|
|
|
|
// FIXME: Don't actually rely on item path, use its TrackingId. |
|
|
|
|
// https://github.com/godotengine/godot/issues/85883 |
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
internal enum RequestFunc { RequestPickup, RequestPlace, RequestThrow } |
|
|
|
|
internal enum AcceptFunc { AcceptPickup, AcceptPlace, AcceptThrow } |
|
|
|
|
// 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)] |
|
|
|
|
internal void RelayRequest(NodePath itemPath, RequestFunc func, Godot.Collections.Array args) |
|
|
|
|
void RequestPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform) |
|
|
|
|
{ |
|
|
|
|
var item = this.GetNodeOrThrow<Item>(itemPath); |
|
|
|
|
var callable = new Callable(item, func.ToString()); |
|
|
|
|
callable.Call(args.ToArray()); |
|
|
|
|
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>() != 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)] |
|
|
|
|
internal void RelayAccept(NodePath itemPath, AcceptFunc func, Godot.Collections.Array args) |
|
|
|
|
void AcceptPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform) |
|
|
|
|
{ |
|
|
|
|
var item = this.GetNodeOrThrow<Item>(itemPath); |
|
|
|
|
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item"); |
|
|
|
|
var grid = VerifyAuthority(this.GetNodeOrThrow<Grid>(gridPath), "Grid"); |
|
|
|
|
|
|
|
|
|
if (GetMultiplayerAuthority() != item.GetMultiplayerAuthority()) |
|
|
|
|
throw new InvalidOperationException( |
|
|
|
|
$"Item {item.GetPath()} not owned by correct player"); |
|
|
|
|
item.Reparent(grid); |
|
|
|
|
item.Transform = localTransform; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var callable = new Callable(item, func.ToString()); |
|
|
|
|
callable.Call(args.ToArray()); |
|
|
|
|
|
|
|
|
|
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>() != player) return; |
|
|
|
|
|
|
|
|
|
RPC.ToAll().Send(AcceptThrow, itemTrackingId); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
[Rpc(CallLocal = true)] |
|
|
|
|
void AcceptThrow(uint itemTrackingId) |
|
|
|
|
{ |
|
|
|
|
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item"); |
|
|
|
|
var player = item.FindParentOrThrow<Player>(); |
|
|
|
|
var world = GetParent<Node3D>(); |
|
|
|
|
|
|
|
|
|
item.Reparent(world, true); |
|
|
|
|
|
|
|
|
|
// Throw item forward and up a bit. |
|
|
|
|
var basis = player.Camera.Camera.GlobalBasis; |
|
|
|
|
var direction = -basis.Z + basis.Y; |
|
|
|
|
item.ApplyImpulse(direction * 2); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> |
|
|
|
|
/// Verifies that this item is managed by this |
|
|
|
|
/// Manager instance, throwing an exception if not. |
|
|
|
|
/// </summary> |
|
|
|
|
void VerifyManager(Item item) |
|
|
|
|
{ |
|
|
|
|
if (item.Manager != this) throw new InvalidOperationException( |
|
|
|
|
$"Item manager ({item.Manager?.GetPath().ToString() ?? "null"}) is not this manager ({GetPath()})"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// <summary> |
|
|
|
|
/// 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. |
|
|
|
|
/// </summary> |
|
|
|
|
T VerifyAuthority<T>(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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|