diff --git a/addons/HLNC/HLNC.props b/addons/HLNC/HLNC.props index 929441c..6ad5611 100644 --- a/addons/HLNC/HLNC.props +++ b/addons/HLNC/HLNC.props @@ -1,5 +1,6 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/addons/HLNC/IBlastoffDriver.cs b/addons/HLNC/IBlastoffDriver.cs index 550775a..9884828 100644 --- a/addons/HLNC/IBlastoffDriver.cs +++ b/addons/HLNC/IBlastoffDriver.cs @@ -12,11 +12,10 @@ public interface IBlastoffServerDriver { /// /// Validate whether the user is allowed to join the desired zone (i.e. server instance) or not. /// - /// The zoneId being requested by the client. This is generated either statically when the Blastoff server is initially spun up, or dynamically when the Blastoff server generates new instances. /// The token sent from the client. This may be a JWT, HMAC, or anything else desired for authentication. /// An optional output parameter to ask Blastoff connect the client to. Only used if the user is not valid and false is returned. /// Return true to allow the user to join. Return false to reject. - public bool BlastoffValidatePeer(Guid zoneId, string token, out Guid redirect); + public bool BlastoffValidatePeer(string token, out Guid redirect); } @@ -32,11 +31,5 @@ public interface IBlastoffClientDriver { /// /// The user's authentication token which is validated in public string BlastoffGetToken(); - - /// - /// NetworkRunner uses this to tell the server which zone the client is trying to connect to. - /// - /// The Zone ID, utilized in the validation process of - public Guid BlastoffGetZoneId(); } } \ No newline at end of file diff --git a/addons/HLNC/INetworkInputHandler.cs b/addons/HLNC/INetworkInputHandler.cs deleted file mode 100644 index 6c466b9..0000000 --- a/addons/HLNC/INetworkInputHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Godot; -using Godot.Collections; - -namespace HLNC -{ - public interface INetworkInputHandler - { - public Godot.Collections.Dictionary InputBuffer { get; } - } -} \ No newline at end of file diff --git a/addons/HLNC/NetworkNode3D.cs b/addons/HLNC/NetworkNode3D.cs index 2f50667..1c67525 100644 --- a/addons/HLNC/NetworkNode3D.cs +++ b/addons/HLNC/NetworkNode3D.cs @@ -1,10 +1,15 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using Godot; +using Grpc.Core; using HLNC.Serialization; using HLNC.Serialization.Serializers; -using Newtonsoft.Json.Linq; +using Microsoft.VisualBasic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; namespace HLNC { @@ -18,15 +23,39 @@ Network properties can only update on the server side. The server receives client inputs, can access them via , and handle them accordingly within NetworkProcess to mutate state. */ + [Tool] public partial class NetworkNode3D : Node3D, IStateSerializable, INotifyPropertyChanged { public bool IsNetworkScene => GetMeta("is_network_scene", false).AsBool(); internal List NetworkSceneChildren = []; + internal List> InitialSetNetworkProperties = []; + public WorldRunner CurrentWorld { get; internal set; } + public Godot.Collections.Dictionary InputBuffer = []; + public Godot.Collections.Dictionary InterestLayers = []; + + [Signal] + public delegate void InterestChangedEventHandler(string peerId, long interestLayers); + public void SetPeerInterest(string peerId, long interestLayers, bool recurse = true) + { + InterestLayers[peerId] = interestLayers; + EmitSignal("InterestChanged", peerId, interestLayers); + if (recurse) + { + foreach (var child in GetNetworkChildren(NetworkChildrenSearchToggle.INCLUDE_SCENES)) + { + child.SetPeerInterest(peerId, interestLayers, recurse); + } + } + } /// public override void _ExitTree() { + if (Engine.IsEditorHint()) + { + return; + } base._ExitTree(); if (NetworkParent != null && NetworkParent.Node is NetworkNode3D _networkNodeParent) { @@ -57,35 +86,41 @@ public NetworkId NetworkParentId } _networkParentId = value; { - if (IsNetworkScene && value != 0 && NetworkRunner.Instance.GetFromNetworkId(value).Node is NetworkNode3D _networkNodeParent) + if (IsNetworkScene && value != 0 && CurrentWorld.GetNodeFromNetworkId(value).Node is NetworkNode3D _networkNodeParent) { _networkNodeParent.NetworkSceneChildren.Add(new NetworkNodeWrapper(this)); } } } } - public NetworkNodeWrapper NetworkParent { get { - return NetworkRunner.Instance.GetFromNetworkId(NetworkParentId); - } internal set { - NetworkParentId = value?.NetworkId ?? 0; - } } + public NetworkNodeWrapper NetworkParent + { + get + { + if (CurrentWorld == null) return null; + return CurrentWorld.GetNodeFromNetworkId(NetworkParentId); + } + internal set + { + NetworkParentId = value?.NetworkId ?? 0; + } + } public bool DynamicSpawn { get; internal set; } = false; // Cannot have more than 8 serializers - public IStateSerailizer[] Serializers { get; private set; } + public IStateSerailizer[] Serializers { get; private set; } = []; - public JObject ToJSON(bool recurse = true) + public BsonDocument ToBSONDocument(bool recurse = true, HashSet skipTypes = null) { if (!IsNetworkScene) { - throw new System.Exception("Only scenes can be converted to JSON: " + GetPath()); + throw new System.Exception("Only scenes can be converted to BSON: " + GetPath()); } - - var result = new JObject(); - result["data"] = new JObject(); + BsonDocument result = new BsonDocument(); + result["data"] = new BsonDocument(); if (IsNetworkScene) { - result["scenePack"] = NetworkScenesRegister.SCENES_PACK[SceneFilePath]; + result["scene"] = SceneFilePath; } // We retain this for debugging purposes. result["nodeName"] = Name.ToString(); @@ -96,59 +131,82 @@ public JObject ToJSON(bool recurse = true) { var nodePath = staticNetworkNodePathAndProps.Key; var nodeProps = staticNetworkNodePathAndProps.Value; - result["data"][nodePath] = new JObject(); - var nodeData = result["data"][nodePath] as JObject; + result["data"][nodePath] = new BsonDocument(); + var nodeData = result["data"][nodePath] as BsonDocument; + var hasValues = false; foreach (var property in nodeProps) { var prop = GetNode(nodePath).Get(property.Value.Name); - if (prop.VariantType == Variant.Type.String) + if (property.Value.Type == Variant.Type.String) { nodeData[property.Value.Name] = prop.ToString(); } - else if (prop.VariantType == Variant.Type.Float) + else if (property.Value.Type == Variant.Type.Float) { nodeData[property.Value.Name] = prop.AsDouble(); } - else if (prop.VariantType == Variant.Type.Int) + else if (property.Value.Type == Variant.Type.Int) { nodeData[property.Value.Name] = prop.AsInt64(); } - else if (prop.VariantType == Variant.Type.Bool) + else if (property.Value.Type == Variant.Type.Bool) { nodeData[property.Value.Name] = prop.AsBool(); } - else if (prop.VariantType == Variant.Type.Vector2) + else if (property.Value.Type == Variant.Type.Vector2) { var vec = prop.As(); - nodeData[property.Value.Name] = new JArray(vec.X, vec.Y); + nodeData[property.Value.Name] = new BsonArray { vec.X, vec.Y }; } - else if (prop.VariantType == Variant.Type.Vector3) + else if (property.Value.Type == Variant.Type.Vector3) { var vec = prop.As(); - nodeData[property.Value.Name] = new JArray(vec.X, vec.Y, vec.Z); + nodeData[property.Value.Name] = new BsonArray { vec.X, vec.Y, vec.Z }; + } + else if (property.Value.Type == Variant.Type.Nil) + { + nodeData[property.Value.Name] = null; + } + else if (property.Value.Type == Variant.Type.PackedByteArray) + { + if (property.Value.Subtype == VariantSubtype.Guid) + { + nodeData[property.Value.Name] = new BsonBinaryData(new Guid(prop.AsByteArray()), GuidRepresentation.Standard); + } + else + { + nodeData[property.Value.Name] = new BsonBinaryData(prop.AsByteArray(), BsonBinarySubType.Binary); + } } + else + { + GD.PrintErr("Serializing to JSON unsupported property type: ", prop.VariantType); + continue; + } + hasValues = true; } - if (!nodeData.HasValues) + if (!hasValues) { - (result["data"] as JObject).Remove(nodePath); + // Delete empty objects from JSON, i.e. network nodes with no network properties. + (result["data"] as BsonDocument).Remove(nodePath); } } } if (recurse) { - result["children"] = new JObject(); + result["children"] = new BsonDocument(); foreach (var child in NetworkSceneChildren) { - if (child.Node is NetworkNode3D networkChild) + if (child.Node is NetworkNode3D networkChild && (skipTypes == null || !skipTypes.Contains(networkChild.GetType()))) { string pathTo = GetPathTo(networkChild.GetParent()); - if (!(result["children"] as JObject).ContainsKey(pathTo)) + if (!(result["children"] as BsonDocument).Contains(pathTo)) { - result["children"][pathTo] = new JArray(); + result["children"][pathTo] = new BsonArray(); } - (result["children"][pathTo] as JArray).Add(networkChild.ToJSON()); + (result["children"][pathTo] as BsonArray).Add(networkChild.ToBSONDocument()); } } } @@ -156,16 +214,29 @@ public JObject ToJSON(bool recurse = true) return result; } - public static async Task FromJSON(JObject data) + public async void FromBSON(byte[] data) { - NetworkNode3D node; - if (data.ContainsKey("scenePack")) - { - node = NetworkScenesRegister.SCENES_MAP[(byte)data["scenePack"]].Instantiate(); - } - else + await NetworkNode3D.FromBSON(data, this); + } + + public static async Task FromBSON(byte[] data, T fillNode = null) where T : NetworkNode3D + { + return await FromBSON(BsonSerializer.Deserialize(data), fillNode); + } + + public static async Task FromBSON(BsonDocument data, T fillNode = null) where T : NetworkNode3D + { + T node = fillNode; + if (fillNode == null) { - node = new NetworkNode3D(); + if (data.Contains("scene")) + { + node = GD.Load(data["scene"].AsString).Instantiate(); + } + else + { + throw new System.Exception("No scene path found in BSON data"); + } } var tcs = new TaskCompletionSource(); node.Ready += () => @@ -178,19 +249,18 @@ public static async Task FromJSON(JObject data) } else { - child.Node.SetMeta("import_from_json", true); + child.Node.SetMeta("import_from_external", true); } } - node.SetMeta("import_from_json", true); + node.SetMeta("import_from_external", true); tcs.SetResult(true); }; NetworkRunner.Instance.AddChild(node); await tcs.Task; - NetworkRunner.Instance.RemoveChild(node); - foreach (var networkNodePathAndProps in data["data"] as JObject) + foreach (var networkNodePathAndProps in data["data"] as BsonDocument) { - var nodePath = networkNodePathAndProps.Key; - var nodeProps = networkNodePathAndProps.Value as JObject; + var nodePath = networkNodePathAndProps.Name; + var nodeProps = networkNodePathAndProps.Value as BsonDocument; var targetNode = node.GetNodeOrNull(nodePath); if (targetNode == null) { @@ -199,44 +269,57 @@ public static async Task FromJSON(JObject data) } foreach (var prop in nodeProps) { - var variantType = targetNode.Get(prop.Key).VariantType; + node.InitialSetNetworkProperties.Add(new Tuple(nodePath, prop.Name)); + var variantType = targetNode.Get(prop.Name).VariantType; + var propData = NetworkScenesRegister.PROPERTIES_MAP[node.SceneFilePath][nodePath][prop.Name]; if (variantType == Variant.Type.String) { - targetNode.Set(prop.Key, prop.Value.ToString()); + targetNode.Set(prop.Name, prop.Value.ToString()); } else if (variantType == Variant.Type.Float) { - targetNode.Set(prop.Key, (float)prop.Value); + targetNode.Set(prop.Name, prop.Value.AsDouble); } else if (variantType == Variant.Type.Int) { - targetNode.Set(prop.Key, (int)prop.Value); + targetNode.Set(prop.Name, prop.Value.AsInt64); } else if (variantType == Variant.Type.Bool) { - targetNode.Set(prop.Key, (bool)prop.Value); + targetNode.Set(prop.Name, (bool)prop.Value); } else if (variantType == Variant.Type.Vector2) { - var vec = prop.Value as JArray; - targetNode.Set(prop.Key, new Vector2((float)vec[0], (float)vec[1])); + var vec = prop.Value as BsonArray; + targetNode.Set(prop.Name, new Vector2((float)vec[0].AsDouble, (float)vec[1].AsDouble)); + } + else if (variantType == Variant.Type.PackedByteArray) + { + if (propData.Subtype == VariantSubtype.Guid) + { + targetNode.Set(prop.Name, new BsonBinaryData(prop.Value.AsGuid, GuidRepresentation.CSharpLegacy).AsByteArray); + } + else + { + targetNode.Set(prop.Name, prop.Value.AsByteArray); + } } else if (variantType == Variant.Type.Vector3) { - var vec = prop.Value as JArray; - targetNode.Set(prop.Key, new Vector3((float)vec[0], (float)vec[1], (float)vec[2])); + var vec = prop.Value as BsonArray; + targetNode.Set(prop.Name, new Vector3((float)vec[0].AsDouble, (float)vec[1].AsDouble, (float)vec[2].AsDouble)); } } } - if (data.ContainsKey("children")) + if (data.Contains("children")) { - foreach (var child in data["children"] as JObject) + foreach (var child in data["children"] as BsonDocument) { - var nodePath = child.Key; - var children = child.Value as JArray; + var nodePath = child.Name; + var children = child.Value as BsonArray; foreach (var childData in children) { - var childNode = await FromJSON(childData as JObject); + var childNode = await FromBSON(childData as BsonDocument); var parent = node.GetNodeOrNull(nodePath); if (parent == null) { @@ -247,13 +330,14 @@ public static async Task FromJSON(JObject data) } } } + NetworkRunner.Instance.RemoveChild(node); return node; } [Signal] public delegate void NetworkPropertyChangedEventHandler(string nodePath, StringName propertyName); - public NetworkNode3D() + public override void _EnterTree() { if (GetType().GetCustomAttributes(typeof(NetworkScenes), true).Length > 0) { @@ -264,11 +348,8 @@ public NetworkNode3D() { return; } - Serializers = [ - new SpawnSerializer(new NetworkNodeWrapper(this)), - new NetworkPropertiesSerializer(new NetworkNodeWrapper(this)), - ]; } + public NetworkId NetworkId { get; internal set; } = -1; public NetPeer InputAuthority { get; internal set; } = null; @@ -277,8 +358,6 @@ public bool IsCurrentOwner get { return NetworkRunner.Instance.IsServer || (!NetworkRunner.Instance.IsServer && InputAuthority == NetworkRunner.Instance.ENetHost); } } - public Dictionary Interest = []; - public static NetworkNode3D FindFromChild(Node node) { while (node != null) @@ -329,16 +408,21 @@ public void Despawn() // NetworkRunner.Instance.Despawn(this); } - public void _NetworkPrepare() + public void _NetworkPrepare(WorldRunner world) { if (Engine.IsEditorHint()) { return; } + CurrentWorld = world; + if (IsNetworkScene) { - NetworkRunner.Instance.RegisterSpawn(new NetworkNodeWrapper(this)); + var networkChildren = GetNetworkChildren(NetworkNode3D.NetworkChildrenSearchToggle.INCLUDE_SCENES).ToList(); + networkChildren.Reverse(); + networkChildren.ForEach(child => child._NetworkPrepare(world)); + world.RegisterSpawn(new NetworkNodeWrapper(this)); if (!NetworkRunner.Instance.IsServer) { // Clients dequeue network scenes and prepare them later via serializers triggered by the server. @@ -373,9 +457,26 @@ public void _NetworkPrepare() } } + SetupSerializers(true); + foreach (var initialSetProp in InitialSetNetworkProperties) + { + EmitSignal("NetworkPropertyChanged", initialSetProp.Item1, initialSetProp.Item2); + } _NetworkReady(); } + internal void SetupSerializers(bool checkIfNetworkScene = false) + { + if (!checkIfNetworkScene || IsNetworkScene) + { + var spawnSerializer = new SpawnSerializer(); + AddChild(spawnSerializer); + var propertySerializer = new NetworkPropertiesSerializer(); + AddChild(propertySerializer); + Serializers = [spawnSerializer, propertySerializer]; + } + } + public virtual void _NetworkReady() { IsNetworkReady = true; @@ -389,36 +490,29 @@ public virtual void _NetworkProcess(int _tick) } if (NetworkRunner.Instance.IsServer) return; - - if (IsCurrentOwner && !NetworkRunner.Instance.IsServer && this is INetworkInputHandler) - { - INetworkInputHandler inputHandler = (INetworkInputHandler)this; - if (inputHandler.InputBuffer.Count > 0) - { - // NetworkRunner.Instance.RpcId(1, "TransferInput", NetworkRunner.Instance.CurrentTick, (byte)NetworkId, inputHandler.InputBuffer); - inputHandler.InputBuffer.Clear(); - } - } } public Godot.Collections.Dictionary GetInput() { if (!IsCurrentOwner) return null; - if (!NetworkRunner.Instance.InputStore.ContainsKey(InputAuthority)) + if (!CurrentWorld.InputStore.ContainsKey(InputAuthority)) return null; byte netId; - if (NetworkRunner.Instance.IsServer) { - netId = NetworkPeerManager.Instance.GetPeerNodeId(InputAuthority, new NetworkNodeWrapper(this)); - } else { + if (NetworkRunner.Instance.IsServer) + { + netId = CurrentWorld.GetPeerNodeId(InputAuthority, new NetworkNodeWrapper(this)); + } + else + { netId = (byte)NetworkId; } - if (!NetworkRunner.Instance.InputStore[InputAuthority].ContainsKey(netId)) + if (!CurrentWorld.InputStore[InputAuthority].ContainsKey(netId)) return null; - var inputs = NetworkRunner.Instance.InputStore[InputAuthority][netId]; - NetworkRunner.Instance.InputStore[InputAuthority].Remove(netId); + var inputs = CurrentWorld.InputStore[InputAuthority][netId]; + CurrentWorld.InputStore[InputAuthority].Remove(netId); return inputs; } diff --git a/addons/HLNC/NetworkNodeWrapper.cs b/addons/HLNC/NetworkNodeWrapper.cs index 7aeec6b..03eaedf 100644 --- a/addons/HLNC/NetworkNodeWrapper.cs +++ b/addons/HLNC/NetworkNodeWrapper.cs @@ -58,6 +58,9 @@ public NetworkNodeWrapper(Node node) { "NetworkId", "NetworkParentId", "DynamicSpawn", + "CurrentWorld", + "InputBuffer", + "InterestLayers" }; foreach (var prop in node.GetPropertyList()) { var pascalName = ToPascalCase(prop["name"].AsString()); @@ -165,7 +168,17 @@ internal set { } } - public NetworkNodeWrapper NetworkParent => NetworkRunner.Instance.GetFromNetworkId(NetworkParentId); + public WorldRunner CurrentWorld { + get { + return Get("CurrentWorld").As(); + } + + internal set { + Set("CurrentWorld", value); + } + } + + public NetworkNodeWrapper NetworkParent => CurrentWorld.GetNodeFromNetworkId(NetworkParentId); // TODO: Handle null? internal byte NetworkSceneId => NetworkScenesRegister.SCENES_PACK[Node.SceneFilePath]; @@ -190,6 +203,26 @@ internal set { } } + public Godot.Collections.Dictionary InputBuffer { + get { + return Get("InputBuffer").As>(); + } + + internal set { + Set("InputBuffer", value); + } + } + + public Godot.Collections.Dictionary InterestLayers { + get { + return Get("InterestLayers").As>(); + } + + internal set { + Set("InterestLayers", value); + } + } + public IStateSerailizer[] Serializers { get { // TODO: Support serializers across other languages / node types @@ -200,14 +233,18 @@ public IStateSerailizer[] Serializers { } } - internal void _NetworkPrepare() { - Call("_NetworkPrepare"); + internal void _NetworkPrepare(WorldRunner worldRunner) { + Call("_NetworkPrepare", [worldRunner]); } public void _NetworkProcess(Tick tick) { Call("_NetworkProcess", tick); } + public void SetPeerInterest(string peerId, long interestLayers, bool recurse = true) { + Call("SetPeerInterest", peerId, interestLayers, recurse); + } + public List StaticNetworkChildren { get { return NetworkScenesRegister.STATIC_NETWORK_NODE_PATHS[Node.SceneFilePath].Aggregate(new List(), (acc, path) => { diff --git a/addons/HLNC/NetworkPeerManager/IPeerController.cs b/addons/HLNC/NetworkPeerManager/IPeerController.cs deleted file mode 100644 index 78b4a91..0000000 --- a/addons/HLNC/NetworkPeerManager/IPeerController.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Godot; - -namespace HLNC -{ - /// - /// Manages the state of a peer in the network, from the perspective of the server. - /// - public interface IPeerStateController - { - - /// - /// Unused - /// - public enum PeerSyncState { - /// - /// Unused - /// - INITIAL - } - - /// - /// Attempts to register a NetworkNode for a peer. This is necessary because peers track different IDs for NetworkNodes than the server does. - /// The reason why is that the Network tracks IDs as an int64, but we don't want to send a full int64 over the network for every node. - /// - /// The NetworkNode to register with the peer - /// The peer in question - /// - public byte TryRegisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null); - - /// - /// Remove the NetworkNode from the peer's registry. - /// - /// The NetworkNode to remove - /// The peer in question - public void DeregisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null); - - /// - /// The most recently acknowledged Tick from the peer. This is not necessarily the current peer's actual Tick (almost certainly slightly behind) - /// - public Tick CurrentTick { get; } - - /// - /// Begin the process of changing the scene. - /// - /// The Networked Scene node to change to - public void ChangeScene(NetworkNodeWrapper node); - - /// - /// Find the peer's ID for a network node as was registered in - /// - /// - /// - /// - public byte GetPeerNodeId(NetPeer peer, NetworkNodeWrapper node); - - /// - /// Net a NetworkNode by ID. - /// - /// The ID of the node. This will be different between the server and the client. - /// - public NetworkNodeWrapper GetNetworkNode(NetworkId networkId); - - /// - /// Check if a client has acknowledged a tick wherein a node was spawned. - /// - /// The server's NetworkId for that node. - /// The peer in question - /// - public bool HasSpawnedForClient(NetworkId networkId, NetPeer peer); - - /// - /// Indicate that a client has acknowledged a tick wherein a node was spawned. - /// - /// The server's NetworkId for that node. - /// The peer in question - /// - public void SetSpawnedForClient(NetworkId networkId, NetPeer peer); - } - -} \ No newline at end of file diff --git a/addons/HLNC/NetworkPeerManager/NetworkPeerManager.cs b/addons/HLNC/NetworkPeerManager/NetworkPeerManager.cs deleted file mode 100644 index 80a13a5..0000000 --- a/addons/HLNC/NetworkPeerManager/NetworkPeerManager.cs +++ /dev/null @@ -1,379 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; -using Godot; -using HLNC.Serialization; - -namespace HLNC -{ - internal partial class NetworkPeerManager : Node, IPeerStateController - { - readonly static byte MAX_NETWORK_NODES = 64; - - // A bit list of all nodes in use by each peer - // For example, 0 0 0 0 (... etc ...) 0 1 0 1 would mean that the first and third nodes are in use - readonly private Dictionary availablePeerNodes = []; - readonly private Dictionary> globalNodeToLocalNodeMap = []; - readonly private Dictionary> localNodeToGlobalNodeMap = []; - private Dictionary> spawnAware = []; - public Dictionary PeerSyncState = []; - public Tick CurrentTick => NetworkRunner.Instance.CurrentTick; - - public bool HasSpawnedForClient(NetworkId networkId, NetPeer peer) - { - if (!spawnAware.ContainsKey(peer)) - { - return false; - } - if (!spawnAware[peer].ContainsKey(networkId)) - { - return false; - } - return spawnAware[peer][networkId]; - } - - public void SetSpawnedForClient(NetworkId networkId, NetPeer peer) - { - if (!spawnAware.ContainsKey(peer)) - { - spawnAware[peer] = []; - } - spawnAware[peer][networkId] = true; - } - - public void ChangeScene(NetworkNodeWrapper node) - { - if (NetworkRunner.Instance.IsServer) return; - - if (NetworkRunner.Instance.CurrentScene != null) { - NetworkRunner.Instance.CurrentScene.Node.QueueFree(); - } - GD.Print("Changing scene to " + node.Node.Name); - // TODO: Support this more generally - GetTree().CurrentScene.AddChild(node.Node); - NetworkRunner.Instance.CurrentScene = node; - var networkChildren = (node.Node as NetworkNode3D).GetNetworkChildren(NetworkNode3D.NetworkChildrenSearchToggle.INCLUDE_SCENES).ToList(); - networkChildren.Reverse(); - networkChildren.ForEach(child => child._NetworkPrepare()); - node._NetworkPrepare(); - } - - public IPeerStateController.PeerSyncState GetPeerSyncState(NetPeer peer) - { - if (!PeerSyncState.ContainsKey(peer)) - { - return IPeerStateController.PeerSyncState.INITIAL; - } - return PeerSyncState[peer]; - } - - private struct PendingSyncState - { - public Tick tick; - public IPeerStateController.PeerSyncState state; - } - - readonly private Dictionary pendingSyncStates = []; - public void SetPeerSyncState(NetPeer peer, IPeerStateController.PeerSyncState state) - { - PeerSyncState[peer] = state; - } - - public void QueuePeerSyncState(NetPeer peer, IPeerStateController.PeerSyncState state) - { - pendingSyncStates[peer] = new PendingSyncState - { - tick = CurrentTick, - state = state - }; - } - - public NetworkNodeWrapper GetNetworkNode(NetworkId networkId) - { - if (NetworkRunner.Instance.NetworkScenes.ContainsKey(networkId)) - { - return NetworkRunner.Instance.NetworkScenes[networkId]; - } - return null; - } - public byte GetPeerNodeId(NetPeer peer, NetworkNodeWrapper node) - { - if (node == null) return 0; - if (!globalNodeToLocalNodeMap.ContainsKey(peer)) - { - return 0; - } - if (!globalNodeToLocalNodeMap[peer].ContainsKey(node.NetworkId)) - { - return 0; - } - return globalNodeToLocalNodeMap[peer][node.NetworkId]; - } - - public NetworkNodeWrapper GetPeerNode(NetPeer peer, byte networkId) - { - if (!localNodeToGlobalNodeMap.ContainsKey(peer)) - { - return null; - } - if (!localNodeToGlobalNodeMap[peer].ContainsKey(networkId)) - { - return null; - } - return NetworkRunner.Instance.NetworkScenes[localNodeToGlobalNodeMap[peer][networkId]]; - } - - public void DeregisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null) - { - if (NetworkRunner.Instance.IsServer) - { - if (peer == null) - { - GD.PrintErr("Server must specify a peer when deregistering a node."); - return; - } - if (globalNodeToLocalNodeMap[peer].ContainsKey(node.NetworkId)) - { - availablePeerNodes[peer] &= ~(1 << globalNodeToLocalNodeMap[peer][node.NetworkId]); - globalNodeToLocalNodeMap[peer].Remove(node.NetworkId); - } - } - else - { - NetworkRunner.Instance.NetworkScenes.Remove(node.NetworkId); - } - } - - // A local peer node ID is assigned to each node that a peer owns - // This allows us to sync nodes across the network without sending long integers - // 0 indicates that the node is not registered. Node ID starts at 1 - // Up to 64 nodes can be networked per peer at a time. - // TODO: Consider supporting more - // TODO: Handle de-registration of nodes (e.g. despawn, and object interest) - public byte TryRegisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null) - { - if (NetworkRunner.Instance.IsServer) - { - if (peer == null) - { - GD.PrintErr("Server must specify a peer when registering a node."); - return 0; - } - if (globalNodeToLocalNodeMap[peer].ContainsKey(node.NetworkId)) - { - return globalNodeToLocalNodeMap[peer][node.NetworkId]; - } - for (byte i = 0; i < MAX_NETWORK_NODES; i++) - { - byte localNodeId = (byte)(i + 1); - if ((availablePeerNodes[peer] & ((long)1 << localNodeId)) == 0) - { - globalNodeToLocalNodeMap[peer][node.NetworkId] = localNodeId; - localNodeToGlobalNodeMap[peer][localNodeId] = node.NetworkId; - availablePeerNodes[peer] |= (long)1 << localNodeId; - return localNodeId; - } - } - - GD.PrintErr("Peer " + peer + " has reached the maximum amount of nodes."); - return 0; - } - - if (NetworkRunner.Instance.NetworkScenes.ContainsKey(node.NetworkId)) - { - return 0; - } - - NetworkRunner.Instance.NetworkScenes[node.NetworkId] = node; - return 1; - } - - public bool is_loading = true; - - private static NetworkPeerManager _instance; - public static NetworkPeerManager Instance => _instance; - - /// - public override void _EnterTree() - { - if (_instance != null) - { - QueueFree(); - } - _instance = this; - } - - [Signal] - public delegate void PlayerJoinedEventHandler(NetPeer peer); - - public void RegisterPlayer(NetPeer peer) - { - PeerSyncState[peer] = IPeerStateController.PeerSyncState.INITIAL; - globalNodeToLocalNodeMap[peer] = []; - localNodeToGlobalNodeMap[peer] = []; - availablePeerNodes[peer] = 0; - } - - public Dictionary ExportState(Godot.Collections.Array peers) - { - Dictionary peerBuffers = []; - foreach (ENetPacketPeer peer in peers) - { - long updatedNodes = 0; - peerBuffers[peer] = new HLBuffer(); - var peerNodesBuffers = new Dictionary(); - var peerNodesSerializersList = new Dictionary(); - foreach (var node in NetworkRunner.Instance.NetworkScenes.Values) - { - var serializersBuffer = new HLBuffer(); - byte serializersRun = 0; - for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) - { - var serializer = node.Serializers[serializerIdx]; - var serializerResult = serializer.Export(this, peer); - if (serializerResult.bytes.Length == 0) - { - continue; - } - serializersRun |= (byte)(1 << serializerIdx); - HLBytes.Pack(serializersBuffer, serializerResult.bytes); - } - if (serializersRun == 0) - { - continue; - } - byte localNodeId = globalNodeToLocalNodeMap[peer][node.NetworkId]; - updatedNodes |= 1 << localNodeId; - peerNodesSerializersList[localNodeId] = serializersRun; - peerNodesBuffers[localNodeId] = new HLBuffer(); - HLBytes.Pack(peerNodesBuffers[localNodeId], serializersBuffer.bytes); - } - - // 1. Pack a bit list of all nodes which have serialized data - HLBytes.Pack(peerBuffers[peer], updatedNodes); - - // 2. Pack what serializers are run for every node - var orderedNodeKeys = peerNodesBuffers.OrderBy(x => x.Key).Select(x => x.Key).ToList(); - foreach (var nodeKey in orderedNodeKeys) - { - HLBytes.Pack(peerBuffers[peer], peerNodesSerializersList[nodeKey]); - } - - // 3. Pack the serialized data for every node - foreach (var nodeKey in orderedNodeKeys) - { - HLBytes.Pack(peerBuffers[peer], peerNodesBuffers[nodeKey].bytes); - } - } - - foreach (var node in NetworkRunner.Instance.NetworkScenes.Values) - { - // Finally, cleanup serializers - foreach (var serializer in node.Serializers) - { - serializer.Cleanup(); - } - } - - return peerBuffers; - } - - public void ImportState(HLBuffer stateBytes) - { - var affectedNodes = HLBytes.UnpackInt64(stateBytes); - var nodeIdToSerializerList = new Dictionary(); - for (byte i = 0; i < MAX_NETWORK_NODES; i++) - { - if ((affectedNodes & ((long)1 << i)) == 0) - { - continue; - } - var serializersRun = HLBytes.UnpackInt8(stateBytes); - nodeIdToSerializerList[i] = serializersRun; - } - - foreach (var nodeIdSerializerList in nodeIdToSerializerList) - { - var localNodeId = nodeIdSerializerList.Key; - NetworkRunner.Instance.NetworkScenes.TryGetValue(localNodeId, out NetworkNodeWrapper node); - if (node == null) { - var blankScene = new NetworkNode3D - { - NetworkId = localNodeId - }; - node = new NetworkNodeWrapper(blankScene); - } - for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) - { - if ((nodeIdSerializerList.Value & ((long)1 << serializerIdx)) == 0) - { - continue; - } - var serializerInstance = node.Serializers[serializerIdx]; - serializerInstance.Import(this, stateBytes, out NetworkNodeWrapper nodeOut); - if (node != nodeOut) - { - node = nodeOut; - serializerIdx = 0; - } - } - } - } - - public void PeerAcknowledge(NetPeer peer, Tick tick) - { - if (pendingSyncStates.TryGetValue(peer, out PendingSyncState pendingSyncState)) - { - if (pendingSyncState.tick <= tick) - { - PeerSyncState[peer] = pendingSyncState.state; - pendingSyncStates.Remove(peer); - } - } - foreach (var node in NetworkRunner.Instance.NetworkScenes.Values) - { - for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) - { - var serializer = node.Serializers[serializerIdx]; - serializer.Acknowledge(this, peer, tick); - } - } - } - public void ClientHandleTick(int incomingTick, byte[] stateBytes) - { - if (incomingTick <= NetworkRunner.Instance.CurrentTick) - { - return; - } - // GD.Print("INCOMING DATA: " + BitConverter.ToString(HLBytes.Decompress(stateBytes))); - NetworkRunner.Instance.CurrentTick = incomingTick; - ImportState(new HLBuffer(stateBytes)); - foreach (var net_id in NetworkRunner.Instance.NetworkScenes.Keys) - { - var node = NetworkRunner.Instance.NetworkScenes[net_id]; - if (node == null) - continue; - if (node.Node.IsQueuedForDeletion()) - { - NetworkRunner.Instance.NetworkScenes.Remove(net_id); - continue; - } - node._NetworkProcess(CurrentTick); - - foreach (var wrapper in node.StaticNetworkChildren) - { - if (wrapper == null || wrapper.Node.IsQueuedForDeletion()) - { - continue; - } - wrapper._NetworkProcess(CurrentTick); - } - } - HLBuffer buffer = new HLBuffer(); - HLBytes.Pack(buffer, incomingTick); - NetworkRunner.Instance.ENetHost.Send(1, buffer.bytes, (int)ENetPacketPeer.FlagUnsequenced); - } - } -} \ No newline at end of file diff --git a/addons/HLNC/NetworkProperty.cs b/addons/HLNC/NetworkProperty.cs index 7417970..4150cc9 100644 --- a/addons/HLNC/NetworkProperty.cs +++ b/addons/HLNC/NetworkProperty.cs @@ -1,26 +1,31 @@ using System; +using HLNC.Serialization; namespace HLNC { /// /// Mark a property as being Networked. - /// The automatically processes these through the to be optimally sent across the network. + /// The automatically processes these through the to be optimally sent across the network. /// Only changes are networked. /// When the client receives a change on the property, if a method exists OnNetworkChange{PropertyName}(int tick, T oldValue, T newValue) it will be called on the client side. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class NetworkProperty : Attribute { - public Flags flags; - public enum Flags + public enum SyncFlags { LinearInterpolation = 1 << 0, LossyConsistency = 1 << 1, - SyncOnInterest = 1 << 2, } - public NetworkProperty(Flags flags = 0) + + public SyncFlags Flags; + public long InterestMask; + public VariantSubtype Subtype; + public NetworkProperty(SyncFlags flags = 0, VariantSubtype subtype = VariantSubtype.None, long interestLayers = 1) { - this.flags = flags; + Flags = flags; + Subtype = subtype; + InterestMask = interestLayers; } } } \ No newline at end of file diff --git a/addons/HLNC/NetworkRunner.cs b/addons/HLNC/NetworkRunner.cs index 502d424..3e95d16 100644 --- a/addons/HLNC/NetworkRunner.cs +++ b/addons/HLNC/NetworkRunner.cs @@ -6,6 +6,7 @@ using Godot; using HLNC.Serialization; using System; +using System.Text; namespace HLNC { @@ -20,23 +21,45 @@ public partial class NetworkRunner : Node [Export] public string ServerAddress = "127.0.0.1"; /// - /// The port for the server to listen on, and the client to connect to. + /// The port for the server to listen on, and the client to connect to. If BlastoffClient is installed, this will be overridden to 20406, the Blastoff port. /// [Export] public int Port = 8888; /// /// The maximum number of allowed connections before the server starts rejecting clients. /// - [Export] public int MaxPeers = 5; + [Export] public int MaxPeers = 100; /// - /// The current Zone ID. This is mainly used for Blastoff. + /// The current World ID. This is mainly used for Blastoff. /// - public string ZoneInstanceId => arguments.ContainsKey("zoneInstanceId") ? arguments["zoneInstanceId"] : "0"; - + public Dictionary Worlds { get; private set; } = []; internal ENetConnection ENet; internal ENetPacketPeer ENetHost; + internal Dictionary Peers = []; + internal Dictionary PeerIds = []; + internal Dictionary> WorldPeerMap = []; + internal Dictionary PeerWorldMap = []; + + public NetPeer GetPeer(string id) + { + if (Peers.ContainsKey(id)) + { + return Peers[id]; + } + return null; + } + + public string GetPeerId(NetPeer peer) + { + if (PeerIds.ContainsKey(peer)) + { + return PeerIds[peer]; + } + return null; + } + /// /// This is set after or is called, i.e. when == true. Before that, this value is unreliable. /// @@ -46,14 +69,15 @@ public partial class NetworkRunner : Node /// This is set to true once or have succeeded. /// public bool NetStarted { get; private set; } - + internal IBlastoffServerDriver BlastoffServer { get; private set; } internal IBlastoffClientDriver BlastoffClient { get; private set; } /// /// These are commands which the server may send to Blastoff, which informs Blastoff how to act upon the client connection. /// - public enum BlastoffCommands { + public enum BlastoffCommands + { /// /// Requests Blastoff to create a new server instance, i.e. of the game. @@ -66,7 +90,7 @@ public enum BlastoffCommands { ValidateClient = 1, /// - /// Requests Blastoff to redirect the user to another zone Id. + /// Requests Blastoff to redirect the user to another world Id. /// RedirectClient = 2, @@ -76,12 +100,12 @@ public enum BlastoffCommands { InvalidClient = 3, } internal HashSet BlastoffPendingValidation = new HashSet(); - internal Guid ZoneId = Guid.Empty; - + /// /// Describes the channels of communication used by the network. /// - public enum ENetChannelId { + public enum ENetChannelId + { /// /// Tick data sent by the server to the client, and from the client indicating the most recent tick it has received. @@ -93,34 +117,18 @@ public enum ENetChannelId { /// Input = 2, - /// - /// Client data sent to the server to authenticate themselves and connect to a zone. - /// - ClientAuth = 3, - /// /// Server communication with Blastoff. Data sent to this channel from a client will be ignored by Blastoff. /// - BlastoffAdmin = 254, + BlastoffAdmin = 249, } - - /// - /// The currently active root network scene. This should only be set via or . - /// - public NetworkNodeWrapper CurrentScene = new NetworkNodeWrapper(null); - - internal int NetworkId_counter = 0; - internal System.Collections.Generic.Dictionary NetworkScenes = []; - private Godot.Collections.Dictionary>> inputStore = []; - public Godot.Collections.Dictionary>> InputStore => inputStore; - private static NetworkRunner _instance; /// /// The singleton instance. /// public static NetworkRunner Instance => _instance; - + /// public override void _EnterTree() { @@ -130,12 +138,12 @@ public override void _EnterTree() } _instance = this; } - internal void DebugPrint(string msg) + internal static void DebugPrint(string msg) { - GD.Print($"{(IsServer ? "Server" : "Client")}: {msg}"); + GD.Print($"{(OS.HasFeature("dedicated_server") ? "Server" : "Client")}: {msg}"); } - System.Collections.Generic.Dictionary arguments = []; + public System.Collections.Generic.Dictionary StartArgs = []; /// public override void _Ready() @@ -145,36 +153,15 @@ public override void _Ready() if (argument.Contains('=')) { var keyValuePair = argument.Split("="); - arguments[keyValuePair[0].TrimStart('-')] = keyValuePair[1]; + StartArgs[keyValuePair[0].TrimStart('-')] = keyValuePair[1]; } else { // Options without an argument will be present in the dictionary, // with the value set to an empty string. - arguments[argument.TrimStart('-')] = ""; - } - } - } - - public void RegisterSpawn(NetworkNodeWrapper wrapper) - { - if (IsServer) - { - NetworkId_counter += 1; - while (NetworkScenes.ContainsKey(NetworkId_counter)) - { - NetworkId_counter += 1; + StartArgs[argument.TrimStart('-')] = ""; } - NetworkScenes[NetworkId_counter] = wrapper; - wrapper.NetworkId = NetworkId_counter; - return; } - - if (!wrapper.DynamicSpawn) - { - wrapper.Node.QueueFree(); - } - } public void InstallBlastoffServerDriver(IBlastoffServerDriver blastoff) @@ -185,7 +172,7 @@ public void InstallBlastoffServerDriver(IBlastoffServerDriver blastoff) return; } BlastoffServer = blastoff; - GD.Print("Server: Blastoff Installed"); + DebugPrint("Blastoff Installed"); } public void InstallBlastoffClientDriver(IBlastoffClientDriver blastoff) @@ -196,7 +183,7 @@ public void InstallBlastoffClientDriver(IBlastoffClientDriver blastoff) return; } BlastoffClient = blastoff; - GD.Print("Client: Blastoff Installed"); + DebugPrint("Blastoff Installed"); } public void StartServer() @@ -205,15 +192,10 @@ public void StartServer() DebugPrint("Starting Server"); GetTree().MultiplayerPoll = false; var custom_port = Port; - if (arguments.ContainsKey("port")) + if (StartArgs.ContainsKey("port")) { - GD.Print("PORT OVERRIDE: {0}", arguments["port"]); - custom_port = int.Parse(arguments["port"]); - } - - if (arguments.ContainsKey("zoneId")) { - ZoneId = new Guid(arguments["zoneId"]); - DebugPrint($"Zone ID: {ZoneId}"); + DebugPrint("PORT OVERRIDE: " + StartArgs["port"]); + custom_port = int.Parse(StartArgs["port"]); } ENet = new ENetConnection(); @@ -225,30 +207,24 @@ public void StartServer() return; } NetStarted = true; - DebugPrint("Started"); + DebugPrint($"Started on {ServerAddress}:{custom_port}"); } public void StartClient() { ENet = new ENetConnection(); ENet.CreateHost(); - ENetHost = ENet.ConnectToHost(ServerAddress, 8888); + ENetHost = ENet.ConnectToHost(ServerAddress, BlastoffClient != null ? 20406 : Port); ENet.Compress(ENetConnection.CompressionMode.RangeCoder); - // ENetHost = ENet.ConnectToHost(ServerAddress, 20406); if (ENetHost == null) { DebugPrint($"Error connecting."); return; } NetStarted = true; - if (BlastoffClient != null) { - var zoneBytes = BlastoffClient.BlastoffGetZoneId().ToByteArray(); - var tokenBytes = System.Text.Encoding.UTF8.GetBytes(BlastoffClient.BlastoffGetToken()); - var combinedBytes = new byte[zoneBytes.Length + tokenBytes.Length]; - zoneBytes.CopyTo(combinedBytes, 0); - tokenBytes.CopyTo(combinedBytes, zoneBytes.Length); - ENetHost.Send((int)ENetChannelId.ClientAuth, combinedBytes, (int)ENetPacketPeer.FlagReliable); - } + var worldRunner = new WorldRunner(); + WorldRunner.CurrentWorld = worldRunner; + GetTree().CurrentScene.AddChild(worldRunner); DebugPrint("Started"); } @@ -268,61 +244,6 @@ public void StartClient() /// public const int MTU = 1400; - /// - /// The current network tick. On the client side, this does not represent the server's current tick, which will always be slightly ahead. - /// - public int CurrentTick { get; internal set; } = 0; - - [Signal] - public delegate void OnAfterNetworkTickEventHandler(Tick tick); - - - private int _frameCounter = 0; - /// - /// This method is executed every tick on the Server side, and kicks off all logic which processes and sends data to every client. - /// - public void ServerProcessTick() - { - - foreach (var net_id in NetworkScenes.Keys) - { - var networkNode = NetworkScenes[net_id]; - if (networkNode == null) - continue; - - if (!IsInstanceValid(networkNode.Node) || networkNode.Node.IsQueuedForDeletion()) - { - NetworkScenes.Remove(net_id); - continue; - } - networkNode._NetworkProcess(CurrentTick); - foreach (var networkChild in networkNode.StaticNetworkChildren) - { - networkChild._NetworkProcess(CurrentTick); - } - } - - var exportedState = NetworkPeerManager.Instance.ExportState(ENet.GetPeers()); - foreach (var peer in ENet.GetPeers()) - { - var size = exportedState[peer].bytes.Length; - // if (network_debug != null) - // { - // debug_data_sizes.Add(compressed_payload.Length); - // } - if (size > MTU) - { - DebugPrint($"Warning: Data size {size} exceeds MTU {MTU}"); - } - - var buffer = new HLBuffer(); - HLBytes.Pack(buffer, CurrentTick); - HLBytes.Pack(buffer, exportedState[peer].bytes, true); - - peer.Send(1, buffer.bytes, (int)ENetPacketPeer.FlagUnsequenced); - } - } - /// public override void _PhysicsProcess(double delta) @@ -330,10 +251,9 @@ public override void _PhysicsProcess(double delta) if (!NetStarted) return; - var enetEvent = ENet.Service(); - while (true) { + var enetEvent = ENet.Service(); var eventType = enetEvent[0].As(); if (eventType == ENetConnection.EventType.None) { @@ -343,7 +263,14 @@ public override void _PhysicsProcess(double delta) switch (eventType) { case ENetConnection.EventType.Connect: - _OnPeerConnected(packetPeer); + if (packetPeer == ENetHost) + { + _OnConnectedToServer(); + } + else + { + _OnPeerConnected(packetPeer); + } break; case ENetConnection.EventType.Disconnect: _OnPeerDisconnected(packetPeer); @@ -351,149 +278,159 @@ public override void _PhysicsProcess(double delta) case ENetConnection.EventType.Receive: var data = new HLBuffer(packetPeer.GetPacket()); var channel = enetEvent[3].As(); - switch ((ENetChannelId)channel) { + switch ((ENetChannelId)channel) + { case ENetChannelId.Tick: - if (IsServer) { + if (IsServer) + { var tick = HLBytes.UnpackInt32(data); - NetworkPeerManager.Instance.PeerAcknowledge(packetPeer, tick); - } else { + PeerWorldMap[packetPeer].PeerAcknowledge(packetPeer, tick); + } + else + { if (data.bytes.Length == 0) { break; } var tick = HLBytes.UnpackInt32(data); var bytes = HLBytes.UnpackByteArray(data); - NetworkPeerManager.Instance.ClientHandleTick(tick, bytes); + // GD.Print(BitConverter.ToString(bytes)); + WorldRunner.CurrentWorld.ClientHandleTick(tick, bytes); + } + break; + case ENetChannelId.Input: + if (IsServer) + { + PeerWorldMap[packetPeer].ReceiveInput(packetPeer, data); + } + else + { + // Clients should never receive messages on the Input channel + break; } - break; + break; case ENetChannelId.BlastoffAdmin: - if (IsServer) { - if (BlastoffServer == null) { + if (IsServer) + { + if (BlastoffServer == null) + { // This channel is only used for Blastoff which must be enabled. break; } - if (BlastoffPendingValidation.Contains(packetPeer)) { + if (BlastoffPendingValidation.Contains(packetPeer)) + { // We're in the process of validating a peer for Blastoff. - // The packet is two parts: a zone UUID, and a token - var zoneId = new Guid(data.bytes[0..32]); - var token = System.Text.Encoding.UTF8.GetString(data.bytes[32..]); - if (BlastoffServer.BlastoffValidatePeer(zoneId, token, out var redirect)) { - _validatePeerConnected(packetPeer); + var token = System.Text.Encoding.UTF8.GetString(data.bytes); + if (BlastoffServer.BlastoffValidatePeer(token, out var worldId)) + { + _validatePeerConnected(packetPeer, worldId, token); packetPeer.Send((int)ENetChannelId.BlastoffAdmin, [(byte)BlastoffCommands.ValidateClient], (int)ENetPacketPeer.FlagReliable); - } else { - // TODO: If redirect is not Guid.Empty or null, we should redirect the client to that zone + } + else + { packetPeer.Send((int)ENetChannelId.BlastoffAdmin, [(byte)BlastoffCommands.InvalidClient], (int)ENetPacketPeer.FlagReliable); } } - } else { + } + else + { // Clients should never receive messages on the Blastoff channel break; } - break; - + break; + } break; } - enetEvent = ENet.Service(); - } - - if (IsServer) - { - _frameCounter += 1; - if (_frameCounter < PhysicsTicksPerNetworkTick) - return; - _frameCounter = 0; - CurrentTick += 1; - ServerProcessTick(); - EmitSignal("OnAfterNetworkTick", CurrentTick); } } - [Signal] - public delegate void PlayerConnectedEventHandler(); + internal void _validatePeerConnected(NetPeer peer, Guid worldId, string token = "") + { + var peerId = Guid.NewGuid().ToString(); + Peers[peerId] = peer; + PeerIds[peer] = peerId; - internal void _validatePeerConnected(NetPeer peer) { foreach (var node in GetTree().GetNodesInGroup("global_interest")) { - if (node is NetworkNode3D networkNode) - networkNode.Interest[peer] = true; + var wrapper = new NetworkNodeWrapper(node); + if (wrapper == null) continue; + wrapper.SetPeerInterest(peerId, Int64.MaxValue, false); } - NetworkPeerManager.Instance.RegisterPlayer(peer); - - EmitSignal("PlayerConnected", peer); + Worlds[worldId].JoinPeer(peer, token); + BlastoffPendingValidation.Remove(peer); } - public void _OnPeerConnected(NetPeer peer) + private void StartBlastoffNegotiation() { - DebugPrint($"Peer {peer} joined"); - - if (!IsServer) + // We build the UUID as a string because of endian issues... or something + // This is related to UUID Representation in C# + var tokenBytes = System.Text.Encoding.UTF8.GetBytes(BlastoffClient.BlastoffGetToken()); + var err = ENetHost.Send((int)ENetChannelId.BlastoffAdmin, tokenBytes, (int)ENetPacketPeer.FlagReliable); + if (err != Error.Ok) { - return; - } - - if (BlastoffServer != null) { - BlastoffPendingValidation.Add(peer); - } else { - _validatePeerConnected(peer); + DebugPrint($"Error sending Blastoff data: {err}"); } } - public void ChangeScenePacked(PackedScene scene) - { - // This allows us to change scenes without using Godot's built-in scene changer - // We do this because Godot's scene changer doesn't work well with networked scenes - if (!IsServer) return; - var node = new NetworkNodeWrapper((NetworkNode3D)scene.Instantiate()); - ChangeSceneInstance(node); - } - - public void ChangeSceneInstance(NetworkNodeWrapper node) + private void _OnConnectedToServer() { - if (!IsServer) return; - if (CurrentScene.Node != null) { - CurrentScene.Node.QueueFree(); + DebugPrint("Connected to server"); + if (BlastoffClient != null) + { + StartBlastoffNegotiation(); } - node.DynamicSpawn = true; - // TODO: Support this more generally - GetTree().CurrentScene.AddChild(node.Node); - CurrentScene = node; - var networkChildren = (node.Node as NetworkNode3D).GetNetworkChildren(NetworkNode3D.NetworkChildrenSearchToggle.INCLUDE_SCENES).ToList(); - networkChildren.Reverse(); - networkChildren.ForEach(child => child._NetworkPrepare()); - node._NetworkPrepare(); - } - public void ChangeZone() { - if (IsServer) return; - // var node = new NetworkNodeWrapper(new NetworkNode3D()); - // ChangeSceneInstance(node); - } - - public void Spawn(NetworkNode3D node, NetworkNode3D parent = null, string nodePath = ".") + private void _OnPeerConnected(NetPeer peer) { - if (!IsServer) return; - - node.DynamicSpawn = true; - node.NetworkParent = new NetworkNodeWrapper(null); - if (parent == null) + DebugPrint($"Peer {peer} joined"); + if (BlastoffServer != null) { - CurrentScene.Node.GetNode(nodePath).AddChild(node); + BlastoffPendingValidation.Add(peer); } else { - parent.GetNode(nodePath).AddChild(node); + // TODO: Don't use GUID Empty + _validatePeerConnected(peer, Guid.Empty); } } - public NetworkNodeWrapper GetFromNetworkId(NetworkId network_id) + [Signal] + public delegate void OnWorldCreatedEventHandler(WorldRunner world); + + public WorldRunner CreateWorldPacked(Guid worldId, PackedScene scene) + { + if (!IsServer) return null; + var node = new NetworkNodeWrapper((NetworkNode3D)scene.Instantiate()); + return CreateWorldInstance(worldId, node); + } + + public WorldRunner CreateWorldInstance(Guid worldId, NetworkNodeWrapper node) { - if (network_id == -1) - return new NetworkNodeWrapper(null); - if (!NetworkScenes.ContainsKey(network_id)) - return new NetworkNodeWrapper(null); - return NetworkScenes[network_id]; + if (!IsServer) return null; + node.DynamicSpawn = true; + var godotPhysicsWorld = new SubViewport + { + + OwnWorld3D = true, + World3D = new World3D(), + Name = worldId.ToString() + }; + var worldRunner = new WorldRunner + { + WorldId = worldId, + RootScene = node, + }; + Worlds[worldId] = worldRunner; + WorldPeerMap[worldId] = []; + + godotPhysicsWorld.AddChild(worldRunner); + godotPhysicsWorld.AddChild(node.Node); + GetTree().CurrentScene.AddChild(godotPhysicsWorld); + node._NetworkPrepare(worldRunner); + EmitSignal("OnWorldCreated", worldRunner); + return worldRunner; } public void _OnPeerDisconnected(ENetPacketPeer peer) @@ -518,32 +455,5 @@ public IEnumerable GetAllNetworkNodes(Node node, bool onlyScenes } } } - public void TransferInput(int tick, byte networkId, Godot.Collections.Dictionary incomingInput) - { - // var sender = MultiplayerInstance.GetRemoteSenderId(); - // var node = NetworkPeerManager.Instance.GetPeerNode(sender, networkId); - - // if (node == null) - // { - // return; - // } - - // if (sender != node.InputAuthority) - // { - // return; - // } - - // if (!inputStore.ContainsKey(sender)) - // { - // inputStore[sender] = []; - // } - - // if (!inputStore[sender].ContainsKey(networkId)) - // { - // inputStore[sender][networkId] = []; - // } - - // inputStore[sender][networkId] = incomingInput; - } } } \ No newline at end of file diff --git a/addons/HLNC/Serialization/HLBytes.cs b/addons/HLNC/Serialization/HLBytes.cs index 70213a5..f312059 100644 --- a/addons/HLNC/Serialization/HLBytes.cs +++ b/addons/HLNC/Serialization/HLBytes.cs @@ -17,7 +17,7 @@ public static void AddRange(ref byte[] array, byte[] toAdd) Array.Resize(ref array, array.Length + toAdd.Length); Array.Copy(toAdd, 0, array, array.Length - toAdd.Length, toAdd.Length); } - public static void PackVariant(HLBuffer buffer, Variant varVal, bool packType = false) + public static void PackVariant(HLBuffer buffer, Variant varVal, bool packLength = false, bool packType = false) { if (varVal.VariantType == Variant.Type.Vector3) { @@ -47,6 +47,13 @@ public static void PackVariant(HLBuffer buffer, Variant varVal, bool packType = { PackArray(buffer, (Godot.Collections.Array)varVal, packType); } + else if (varVal.VariantType == Variant.Type.PackedByteArray) + { + Pack(buffer, (byte[])varVal, packLength, packType); + } else if (varVal.VariantType == Variant.Type.String) + { + Pack(buffer, (string)varVal, packType); + } else { GD.Print("HLBytes.Pack: Unhandled type: " + varVal.VariantType); @@ -264,7 +271,21 @@ public static void Pack(HLBuffer buffer, byte[] varVal, bool packLength = false, buffer.pointer += varVal.Length; } - public static Variant? UnpackVariant(HLBuffer buffer, Variant.Type? knownType = null) + public static void Pack(HLBuffer buffer, string varVal, bool packType = false) + { + if (packType) + { + Array.Resize(ref buffer.bytes, buffer.bytes.Length + 1); + buffer.bytes[buffer.pointer] = (byte)Variant.Type.String; + buffer.pointer += 1; + } + Pack(buffer, varVal.Length); + Array.Resize(ref buffer.bytes, buffer.bytes.Length + varVal.Length); + Array.Copy(System.Text.Encoding.UTF8.GetBytes(varVal), 0, buffer.bytes, buffer.pointer, varVal.Length); + buffer.pointer += varVal.Length; + } + + public static Variant? UnpackVariant(HLBuffer buffer, int length = 0, Variant.Type? knownType = null) { Variant.Type type; if (knownType.HasValue) @@ -306,7 +327,11 @@ public static void Pack(HLBuffer buffer, byte[] varVal, bool packLength = false, } else if (type == Variant.Type.PackedByteArray) { - return UnpackByteArray(buffer); + return UnpackByteArray(buffer, length); + } + else if (type == Variant.Type.String) + { + return UnpackString(buffer); } else { @@ -365,7 +390,11 @@ public static float UnpackFloat(HLBuffer buffer) return BitConverter.ToSingle(bytes, 0); } - // Alias for UnpackInt8 + /// + /// Alias for + /// + /// + /// public static byte UnpackByte(HLBuffer buffer) { return UnpackInt8(buffer); @@ -437,14 +466,25 @@ public static int[] UnpackInt32Array(HLBuffer buffer) return result; } - public static byte[] UnpackByteArray(HLBuffer buffer) + public static byte[] UnpackByteArray(HLBuffer buffer, int length = 0) { - var size = BitConverter.ToInt32(buffer.bytes, buffer.pointer); - buffer.pointer += 4; + var size = length; + if (size == 0) + { + size = UnpackInt32(buffer); + } var result = new byte[size]; Array.Copy(buffer.bytes, buffer.pointer, result, 0, size); buffer.pointer += size; return result; } + + public static string UnpackString(HLBuffer buffer) + { + var size = UnpackInt32(buffer); + var result = System.Text.Encoding.UTF8.GetString(buffer.bytes, buffer.pointer, size); + buffer.pointer += size; + return result; + } } } \ No newline at end of file diff --git a/addons/HLNC/Serialization/NetworkScenesRegister.cs b/addons/HLNC/Serialization/NetworkScenesRegister.cs index 1e409f3..82310a5 100644 --- a/addons/HLNC/Serialization/NetworkScenesRegister.cs +++ b/addons/HLNC/Serialization/NetworkScenesRegister.cs @@ -7,12 +7,21 @@ namespace HLNC.Serialization { + public enum VariantSubtype { + None, + Guid, + Byte, + Int, + + } internal struct CollectedNetworkProperty { public string NodePath; public string Name; public Variant.Type Type; public byte Index; + public VariantSubtype Subtype; + public long InterestMask; } public partial class NetworkScenesRegister : Node { @@ -51,7 +60,7 @@ public partial class NetworkScenesRegister : Node internal delegate void LoadCompleteEventHandler(); // public static GetPropertyById - public NetworkScenesRegister() + public override void _EnterTree() { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { @@ -126,6 +135,8 @@ public NetworkScenesRegister() continue; } + var subType = (attr as NetworkProperty).Subtype; + propertyId += 1; if (propertyId >= MAX_NETWORK_PROPERTIES) { @@ -133,9 +144,17 @@ public NetworkScenesRegister() return; } Variant.Type propType = Variant.Type.Nil; - if (property.PropertyType == typeof(int)) + if (property.PropertyType == typeof(long) || property.PropertyType == typeof(int) || property.PropertyType == typeof(byte)) { propType = Variant.Type.Int; + if (property.PropertyType == typeof(byte)) + { + subType = VariantSubtype.Byte; + } + else if (property.PropertyType == typeof(int)) + { + subType = VariantSubtype.Int; + } } else if (property.PropertyType == typeof(float)) { @@ -156,6 +175,9 @@ public NetworkScenesRegister() else if (property.PropertyType == typeof(bool)) { propType = Variant.Type.Bool; + } else if (property.PropertyType == typeof(byte[])) + { + propType = Variant.Type.PackedByteArray; } else { @@ -169,6 +191,8 @@ public NetworkScenesRegister() Name = property.Name, Type = propType, Index = (byte)propertyId, + Subtype = subType, + InterestMask = (attr as NetworkProperty).InterestMask, }; PROPERTIES_MAP[scenePath].TryAdd(relativeChildPath, []); PROPERTIES_MAP[scenePath][relativeChildPath].TryAdd(property.Name, collectedProperty); diff --git a/addons/HLNC/Serialization/Serializers/DespawnSerializer.cs b/addons/HLNC/Serialization/Serializers/DespawnSerializer.cs index 323b18c..9091f8f 100644 --- a/addons/HLNC/Serialization/Serializers/DespawnSerializer.cs +++ b/addons/HLNC/Serialization/Serializers/DespawnSerializer.cs @@ -18,7 +18,7 @@ private Data Deserialize(HLBuffer data) return new Data(); } - public void Import(IPeerStateController peerStateController, HLBuffer buffer, out NetworkNodeWrapper nodeOut) + public void Import(WorldRunner currentWorld, HLBuffer buffer, out NetworkNodeWrapper nodeOut) { nodeOut = node; var data = Deserialize(buffer); @@ -27,7 +27,7 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou public void Cleanup() { } - public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) + public HLBuffer Export(WorldRunner currentWorld, NetPeer peerId) { var buffer = new HLBuffer(); // Dictionary despawnsBuffer = new Dictionary(); @@ -55,7 +55,7 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) return buffer; } - public void Acknowledge(IPeerStateController peerStateController, NetPeer peer, Tick tick) + public void Acknowledge(WorldRunner currentWorld, NetPeer peer, Tick tick) { // if (!DespawnBuffers.ContainsKey(peer)) // return; diff --git a/addons/HLNC/Serialization/Serializers/IStateSerializable.cs b/addons/HLNC/Serialization/Serializers/IStateSerializable.cs index 7c2ac9a..e9817b7 100644 --- a/addons/HLNC/Serialization/Serializers/IStateSerializable.cs +++ b/addons/HLNC/Serialization/Serializers/IStateSerializable.cs @@ -18,7 +18,7 @@ public interface IStateSerailizer /// /// /// - public void Import(IPeerStateController networkState, HLBuffer data, out NetworkNodeWrapper nodeOut); + public void Import(WorldRunner currentWorld, HLBuffer data, out NetworkNodeWrapper nodeOut); /// /// Server-side only. Serialize and send data to the client. @@ -26,8 +26,8 @@ public interface IStateSerailizer /// /// /// - public HLBuffer Export(IPeerStateController networkState, NetPeer peer); - public void Acknowledge(IPeerStateController networkState, NetPeer peer, Tick tick); + public HLBuffer Export(WorldRunner currentWorld, NetPeer peer); + public void Acknowledge(WorldRunner currentWorld, NetPeer peer, Tick tick); public void PhysicsProcess(double delta); public void Cleanup(); } diff --git a/addons/HLNC/Serialization/Serializers/NetworkPropertiesSerializer.cs b/addons/HLNC/Serialization/Serializers/NetworkPropertiesSerializer.cs index bdc70cf..5a21474 100644 --- a/addons/HLNC/Serialization/Serializers/NetworkPropertiesSerializer.cs +++ b/addons/HLNC/Serialization/Serializers/NetworkPropertiesSerializer.cs @@ -5,7 +5,7 @@ namespace HLNC.Serialization.Serializers { - internal class NetworkPropertiesSerializer : IStateSerailizer + internal partial class NetworkPropertiesSerializer : Node, IStateSerailizer { private struct Data { @@ -27,9 +27,12 @@ public struct LerpableChangeQueue private Dictionary lerpableChangeQueue = []; private Dictionary propertyUpdated = []; - public NetworkPropertiesSerializer(NetworkNodeWrapper wrapper) + + private Dictionary peerInitialPropSync = []; + public override void _EnterTree() { - this.wrapper = wrapper; + wrapper = new NetworkNodeWrapper(GetParent()); + Name = "NetworkPropertiesSerializer"; // First, determine if the Node class has the NetworkScene attribute // This is because only a network scene will serialize network node properties recursively @@ -38,36 +41,45 @@ public NetworkPropertiesSerializer(NetworkNodeWrapper wrapper) return; } - wrapper.Node.Ready += () => + if (NetworkRunner.Instance.IsServer) { - if (NetworkRunner.Instance.IsServer) + wrapper.Node.Connect("NetworkPropertyChanged", Callable.From((string nodePath, string propertyName) => { - wrapper.Node.Connect("NetworkPropertyChanged", Callable.From((string nodePath, string propertyName) => + if (!NetworkScenesRegister.PROPERTIES_MAP.TryGetValue(wrapper.Node.SceneFilePath, out var _prop)) { - if (!NetworkScenesRegister.PROPERTIES_MAP.TryGetValue(wrapper.Node.SceneFilePath, out var _prop)) { - GD.Print("Failed to load ", wrapper.Node.SceneFilePath); - return; - } - if (!NetworkScenesRegister.PROPERTIES_MAP[wrapper.Node.SceneFilePath].TryGetValue(nodePath, out var _prop2)) { - GD.Print("Failed to process ", wrapper.Node.SceneFilePath, "/", nodePath, "/", propertyName); - return; - } - if (NetworkScenesRegister.PROPERTIES_MAP[wrapper.Node.SceneFilePath][nodePath].TryGetValue(propertyName, out var property)) - { - propertyUpdated[property.Index] = true; - } - })); - } - else + GD.Print("Failed to load ", wrapper.Node.SceneFilePath); + return; + } + if (!NetworkScenesRegister.PROPERTIES_MAP[wrapper.Node.SceneFilePath].TryGetValue(nodePath, out var _prop2)) + { + GD.Print("Failed to process ", wrapper.Node.SceneFilePath, "/", nodePath, "/", propertyName); + return; + } + if (NetworkScenesRegister.PROPERTIES_MAP[wrapper.Node.SceneFilePath][nodePath].TryGetValue(propertyName, out var property)) + { + propertyUpdated[property.Index] = true; + nonDefaultProperties.Add(property.Index); + } + })); + + wrapper.Node.Connect("InterestChanged", Callable.From((string peerId, long interest) => { - // As a client, apply all the cached changes - foreach (var propIndex in cachedPropertyChanges.Keys) + var peer = NetworkRunner.Instance.GetPeer(peerId); + if (!peerInitialPropSync.ContainsKey(peer)) { - var prop = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][propIndex]; - ImportProperty(prop, NetworkRunner.Instance.CurrentTick, cachedPropertyChanges[propIndex]); + peerInitialPropSync.Remove(peer); } + })); + } + else + { + // As a client, apply all the cached changes + foreach (var propIndex in cachedPropertyChanges.Keys) + { + var prop = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][propIndex]; + ImportProperty(prop, wrapper.CurrentWorld.CurrentTick, cachedPropertyChanges[propIndex]); } - }; + } } @@ -75,6 +87,10 @@ public void ImportProperty(CollectedNetworkProperty prop, Tick tick, Variant val { var propNode = wrapper.Node.GetNode(prop.NodePath); Variant oldVal = propNode.Get(prop.Name); + if (oldVal.Equals(value)) + { + return; + } var friendlyPropName = prop.Name; if (friendlyPropName.StartsWith("network_")) { @@ -89,6 +105,12 @@ public void ImportProperty(CollectedNetworkProperty prop, Tick tick, Variant val propNode.Call("_on_network_change_" + friendlyPropName, tick, oldVal, value); } +#if DEBUG + var netProps = GetMeta("NETWORK_PROPS", new Godot.Collections.Dictionary()).AsGodotDictionary(); + netProps[prop.NodePath + ":" + prop.Name] = value; + SetMeta("NETWORK_PROPS", netProps); +#endif + lerpableChangeQueue[prop.NodePath + ":" + prop.Name] = new LerpableChangeQueue { Prop = prop, @@ -113,23 +135,23 @@ private Data Deserialize(HLBuffer buffer) continue; } var prop = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][(byte)i]; - var varVal = HLBytes.UnpackVariant(buffer, prop.Type); + var varVal = HLBytes.UnpackVariant(buffer, knownType: prop.Type); data.properties[(byte)i] = varVal.Value; } return data; } - public void Import(IPeerStateController peerStateController, HLBuffer buffer, out NetworkNodeWrapper nodeOut) + public void Import(WorldRunner currentWorld, HLBuffer buffer, out NetworkNodeWrapper nodeOut) { nodeOut = wrapper; - + var data = Deserialize(buffer); foreach (var propIndex in data.properties.Keys) { var prop = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][propIndex]; if (wrapper.Node.IsNodeReady()) { - ImportProperty(prop, peerStateController.CurrentTick, data.properties[propIndex]); + ImportProperty(prop, currentWorld.CurrentTick, data.properties[propIndex]); } else { @@ -140,15 +162,47 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou return; } + private HashSet nonDefaultProperties = []; + private Dictionary> peerBufferCache = []; // This should instead be a map of variable values that we can resend until acknowledgement - public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) + private long FilterPropsAgainstInterest(NetPeer peer, long prop) { - var buffer = new HLBuffer(); + var result = prop; + var peerId = NetworkRunner.Instance.GetPeerId(peer); + if (!wrapper.InterestLayers.ContainsKey(peerId) || wrapper.InterestLayers[peerId] == 0) + { + GD.Print("No interest layers found for " + peerId + " on " + wrapper.Node.Name); + return 0; + } + for (var i = 0; i < MAX_NETWORK_PROPERTIES; i++) + { + if ((prop & (long)1 << i) == 0) + { + continue; + } - if (!peerStateController.HasSpawnedForClient(wrapper.NetworkId, peerId)) + var propNode = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][(byte)i]; + if ((propNode.InterestMask & (long)wrapper.InterestLayers[peerId]) == 0) + { + result &= ~(long)1 << i; + GD.Print("Filtering ", propNode.Name, " from ", wrapper.Node.Name, " for ", peerId); + } + } + return result; + } + + public HLBuffer Export(WorldRunner currentWorld, NetPeer peerId) + { + var buffer = new HLBuffer(); + if (!peerInitialPropSync.ContainsKey(peerId)) + { + peerInitialPropSync[peerId] = 0; + } + if (!currentWorld.HasSpawnedForClient(wrapper.NetworkId, peerId)) { + // GD.Print("Client ", peerId, " has not spawned ", wrapper.Node.Name); // The target client is not aware of this node yet. Don't send updates. return buffer; } @@ -164,13 +218,23 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) foreach (var propIndex in propertyUpdated.Keys) { propertiesUpdated |= (long)1 << propIndex; - propertyUpdated[propIndex] = false; + } + + foreach (var propIndex in nonDefaultProperties) + { + var propSlot = (long)1 << propIndex; + if ((peerInitialPropSync[peerId] & propSlot) == 0) + { + + propertiesUpdated |= propSlot; + peerInitialPropSync[peerId] |= propSlot; + } } // Store them in the cache to resend in the future until the client acknowledges having received the update if (propertiesUpdated != 0) { - peerBufferCache[peerId][peerStateController.CurrentTick] = propertiesUpdated; + peerBufferCache[peerId][currentWorld.CurrentTick] = propertiesUpdated; } propertiesUpdated = 0; @@ -178,11 +242,15 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) // Now collect every pending property update foreach (var tick in peerBufferCache[peerId].Keys.OrderBy(x => x)) { - propertiesUpdated |= peerBufferCache[peerId][tick]; + var filteredProps = FilterPropsAgainstInterest(peerId, peerBufferCache[peerId][tick]); + propertiesUpdated |= filteredProps; + } if (propertiesUpdated == 0) + { return buffer; + } // Indicate which variables have been updated HLBytes.Pack(buffer, propertiesUpdated); @@ -198,7 +266,7 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) var prop = NetworkScenesRegister.PROPERTY_LOOKUP[wrapper.Node.SceneFilePath][(byte)i]; var propNode = wrapper.Node.GetNode(prop.NodePath); var varVal = propNode.Get(prop.Name); - HLBytes.PackVariant(buffer, varVal); + HLBytes.PackVariant(buffer, varVal, true); } return buffer; @@ -209,7 +277,7 @@ public void Cleanup() propertyUpdated.Clear(); } - public void Acknowledge(IPeerStateController peerStateController, NetPeer peerId, Tick latestAck) + public void Acknowledge(WorldRunner currentWorld, NetPeer peerId, Tick latestAck) { if (!peerBufferCache.ContainsKey(peerId)) { @@ -231,6 +299,10 @@ private static Vector3 Lerp(Vector3 First, Vector3 Second, float Amount) public void PhysicsProcess(double delta) { + if (NetworkRunner.Instance.IsServer) + { + return; + } foreach (var queueKey in lerpableChangeQueue.Keys.ToList()) { diff --git a/addons/HLNC/Serialization/Serializers/SpawnSerializer.cs b/addons/HLNC/Serialization/Serializers/SpawnSerializer.cs index f1f2463..900d89e 100644 --- a/addons/HLNC/Serialization/Serializers/SpawnSerializer.cs +++ b/addons/HLNC/Serialization/Serializers/SpawnSerializer.cs @@ -4,7 +4,7 @@ namespace HLNC.Serialization.Serializers { - internal class SpawnSerializer(NetworkNodeWrapper wrapper) : IStateSerailizer + internal partial class SpawnSerializer : Node, IStateSerailizer { private struct Data { @@ -16,7 +16,14 @@ private struct Data public byte hasInputAuthority; } - private NetworkNodeWrapper wrapper = wrapper; + private NetworkNodeWrapper wrapper; + + public override void _EnterTree() + { + base._EnterTree(); + Name = "SpawnSerializer"; + wrapper = new NetworkNodeWrapper(GetParent()); + } private Dictionary setupTicks = []; private Data Deserialize(HLBuffer data) @@ -37,13 +44,13 @@ private Data Deserialize(HLBuffer data) return spawnData; } - public void Import(IPeerStateController peerStateController, HLBuffer buffer, out NetworkNodeWrapper nodeOut) + public void Import(WorldRunner currentWorld, HLBuffer buffer, out NetworkNodeWrapper nodeOut) { nodeOut = wrapper; var data = Deserialize(buffer); // If the node is already registered, then we don't need to spawn it again - var result = peerStateController.TryRegisterPeerNode(nodeOut); + var result = currentWorld.TryRegisterPeerNode(nodeOut); if (result == 0) { return; @@ -52,10 +59,10 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou var networkId = wrapper.NetworkId; // Deregister and delete the node, because it is simply a "Placeholder" that doesn't really exist - peerStateController.DeregisterPeerNode(nodeOut); + currentWorld.DeregisterPeerNode(nodeOut); wrapper.Node.QueueFree(); - var networkParent = peerStateController.GetNetworkNode(data.parentId); + var networkParent = currentWorld.GetNetworkNode(data.parentId); if (data.parentId != 0 && networkParent == null) { // The parent node is not registered, so we can't spawn this node @@ -63,22 +70,23 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou return; } - var newNode = NetworkScenesRegister.SCENES_MAP[data.classId].Instantiate(); - nodeOut = new NetworkNodeWrapper(newNode) - { - NetworkId = networkId, - DynamicSpawn = true - }; + NetworkRunner.Instance.RemoveChild(nodeOut.Node); + var newNode = NetworkScenesRegister.SCENES_MAP[data.classId].Instantiate(); + newNode.DynamicSpawn = true; + newNode.NetworkId = networkId; + newNode.CurrentWorld = currentWorld; + newNode.SetupSerializers(); + NetworkRunner.Instance.AddChild(newNode); + nodeOut = new NetworkNodeWrapper(newNode); if (networkParent != null) { nodeOut.NetworkParentId = networkParent.NetworkId; } - peerStateController.TryRegisterPeerNode(nodeOut); + currentWorld.TryRegisterPeerNode(nodeOut); // Iterate through all child nodes of nodeOut // If the child node is a NetworkNodeWrapper, then we set it to dynamic spawn // We don't register it as a node in the NetworkRunner because only the parent needs registration - NetworkRunner.Instance.AddChild(nodeOut.Node); var children = nodeOut.Node.GetChildren().ToList(); List networkChildren = new List(); while (children.Count > 0) @@ -106,7 +114,7 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou if (data.parentId == 0) { - peerStateController.ChangeScene(nodeOut); + currentWorld.ChangeScene(nodeOut); return; } @@ -123,38 +131,34 @@ public void Import(IPeerStateController peerStateController, HLBuffer buffer, ou } GD.Print("Spawned", nodeOut.Node.GetPath()); - foreach (var child in networkChildren) - { - child._NetworkPrepare(); - } - nodeOut._NetworkPrepare(); + nodeOut._NetworkPrepare(currentWorld); return; } - public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) + public HLBuffer Export(WorldRunner currentWorld, NetPeer peerId) { var buffer = new HLBuffer(); - if (peerStateController.HasSpawnedForClient(wrapper.NetworkId, peerId)) + if (currentWorld.HasSpawnedForClient(wrapper.NetworkId, peerId)) { // The target client is already aware of this node. return buffer; } - if (wrapper.NetworkParent != null && !peerStateController.HasSpawnedForClient(wrapper.NetworkParent.NetworkId, peerId)) + if (wrapper.NetworkParent != null && !currentWorld.HasSpawnedForClient(wrapper.NetworkParent.NetworkId, peerId)) { // The parent node is not registered with the client yet, so we can't spawn this node return buffer; } - var id = peerStateController.TryRegisterPeerNode(wrapper, peerId); + var id = currentWorld.TryRegisterPeerNode(wrapper, peerId); if (id == 0) { // Unable to spawn this node. The client is already tracking the max amount. return buffer; } - setupTicks[peerId] = peerStateController.CurrentTick; + setupTicks[peerId] = currentWorld.CurrentTick; HLBytes.Pack(buffer, wrapper.NetworkSceneId); // Pack the node path @@ -169,7 +173,7 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) } // Pack the parent network ID and the node path - var parentId = peerStateController.GetPeerNodeId(peerId, wrapper.NetworkParent); + var parentId = currentWorld.GetPeerNodeId(peerId, wrapper.NetworkParent); HLBytes.Pack(buffer, parentId); var networkSceneId = NetworkScenesRegister.SCENES_PACK[wrapper.NetworkParent.Node.SceneFilePath]; HLBytes.Pack(buffer, NetworkScenesRegister.NODE_PATHS_PACK[networkSceneId][wrapper.NetworkParent.Node.GetPathTo(wrapper.Node.GetParent())]); @@ -193,7 +197,7 @@ public HLBuffer Export(IPeerStateController peerStateController, NetPeer peerId) return buffer; } - public void Acknowledge(IPeerStateController peerStateController, NetPeer peer, Tick tick) + public void Acknowledge(WorldRunner currentWorld, NetPeer peer, Tick tick) { var peerTick = setupTicks.TryGetValue(peer, out var setupTick) ? setupTick : 0; if (setupTick == 0) @@ -203,7 +207,7 @@ public void Acknowledge(IPeerStateController peerStateController, NetPeer peer, if (tick >= peerTick) { - peerStateController.SetSpawnedForClient(wrapper.NetworkId, peer); + currentWorld.SetSpawnedForClient(wrapper.NetworkId, peer); } } diff --git a/addons/HLNC/Utilities/NetworkTransform.cs b/addons/HLNC/Utilities/NetworkTransform.cs index 0d053e3..a07dd63 100644 --- a/addons/HLNC/Utilities/NetworkTransform.cs +++ b/addons/HLNC/Utilities/NetworkTransform.cs @@ -31,10 +31,10 @@ public void OnNetworkChangeIsTeleporting(Tick tick, bool from, bool to) /// public override void _NetworkReady() { - base._Ready(); + base._NetworkReady(); TargetNode ??= GetParent3D(); SourceNode ??= GetParent3D(); - if (GetMeta("import_from_json", false).AsBool()) + if (GetMeta("import_from_external", false).AsBool()) { SourceNode.Position = NetPosition; SourceNode.Rotation = NetRotation; @@ -117,12 +117,16 @@ public double NetworkLerpNetRotation(Variant from, Variant to, double weight) /// public override void _PhysicsProcess(double delta) { + if (Engine.IsEditorHint()) + { + return; + } base._PhysicsProcess(delta); - if (!IsNetworkReady) return; if (NetworkRunner.Instance.IsServer) { return; } + if (!IsNetworkReady) return; TargetNode.Position = NetPosition; TargetNode.Rotation = NetRotation; } diff --git a/addons/HLNC/WorldRunner.cs b/addons/HLNC/WorldRunner.cs new file mode 100644 index 0000000..f886026 --- /dev/null +++ b/addons/HLNC/WorldRunner.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using Godot; +using Heavenlode; +using HLNC.Serialization; + +namespace HLNC +{ + public partial class WorldRunner : Node + { + public enum PeerSyncStatus { + INITIAL, + IN_WORLD + } + + public struct PeerState + { + public NetPeer Peer; + public Tick Tick; + public PeerSyncStatus Status; + public string Token; + public Dictionary WorldToPeerNodeMap; + public Dictionary PeerToWorldNodeMap; + public Dictionary SpawnAware; + public long AvailableNodes; + } + + public Guid WorldId { get; internal set; } + + // A bit list of all nodes in use by each peer + // For example, 0 0 0 0 (... etc ...) 0 1 0 1 would mean that the first and third nodes are in use + public long ClientAvailableNodes = 0; + readonly static byte MAX_NETWORK_NODES = 64; + private Dictionary PeerStates = []; + + [Signal] + public delegate void OnPeerSyncStatusChangeEventHandler(string peerId, int status); + + + /// + /// Only applicable on the client side. + /// + public static WorldRunner CurrentWorld { get; internal set; } + + /// + /// Only used by the client to determine the current root scene. + /// + public NetworkNodeWrapper RootScene; + + internal int NetworkId_counter = 0; + internal System.Collections.Generic.Dictionary NetworkScenes = []; + private Godot.Collections.Dictionary>> inputStore = []; + public Godot.Collections.Dictionary>> InputStore => inputStore; + + public override void _Ready() + { + base._Ready(); + Name = "WorldRunner"; + } + + internal void DebugPrint(string msg) + { + GD.Print($"{(OS.HasFeature("dedicated_server") ? "Server" : "Client")} (world {WorldId}): {msg}"); + } + + /// + /// The current network tick. On the client side, this does not represent the server's current tick, which will always be slightly ahead. + /// + public int CurrentTick { get; internal set; } = 0; + + public NetworkNodeWrapper GetNodeFromNetworkId(NetworkId network_id) + { + if (network_id == -1) + return new NetworkNodeWrapper(null); + if (!NetworkScenes.ContainsKey(network_id)) + return new NetworkNodeWrapper(null); + return NetworkScenes[network_id]; + } + + [Signal] + public delegate void OnAfterNetworkTickEventHandler(Tick tick); + + [Signal] + public delegate void OnPlayerJoinedEventHandler(string peerId); + + private int _frameCounter = 0; + /// + /// This method is executed every tick on the Server side, and kicks off all logic which processes and sends data to every client. + /// + public void ServerProcessTick() + { + + foreach (var net_id in NetworkScenes.Keys) + { + var networkNode = NetworkScenes[net_id]; + if (networkNode == null) + continue; + + if (!IsInstanceValid(networkNode.Node) || networkNode.Node.IsQueuedForDeletion()) + { + NetworkScenes.Remove(net_id); + continue; + } + networkNode._NetworkProcess(CurrentTick); + networkNode.InputBuffer = []; + foreach (var networkChild in networkNode.StaticNetworkChildren) + { + networkChild._NetworkProcess(CurrentTick); + networkChild.InputBuffer = []; + } + } + + var peers = PeerStates.Keys.ToList(); + var exportedState = ExportState(peers); + foreach (var peer in peers) + { + var size = exportedState[peer].bytes.Length; + if (size > NetworkRunner.MTU) + { + NetworkRunner.DebugPrint($"Warning: Data size {size} exceeds MTU {NetworkRunner.MTU}"); + } + + var buffer = new HLBuffer(); + HLBytes.Pack(buffer, CurrentTick); + HLBytes.Pack(buffer, exportedState[peer].bytes, true); + + peer.Send(1, buffer.bytes, (int)ENetPacketPeer.FlagUnsequenced); + } + } + + public override void _PhysicsProcess(double delta) + { + base._PhysicsProcess(delta); + + if (NetworkRunner.Instance.IsServer) + { + _frameCounter += 1; + if (_frameCounter < NetworkRunner.PhysicsTicksPerNetworkTick) + return; + _frameCounter = 0; + CurrentTick += 1; + ServerProcessTick(); + EmitSignal("OnAfterNetworkTick", CurrentTick); + } + } + + public bool HasSpawnedForClient(NetworkId networkId, NetPeer peer) + { + if (!PeerStates.ContainsKey(peer)) + { + return false; + } + if (!PeerStates[peer].SpawnAware.ContainsKey(networkId)) + { + return false; + } + return PeerStates[peer].SpawnAware[networkId]; + } + + public void SetSpawnedForClient(NetworkId networkId, NetPeer peer) + { + PeerStates[peer].SpawnAware[networkId] = true; + } + + public void ChangeScene(NetworkNodeWrapper node) + { + if (NetworkRunner.Instance.IsServer) return; + + if (RootScene != null) { + RootScene.Node.QueueFree(); + } + NetworkRunner.DebugPrint("Changing scene to " + node.Node.Name); + // TODO: Support this more generally + GetTree().CurrentScene.AddChild(node.Node); + RootScene = node; + node._NetworkPrepare(this); + } + + public PeerState? GetPeerWorldState(string peerId) + { + var peer = NetworkRunner.Instance.GetPeer(peerId); + if (!PeerStates.ContainsKey(peer)) + { + return null; + } + return PeerStates[peer]; + } + + public PeerState? GetPeerWorldState(NetPeer peer) + { + if (!PeerStates.ContainsKey(peer)) + { + return null; + } + return PeerStates[peer]; + } + + readonly private Dictionary pendingSyncStates = []; + public void SetPeerState(NetPeer peer, PeerState state) + { + if (PeerStates[peer].Status != state.Status) { + EmitSignal("OnPeerSyncStatusChange", NetworkRunner.Instance.GetPeerId(peer), (int)state.Status); + } + if (state.Status == PeerSyncStatus.IN_WORLD) + { + EmitSignal("OnPlayerJoined", NetworkRunner.Instance.GetPeerId(peer)); + } + PeerStates[peer] = state; + } + + public void QueuePeerState(NetPeer peer, PeerSyncStatus status) + { + var newState = PeerStates[peer]; + newState.Status = status; + newState.Tick = CurrentTick; + pendingSyncStates[peer] = newState; + } + + public NetworkNodeWrapper GetNetworkNode(NetworkId networkId) + { + if (NetworkScenes.ContainsKey(networkId)) + { + return NetworkScenes[networkId]; + } + return null; + } + public byte GetPeerNodeId(NetPeer peer, NetworkNodeWrapper node) + { + if (node == null) return 0; + if (!PeerStates.ContainsKey(peer)) + { + return 0; + } + if (!PeerStates[peer].WorldToPeerNodeMap.ContainsKey(node.NetworkId)) + { + return 0; + } + return PeerStates[peer].WorldToPeerNodeMap[node.NetworkId]; + } + + public NetworkNodeWrapper GetPeerNode(NetPeer peer, byte networkId) + { + if (!PeerStates.ContainsKey(peer)) + { + return null; + } + if (!PeerStates[peer].PeerToWorldNodeMap.ContainsKey(networkId)) + { + return null; + } + return NetworkScenes[PeerStates[peer].PeerToWorldNodeMap[networkId]]; + } + + public void DeregisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null) + { + if (NetworkRunner.Instance.IsServer) + { + if (peer == null) + { + GD.PrintErr("Server must specify a peer when deregistering a node."); + return; + } + if (PeerStates[peer].WorldToPeerNodeMap.ContainsKey(node.NetworkId)) + { + var peerState = PeerStates[peer]; + peerState.AvailableNodes &= ~(1 << PeerStates[peer].WorldToPeerNodeMap[node.NetworkId]); + PeerStates[peer] = peerState; + PeerStates[peer].WorldToPeerNodeMap.Remove(node.NetworkId); + } + } + else + { + NetworkScenes.Remove(node.NetworkId); + } + } + + // A local peer node ID is assigned to each node that a peer owns + // This allows us to sync nodes across the network without sending long integers + // 0 indicates that the node is not registered. Node ID starts at 1 + // Up to 64 nodes can be networked per peer at a time. + // TODO: Consider supporting more + // TODO: Handle de-registration of nodes (e.g. despawn, and object interest) + public byte TryRegisterPeerNode(NetworkNodeWrapper node, NetPeer peer = null) + { + if (NetworkRunner.Instance.IsServer) + { + if (peer == null) + { + GD.PrintErr("Server must specify a peer when registering a node."); + return 0; + } + if (PeerStates[peer].WorldToPeerNodeMap.ContainsKey(node.NetworkId)) + { + return PeerStates[peer].WorldToPeerNodeMap[node.NetworkId]; + } + for (byte i = 0; i < MAX_NETWORK_NODES; i++) + { + byte localNodeId = (byte)(i + 1); + if ((PeerStates[peer].AvailableNodes & ((long)1 << localNodeId)) == 0) + { + PeerStates[peer].WorldToPeerNodeMap[node.NetworkId] = localNodeId; + PeerStates[peer].PeerToWorldNodeMap[localNodeId] = node.NetworkId; + var peerState = PeerStates[peer]; + peerState.AvailableNodes |= (long)1 << localNodeId; + PeerStates[peer] = peerState; + return localNodeId; + } + } + + GD.PrintErr("Peer " + peer + " has reached the maximum amount of nodes."); + return 0; + } + + if (NetworkScenes.ContainsKey(node.NetworkId)) + { + return 0; + } + + NetworkScenes[node.NetworkId] = node; + return 1; + } + + public NetworkNode3D Spawn(NetworkNode3D node, NetworkNode3D parent = null, NetPeer inputAuthority = null, string nodePath = ".") + { + if (!NetworkRunner.Instance.IsServer) return null; + + node.DynamicSpawn = true; + node.CurrentWorld = this; + node.InputAuthority = inputAuthority; + if (parent == null) + { + node.NetworkParent = RootScene; + node.NetworkParent.Node.GetNode(nodePath).AddChild(node); + } + else + { + node.NetworkParent = new NetworkNodeWrapper(parent); + parent.GetNode(nodePath).AddChild(node); + } + node._NetworkPrepare(this); + return node; + } + + public void JoinPeer(NetPeer peer, string token) + { + NetworkRunner.Instance.PeerWorldMap[peer] = this; + PeerStates[peer] = new PeerState + { + Peer = peer, + Tick = 0, + Status = PeerSyncStatus.INITIAL, + Token = token, + WorldToPeerNodeMap = [], + PeerToWorldNodeMap = [], + SpawnAware = [] + }; + } + + public Dictionary ExportState(List peers) + { + Dictionary peerBuffers = []; + foreach (ENetPacketPeer peer in peers) + { + long updatedNodes = 0; + peerBuffers[peer] = new HLBuffer(); + var peerNodesBuffers = new Dictionary(); + var peerNodesSerializersList = new Dictionary(); + foreach (var node in NetworkScenes.Values) + { + var serializersBuffer = new HLBuffer(); + byte serializersRun = 0; + for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) + { + var serializer = node.Serializers[serializerIdx]; + var serializerResult = serializer.Export(this, peer); + if (serializerResult.bytes.Length == 0) + { + continue; + } + serializersRun |= (byte)(1 << serializerIdx); + HLBytes.Pack(serializersBuffer, serializerResult.bytes); + } + if (serializersRun == 0) + { + continue; + } + byte localNodeId = PeerStates[peer].WorldToPeerNodeMap[node.NetworkId]; + updatedNodes |= (long)1 << localNodeId; + peerNodesSerializersList[localNodeId] = serializersRun; + peerNodesBuffers[localNodeId] = new HLBuffer(); + HLBytes.Pack(peerNodesBuffers[localNodeId], serializersBuffer.bytes); + } + + // 1. Pack a bit list of all nodes which have serialized data + HLBytes.Pack(peerBuffers[peer], updatedNodes); + + // 2. Pack what serializers are run for every node + var orderedNodeKeys = peerNodesBuffers.OrderBy(x => x.Key).Select(x => x.Key).ToList(); + foreach (var nodeKey in orderedNodeKeys) + { + HLBytes.Pack(peerBuffers[peer], peerNodesSerializersList[nodeKey]); + } + + // 3. Pack the serialized data for every node + foreach (var nodeKey in orderedNodeKeys) + { + HLBytes.Pack(peerBuffers[peer], peerNodesBuffers[nodeKey].bytes); + } + } + + foreach (var node in NetworkScenes.Values) + { + // Finally, cleanup serializers + foreach (var serializer in node.Serializers) + { + serializer.Cleanup(); + } + } + + return peerBuffers; + } + + public void ImportState(HLBuffer stateBytes) + { + var affectedNodes = HLBytes.UnpackInt64(stateBytes); + var nodeIdToSerializerList = new Dictionary(); + for (byte i = 0; i < MAX_NETWORK_NODES; i++) + { + if ((affectedNodes & ((long)1 << i)) == 0) + { + continue; + } + var serializersRun = HLBytes.UnpackInt8(stateBytes); + nodeIdToSerializerList[i] = serializersRun; + } + + foreach (var nodeIdSerializerList in nodeIdToSerializerList) + { + var localNodeId = nodeIdSerializerList.Key; + NetworkScenes.TryGetValue(localNodeId, out NetworkNodeWrapper node); + if (node == null) { + var blankScene = new NetworkNode3D + { + NetworkId = localNodeId + }; + blankScene.SetupSerializers(); + NetworkRunner.Instance.AddChild(blankScene); + node = new NetworkNodeWrapper(blankScene); + } + for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) + { + if ((nodeIdSerializerList.Value & ((long)1 << serializerIdx)) == 0) + { + continue; + } + var serializerInstance = node.Serializers[serializerIdx]; + serializerInstance.Import(this, stateBytes, out NetworkNodeWrapper nodeOut); + if (node != nodeOut) + { + node = nodeOut; + serializerIdx = 0; + } + } + } + } + + public void PeerAcknowledge(NetPeer peer, Tick tick) + { + if (PeerStates[peer].Tick >= tick) + { + return; + } + if (PeerStates[peer].Status == PeerSyncStatus.INITIAL) + { + var newPeerState = PeerStates[peer]; + newPeerState.Tick = tick; + newPeerState.Status = PeerSyncStatus.IN_WORLD; + // The first time a peer acknowledges a tick, we know they are in the zone + SetPeerState(peer, newPeerState); + } + // if (pendingSyncStates.TryGetValue(peer, out PendingSyncState pendingSyncState)) + // { + // if (pendingSyncState.tick <= tick) + // { + // PeerState[peer] = pendingSyncState.state; + // pendingSyncStates.Remove(peer); + // } + // } + foreach (var node in NetworkScenes.Values) + { + for (var serializerIdx = 0; serializerIdx < node.Serializers.Length; serializerIdx++) + { + var serializer = node.Serializers[serializerIdx]; + serializer.Acknowledge(this, peer, tick); + } + } + } + public void ClientHandleTick(int incomingTick, byte[] stateBytes) + { + if (incomingTick <= CurrentTick) + { + return; + } + // GD.Print("INCOMING DATA: " + BitConverter.ToString(HLBytes.Decompress(stateBytes))); + CurrentTick = incomingTick; + ImportState(new HLBuffer(stateBytes)); + foreach (var net_id in NetworkScenes.Keys) + { + var node = NetworkScenes[net_id]; + if (node == null) + continue; + if (node.Node.IsQueuedForDeletion()) + { + NetworkScenes.Remove(net_id); + continue; + } + node._NetworkProcess(CurrentTick); + SendInput(node); + + foreach (var staticChild in node.StaticNetworkChildren) + { + if (staticChild == null || staticChild.Node.IsQueuedForDeletion()) + { + continue; + } + staticChild._NetworkProcess(CurrentTick); + SendInput(staticChild); + } + } + HLBuffer buffer = new HLBuffer(); + HLBytes.Pack(buffer, incomingTick); + NetworkRunner.Instance.ENetHost.Send(1, buffer.bytes, (int)ENetPacketPeer.FlagUnsequenced); + } + + public void RegisterSpawn(NetworkNodeWrapper wrapper) + { + if (NetworkRunner.Instance.IsServer) + { + NetworkId_counter += 1; + while (NetworkScenes.ContainsKey(NetworkId_counter)) + { + NetworkId_counter += 1; + } + NetworkScenes[NetworkId_counter] = wrapper; + wrapper.NetworkId = NetworkId_counter; + return; + } + + if (!wrapper.DynamicSpawn) + { + wrapper.Node.QueueFree(); + } + } + + public void SendInput(NetworkNodeWrapper networkNode) { + if (NetworkRunner.Instance.IsServer) return; + var setInputs = networkNode.InputBuffer.Keys.Aggregate((long)0, (acc, key) => { + acc |= (long)1 << key; + return acc; + }); + if (setInputs == 0) + { + return; + } + + var inputBuffer = new HLBuffer(); + HLBytes.Pack(inputBuffer, (byte)networkNode.NetworkId); + HLBytes.Pack(inputBuffer, setInputs); + foreach (var key in networkNode.InputBuffer.Keys) + { + HLBytes.Pack(inputBuffer, key); + HLBytes.PackVariant(inputBuffer, networkNode.InputBuffer[key], true, true); + } + + NetworkRunner.Instance.ENetHost.Send((int)NetworkRunner.ENetChannelId.Input, inputBuffer.bytes, (int)ENetPacketPeer.FlagReliable); + networkNode.InputBuffer = []; + } + + public void ReceiveInput(NetPeer peer, HLBuffer buffer) + { + if (!NetworkRunner.Instance.IsServer) return; + var networkId = HLBytes.UnpackByte(buffer); + var worldNetworkId = PeerStates[peer].PeerToWorldNodeMap.GetValueOrDefault(networkId, -1); + var node = GetNodeFromNetworkId(worldNetworkId); + if (node == null) + { + GD.PrintErr("Received input for unknown node " + worldNetworkId); + return; + } + + if (node.InputAuthority != peer) + { + GD.PrintErr("Received input for node " + worldNetworkId + " from unauthorized peer " + peer); + return; + } + + var setInputs = HLBytes.UnpackInt64(buffer); + while (setInputs > 0) + { + var key = HLBytes.UnpackInt8(buffer); + var value = HLBytes.UnpackVariant(buffer); + if (value.HasValue) { + node.InputBuffer[key] = value.Value; + } + setInputs &= ~((long)1 << key); + } + } + } +} \ No newline at end of file diff --git a/addons/HLNC/plugin.gd b/addons/HLNC/plugin.gd index df86db7..2a5fe25 100644 --- a/addons/HLNC/plugin.gd +++ b/addons/HLNC/plugin.gd @@ -2,7 +2,6 @@ extends EditorPlugin const AUTOLOAD_RUNNER = "NetworkRunner" -const AUTOLOAD_STATE_MANAGER = "NetworkPeerManager" const AUTOLOAD_SCENES_REGISTER = "NetworkScenesRegister" # const MainPanel = preload("res://addons/HLNC/editor_plugin/main_screen.tscn") @@ -29,7 +28,6 @@ func _enter_tree(): # editor_file_system.reimport_files(["res://addons/HLNC/generated/registered_nodes.cs"]) # editor_file_system.scan() add_autoload_singleton(AUTOLOAD_RUNNER, "res://addons/HLNC/NetworkRunner.cs") - add_autoload_singleton(AUTOLOAD_STATE_MANAGER, "res://addons/HLNC/NetworkPeerManager/NetworkPeerManager.cs") add_autoload_singleton(AUTOLOAD_SCENES_REGISTER, "res://addons/HLNC/Serialization/NetworkScenesRegister.cs") # main_panel_instance = MainPanel.instantiate() # # Add the main panel to the editor's main viewport. @@ -46,7 +44,6 @@ func _exit_tree(): # if main_panel_instance: # main_panel_instance.queue_free() remove_autoload_singleton(AUTOLOAD_SCENES_REGISTER) - remove_autoload_singleton(AUTOLOAD_STATE_MANAGER) remove_autoload_singleton(AUTOLOAD_RUNNER)