|
| 1 | +using Godot; |
| 2 | +using System; |
| 3 | +using System.Linq; |
| 4 | +using System.Threading; |
| 5 | +using System.Threading.Tasks; |
| 6 | +using System.Collections.Generic; |
| 7 | +using System.Collections.Concurrent; |
| 8 | +using ENet; |
| 9 | +using Common.Netcode; |
| 10 | + |
| 11 | +using Thread = System.Threading.Thread; // CS0104: Ambigious reference between 'Godot.Thread' and 'System.Threading.Thread' |
| 12 | +using Version = Common.Netcode.Version; // CS0104: Ambiguous reference between 'Common.Netcode.Version' and 'System.Version' |
| 13 | + |
| 14 | +namespace Valk.Modules.Netcode.Client |
| 15 | +{ |
| 16 | + public abstract class ENetClient : Node |
| 17 | + { |
| 18 | + public static readonly ConcurrentQueue<ClientPacket> Outgoing = new ConcurrentQueue<ClientPacket>(); |
| 19 | + public static readonly ConcurrentQueue<ENetCmd> ENetCmds = new ConcurrentQueue<ENetCmd>(); |
| 20 | + |
| 21 | + private static readonly ConcurrentBag<Packet> Incoming = new ConcurrentBag<Packet>(); |
| 22 | + private static readonly ConcurrentQueue<GodotCmd> GodotCmds = new ConcurrentQueue<GodotCmd>(); |
| 23 | + private static readonly Dictionary<ServerPacketOpcode, HandlePacket> HandlePacket = typeof(HandlePacket).Assembly.GetTypes().Where(x => typeof(HandlePacket).IsAssignableFrom(x) && !x.IsAbstract).Select(Activator.CreateInstance).Cast<HandlePacket>().ToDictionary(x => x.Opcode, x => x); |
| 24 | + public static bool ENetThreadRunning; |
| 25 | + private static bool RunningNetCode; |
| 26 | + |
| 27 | + public static readonly Version Version = new Version { Major = 0, Minor = 1, Patch = 0 }; |
| 28 | + |
| 29 | + public abstract void ProcessGodotCommands(GodotCmd cmd); |
| 30 | + public abstract void Connect(Event netEvent); |
| 31 | + public abstract void Disconnect(Event netEvent); |
| 32 | + public abstract void Timeout(Event netEvent); |
| 33 | + |
| 34 | + public override void _Process(float delta) |
| 35 | + { |
| 36 | + while (GodotCmds.TryDequeue(out GodotCmd cmd)) |
| 37 | + { |
| 38 | + switch (cmd.Opcode) |
| 39 | + { |
| 40 | + case GodotOpcode.ENetPacket: |
| 41 | + var packetReader = (PacketReader)cmd.Data[0]; |
| 42 | + var opcode = (ServerPacketOpcode)packetReader.ReadByte(); |
| 43 | + |
| 44 | + HandlePacket[opcode].Handle(packetReader); |
| 45 | + packetReader.Dispose(); |
| 46 | + return; |
| 47 | + case GodotOpcode.LogMessage: |
| 48 | + GD.Print((string)cmd.Data[0]); |
| 49 | + return; |
| 50 | + case GodotOpcode.ExitApp: |
| 51 | + GetTree().Quit(); |
| 52 | + return; |
| 53 | + } |
| 54 | + |
| 55 | + ProcessGodotCommands(cmd); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + public void Connect(string ip, ushort port, string jwt) |
| 60 | + { |
| 61 | + if (ENetThreadRunning) |
| 62 | + { |
| 63 | + GD.Print("ENet thread is running already"); |
| 64 | + return; |
| 65 | + } |
| 66 | + |
| 67 | + ENetThreadRunning = true; |
| 68 | + Task.Run(() => ENetThreadWorker(ip, port, jwt)); |
| 69 | + } |
| 70 | + |
| 71 | + private void ENetThreadWorker(string ip, ushort port, string jwt) |
| 72 | + { |
| 73 | + Library.Initialize(); |
| 74 | + GDLog("ENet library initialized successfully"); |
| 75 | + var wantsToExit = false; |
| 76 | + var wantsToDisconnect = false; |
| 77 | + |
| 78 | + using (var client = new Host()) |
| 79 | + { |
| 80 | + var address = new Address(); |
| 81 | + |
| 82 | + address.SetHost(ip); |
| 83 | + address.Port = port; |
| 84 | + client.Create(); |
| 85 | + |
| 86 | + //GDLog("Attempting to connect to the game server..."); |
| 87 | + var peer = client.Connect(address); |
| 88 | + |
| 89 | + uint pingInterval = 1000; // Pings are used both to monitor the liveness of the connection and also to dynamically adjust the throttle during periods of low traffic so that the throttle has reasonable responsiveness during traffic spikes. |
| 90 | + uint timeout = 5000; // Will be ignored if maximum timeout is exceeded |
| 91 | + uint timeoutMinimum = 5000; // The timeout for server not sending the packet to the client sent from the server |
| 92 | + uint timeoutMaximum = 5000; // The timeout for server not receiving the packet sent from the client |
| 93 | + |
| 94 | + peer.PingInterval(pingInterval); |
| 95 | + peer.Timeout(timeout, timeoutMinimum, timeoutMaximum); |
| 96 | + |
| 97 | + RunningNetCode = true; |
| 98 | + while (RunningNetCode) { |
| 99 | + var polled = false; |
| 100 | + |
| 101 | + // ENet Cmds from Godot Thread |
| 102 | + while (ENetCmds.TryDequeue(out ENetCmd cmd)) |
| 103 | + { |
| 104 | + switch (cmd.Opcode) |
| 105 | + { |
| 106 | + case ENetOpcode.ClientWantsToExitApp: |
| 107 | + peer.Disconnect(0); |
| 108 | + RunningNetCode = false; |
| 109 | + wantsToExit = true; |
| 110 | + break; |
| 111 | + case ENetOpcode.ClientWantsToDisconnect: |
| 112 | + peer.Disconnect(0); |
| 113 | + RunningNetCode = false; |
| 114 | + wantsToDisconnect = true; |
| 115 | + break; |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + // Incoming |
| 120 | + while (Incoming.TryTake(out Packet packet)) |
| 121 | + GodotCmds.Enqueue(new GodotCmd { Opcode = GodotOpcode.ENetPacket, Data = new List<object> { new PacketReader(packet) }}); |
| 122 | + |
| 123 | + // Outgoing |
| 124 | + while (Outgoing.TryDequeue(out ClientPacket clientPacket)) |
| 125 | + { |
| 126 | + byte channelID = 0; // The channel all networking traffic will be going through |
| 127 | + var packet = default(Packet); |
| 128 | + packet.Create(clientPacket.Data, clientPacket.PacketFlags); |
| 129 | + peer.Send(channelID, ref packet); |
| 130 | + } |
| 131 | + |
| 132 | + while (!polled) { |
| 133 | + if (client.CheckEvents(out Event netEvent) <= 0) { |
| 134 | + if (client.Service(15, out netEvent) <= 0) |
| 135 | + break; |
| 136 | + |
| 137 | + polled = true; |
| 138 | + } |
| 139 | + |
| 140 | + switch (netEvent.Type) |
| 141 | + { |
| 142 | + case EventType.Connect: |
| 143 | + // Send login request |
| 144 | + Outgoing.Enqueue(new ClientPacket((byte)ClientPacketOpcode.Login, new WPacketLogin |
| 145 | + { |
| 146 | + JsonWebToken = jwt, |
| 147 | + VersionMajor = Version.Major, |
| 148 | + VersionMinor = Version.Minor, |
| 149 | + VersionPatch = Version.Patch |
| 150 | + })); |
| 151 | + |
| 152 | + Connect(netEvent); |
| 153 | + break; |
| 154 | + case EventType.Receive: |
| 155 | + // Receive |
| 156 | + var packet = netEvent.Packet; |
| 157 | + if (packet.Length > GamePacket.MaxSize) |
| 158 | + { |
| 159 | + GDLog($"Tried to read packet from server of size {packet.Length} when max packet size is {GamePacket.MaxSize}"); |
| 160 | + packet.Dispose(); |
| 161 | + continue; |
| 162 | + } |
| 163 | + |
| 164 | + Incoming.Add(netEvent.Packet); |
| 165 | + break; |
| 166 | + case EventType.Timeout: |
| 167 | + RunningNetCode = false; |
| 168 | + wantsToDisconnect = true; |
| 169 | + Timeout(netEvent); |
| 170 | + break; |
| 171 | + case EventType.Disconnect: |
| 172 | + RunningNetCode = false; |
| 173 | + wantsToDisconnect = true; |
| 174 | + Disconnect(netEvent); |
| 175 | + break; |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + client.Flush(); |
| 181 | + } |
| 182 | + |
| 183 | + Library.Deinitialize(); |
| 184 | + ENetThreadRunning = false; |
| 185 | + |
| 186 | + if (wantsToDisconnect) |
| 187 | + GodotCmds.Enqueue(new GodotCmd { Opcode = GodotOpcode.LoadMainMenu }); |
| 188 | + |
| 189 | + if (wantsToExit) |
| 190 | + GodotCmds.Enqueue(new GodotCmd { Opcode = GodotOpcode.ExitApp }); |
| 191 | + } |
| 192 | + |
| 193 | + protected static void GDLog(string text) => GodotCmds.Enqueue(new GodotCmd { Opcode = GodotOpcode.LogMessage, Data = new List<object> { text }}); |
| 194 | + } |
| 195 | + |
| 196 | + public class GodotCmd |
| 197 | + { |
| 198 | + public GodotOpcode Opcode { get; set; } |
| 199 | + public List<object> Data { get; set; } |
| 200 | + } |
| 201 | + |
| 202 | + public class ENetCmd |
| 203 | + { |
| 204 | + public ENetOpcode Opcode { get; set; } |
| 205 | + public List<object> Data { get; set; } |
| 206 | + } |
| 207 | + |
| 208 | + public enum GodotOpcode |
| 209 | + { |
| 210 | + ENetPacket, |
| 211 | + LogMessage, |
| 212 | + LoadMainMenu, |
| 213 | + ExitApp |
| 214 | + } |
| 215 | + |
| 216 | + public enum ENetOpcode |
| 217 | + { |
| 218 | + ClientWantsToExitApp, |
| 219 | + ClientWantsToDisconnect |
| 220 | + } |
| 221 | +} |
0 commit comments