|
|
|
public partial class ItemManager : Node
|
|
|
|
{
|
|
|
|
uint _trackingIdCounter = 0;
|
|
|
|
public Dictionary<uint, Item> TrackedItems { get; } = [];
|
|
|
|
|
|
|
|
public override void _Ready()
|
|
|
|
{
|
|
|
|
var items = GetParent().FindChildren("*", "res://objects/Item.cs", owned: false).Cast<Item>();
|
|
|
|
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>() != 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<Grid>(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>() != 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>();
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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;
|
|
|
|
}
|
|
|
|
}
|