using Godot.NativeInterop; public partial class Item : RigidBody3D { public MultiplayerSynchronizer Sync { get; internal set; } public ItemManager Manager { get; internal set; } = null; // TODO: Replace with Owner? public uint TrackingId { get; internal set; } /// Child node representing the 3D model of this item. public virtual Node3D Model => GetNode("Model"); /// Whether this item is attached to a grid. public bool IsAttached => GetParent() is Grid; /// Size of the item in grid spaces. [Export] public Vector3I Size { get; set; } public override void _Ready() { // Set the collision properties here so we don't have to specify them in each item scene separately. CollisionLayer = (uint)(PhysicsLayer.Item | PhysicsLayer.Pickup); CollisionMask = (uint)(PhysicsLayer.Static | PhysicsLayer.Dynamic | PhysicsLayer.Player | PhysicsLayer.Item); // TODO: Find a better way to better import models with colliders. // TODO: Import items dynamically at runtime? // TODO: Use PostImport tool script? foreach (var body in FindChildren("*", "StaticBody3D").Cast()) { foreach (var shape in body.GetChildren().OfType()) shape.Reparent(this); body.GetParent().RemoveChild(body); } // Set up syncronization for this item when its physics are enabled. // Sync = new() { RootPath = ".." }; // var config = Sync.ReplicationConfig = new(); // config.AddProperty(":position"); // config.AddProperty(":rotation"); // config.AddProperty(":linear_velocity"); // config.AddProperty(":angular_velocity"); // AddChild(Sync); UpdatePhysicsState(); } public override void _Notification(int what) { switch ((long)what) { case NotificationParented: if (IsInsideTree()) UpdatePhysicsState(); break; } } void UpdatePhysicsState() { Freeze = IsAttached; // Sync.PublicVisibility = !IsAttached; } public void TryPickup() { if (this.IsAuthority()) RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptPickup, new Godot.Collections.Array { Multiplayer.GetUniqueId() }); // RPC.ToAll().Send(AcceptPickup, Multiplayer.GetUniqueId()); else RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), ItemManager.RequestFunc.RequestPickup, new Godot.Collections.Array()); // RPC.To(GetMultiplayerAuthority()).Send(RequestPickup); } [Rpc(RpcMode.AnyPeer)] void RequestPickup() { // Pickup will be requested by any player to the owner of the item. // If this message is received by anyone else, ignore it (for now). if (!IsMultiplayerAuthority()) return; var player = Multiplayer.GetRemoteSenderPlayer(); if (player.Pickup.HasItemsHeld) return; // TODO: Check if player is in range. RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptPickup, new Godot.Collections.Array { player.PeerId }); // RPC.ToAll().Send(AcceptPickup, player.PeerId); } [Rpc(CallLocal = true)] void AcceptPickup(int peerId) { var player = Game.Players.ByPeerId(peerId); Reparent(player.Pickup); Transform = Transform3D.Identity; // TODO: Rotate item? } public void TryPlace(Grid grid, Transform3D localTransform) { if (this.IsAuthority()) RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptPlace, new Godot.Collections.Array { grid.GetPath(), localTransform }); // RPC.ToAll().Send(AcceptPlace, grid.GetPath(), localTransform); else RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), ItemManager.RequestFunc.RequestPlace, new Godot.Collections.Array { grid.GetPath(), localTransform }); // RPC.To(GetMultiplayerAuthority()).Send(RequestPlace, grid.GetPath(), localTransform); } [Rpc(RpcMode.AnyPeer)] void RequestPlace(NodePath gridPath, Transform3D localTransform) { // Must be received by the owner of this item. if (!IsMultiplayerAuthority()) return; if (GetNodeOrNull(gridPath) is not Grid grid) return; // Item and Grid must be owned by the same peer. if (!grid.IsMultiplayerAuthority()) return; if (!grid.CanPlaceAt(this, localTransform)) return; RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptPlace, new Godot.Collections.Array { gridPath, localTransform }); // RPC.ToAll().Send(AcceptPlace, gridPath, localTransform); } [Rpc(CallLocal = true)] void AcceptPlace(NodePath gridPath, Transform3D localTransform) { if (GetNodeOrNull(gridPath) is not Grid grid) return; // Item and Grid must be owned by the same peer. if (grid.GetMultiplayerAuthority() != GetMultiplayerAuthority()) return; Reparent(grid); Transform = localTransform; } public void TryThrow() { if (this.IsAuthority()) RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptThrow, new Godot.Collections.Array()); // RPC.ToAll().Send(AcceptThrow); else RPC.To(GetMultiplayerAuthority()).Send(Manager.RelayRequest, GetPath(), ItemManager.RequestFunc.RequestThrow, new Godot.Collections.Array()); // RPC.To(GetMultiplayerAuthority()).Send(RequestThrow); } [Rpc(RpcMode.AnyPeer)] void RequestThrow() { if (!IsMultiplayerAuthority()) return; var player = Multiplayer.GetRemoteSenderPlayer(); if (this.FindParentOrNull() != player) return; RPC.ToAll().Send(Manager.RelayAccept, GetPath(), ItemManager.AcceptFunc.AcceptThrow, new Godot.Collections.Array()); // RPC.ToAll().Send(AcceptThrow); } [Rpc(CallLocal = true)] void AcceptThrow() { var player = this.FindParentOrThrow(); var world = Manager.GetParent(); Reparent(world, true); // Throw item forward and up a bit. var basis = player.Camera.Camera.GlobalBasis; var direction = -basis.Z + basis.Y; ApplyImpulse(direction * 2); } }