package net; import net.NetPacket.ExplodableUpdatePacket; import gui.MPMessageGui; import gui.MessageBoxOkDlg; import gui.JoinServerGui; import gui.MPPreGameDlg; import net.NetPacket.ScoreboardPacket; import gui.MPPlayMissionGui; import gui.Canvas; import net.MasterServerClient.RemoteServerInfo; import src.ResourceLoader; import src.AudioManager; import net.NetPacket.GemPickupPacket; import net.NetPacket.GemSpawnPacket; import net.BitStream.InputBitStream; import net.BitStream.OutputBitStream; import net.NetPacket.PowerupPickupPacket; import net.ClientConnection; import net.NetPacket.MarbleUpdatePacket; import net.NetPacket.MarbleMovePacket; import haxe.Json; import datachannel.RTCPeerConnection; import datachannel.RTCDataChannel; import src.Console; import net.NetCommands; import src.MarbleGame; import src.Settings; enum abstract NetPacketType(Int) from Int to Int { var NullPacket; var ClientIdAssign; var NetCommand; var Ping; var PingBack; var MarbleUpdate; var MarbleMove; var PowerupPickup; var GemSpawn; var GemPickup; var ExplodableUpdate; var PlayerInfo; var ScoreBoardInfo; } @:publicFields class ServerInfo { var id:String; var name:String; var hostname:String; var description:String; var players:Int; var maxPlayers:Int; var password:String; var state:String; var platform:NetPlatform; public function new(name:String, hostname:String, description:String, players:Int, maxPlayers:Int, password:String, state:String, platform:NetPlatform) { this.id = Uuid.v4(); this.name = name; this.hostname = hostname; this.description = description; this.players = players; this.maxPlayers = maxPlayers; this.password = password; this.state = state; this.platform = platform; } } @:publicFields @:structInit class ConnectedServerInfo { var name:String; var description:String; var quickRespawn:Bool; var forceSpectator:Bool; var competitiveMode:Bool; var oldSpawns:Bool; } class Net { static var client:RTCPeerConnection; static var clientDatachannel:RTCDataChannel; static var clientDatachannelUnreliable:RTCDataChannel; public static var isMP:Bool; public static var isHost:Bool; public static var isClient:Bool; public static var lobbyHostReady:Bool; public static var lobbyClientReady:Bool; public static var hostReady:Bool; public static var hostSpectate:Bool; public static var clientSpectate:Bool; static var clientIdAllocs:Int = 1; public static var clientId:Int; public static var networkRNG:Float; public static var clients:Map = []; public static var clientIdMap:Map = []; public static var clientConnection:ClientConnection; public static var serverInfo:ServerInfo; public static var remoteServerInfo:RemoteServerInfo; public static var connectedServerInfo:ConnectedServerInfo; static var stunServers = ["stun:stun.l.google.com:19302"]; public static var turnServers:Array = []; static var onTurnServersReceived:Null<() -> Void> = null; public static function hostServer(name:String, description:String, maxPlayers:Int, password:String, onHosted:() -> Void) { serverInfo = new ServerInfo(name, Settings.highscoreName, description, 1, maxPlayers, password, "LOBBY", getPlatform()); MasterServerClient.connectToMasterServer(() -> { isHost = true; isClient = false; clientId = 0; isMP = true; MasterServerClient.instance.sendServerInfo(serverInfo); Net.connectedServerInfo = { name: name, description: description, competitiveMode: Settings.serverSettings.competitiveMode, quickRespawn: Settings.serverSettings.quickRespawn, forceSpectator: Settings.serverSettings.forceSpectators, oldSpawns: Settings.serverSettings.oldSpawns }; onHosted(); }); } public static function addClientFromSdp(sdpString:String, onFinishSdp:String->Void, turnTried:Bool = false) { if (Net.turnServers.length == 0 && !turnTried) { MasterServerClient.requestTurnCredentials(); Net.onTurnServersReceived = () -> { Net.onTurnServersReceived = null; addClientFromSdp(sdpString, onFinishSdp, true); }; return; } var peer = new RTCPeerConnection(stunServers.concat(Net.turnServers), "0.0.0.0"); var sdpObj = Json.parse(sdpString); peer.setRemoteDescription(sdpObj.sdp, sdpObj.type); addClient(peer, onFinishSdp); } static function addClient(peer:RTCPeerConnection, onFinishSdp:String->Void) { var candidates = []; peer.onLocalCandidate = (c) -> { Console.log('Local candidate: ' + c); if (c != "") candidates.push('a=${c}'); } peer.onStateChange = (s) -> { switch (s) { case RTC_CLOSED: Console.log("RTC State change: Connection closed!"); case RTC_CONNECTED: Console.log("RTC State change: Connected!"); case RTC_CONNECTING: Console.log("RTC State change: Connecting..."); case RTC_DISCONNECTED: Console.log("RTC State change: Disconnected!"); case RTC_FAILED: Console.log("RTC State change: Failed!"); case RTC_NEW: Console.log("RTC State change: New..."); } } var sdpFinished = false; var finishSdp = () -> { if (sdpFinished) return; if (peer == null) return; sdpFinished = true; var sdpObj = StringTools.trim(peer.localDescription); sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n'; onFinishSdp(Json.stringify({ sdp: sdpObj, type: "answer" })); } peer.onGatheringStateChange = (s) -> { switch (s) { case RTC_GATHERING_COMPLETE: Console.log("Gathering complete!"); case RTC_GATHERING_INPROGRESS: Console.log("Gathering in progress..."); case RTC_GATHERING_NEW: Console.log("Gathering new..."); } if (s == RTC_GATHERING_COMPLETE) { finishSdp(); } } var reliable:datachannel.RTCDataChannel = null; var unreliable:datachannel.RTCDataChannel = null; peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> { if (dc.name == "mp") reliable = dc; if (dc.name == "unreliable") { unreliable = dc; switch (dc.reliability) { case Reliable: Console.log("Error opening unreliable datachannel!"); case Unreliable(maxRetransmits, maxLifetime): Console.log("Opened unreliable datachannel: " + maxRetransmits + " " + maxLifetime); } } if (reliable != null && unreliable != null) onClientConnect(peer, reliable, unreliable); } } static function addGhost(id:Int) { var ghost = new DummyConnection(id); clientIdMap[id] = ghost; } public static function joinServer(serverName:String, password:String, connectedCb:() -> Void, turnTried:Bool = false) { MasterServerClient.connectToMasterServer(() -> { if (Net.turnServers.length == 0 && !turnTried) { MasterServerClient.requestTurnCredentials(); Net.onTurnServersReceived = () -> { Net.onTurnServersReceived = null; joinServer(serverName, password, connectedCb, true); }; return; } client = new RTCPeerConnection(stunServers.concat(Net.turnServers), "0.0.0.0"); var candidates = []; var closing = false; isMP = true; isHost = false; isClient = true; var closeFunc = (msg:String, forceShow:Bool) -> { if (closing) return; closing = true; var weLeftOurselves = !Net.isClient; // If we left ourselves, this would be set to false due to order of ops, disconnect being called first, and then the datachannel closing disconnect(); if (MarbleGame.instance.world != null) { MarbleGame.instance.quitMission(); } if (!weLeftOurselves || forceShow) { if (MarbleGame.canvas.content is MPMessageGui) { var loadGui:MPMessageGui = cast MarbleGame.canvas.content; if (loadGui != null) { loadGui.setTexts("Error", msg); } } else { MarbleGame.canvas.setContent(new MPMessageGui("Error", msg)); } } } client.onLocalCandidate = (c) -> { Console.log('Local candidate: ' + c); if (c != "") candidates.push('a=${c}'); } client.onStateChange = (s) -> { switch (s) { case RTC_CLOSED: Console.log("RTC State change: Connection closed!"); closeFunc("Connection closed", false); case RTC_CONNECTED: Console.log("RTC State change: Connected!"); case RTC_CONNECTING: Console.log("RTC State change: Connecting..."); case RTC_DISCONNECTED: Console.log("RTC State change: Disconnected!"); case RTC_FAILED: Console.log("RTC State change: Failed!"); case RTC_NEW: Console.log("RTC State change: New..."); } } var sdpFinished = false; var finishSdp = () -> { if (sdpFinished) return; sdpFinished = true; if (client == null) return; Console.log("Local Description Set!"); var sdpObj = StringTools.trim(client.localDescription); sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n'; MasterServerClient.instance.sendConnectToServer(serverName, Json.stringify({ sdp: sdpObj, type: "offer" }), password); } client.onGatheringStateChange = (s) -> { switch (s) { case RTC_GATHERING_COMPLETE: Console.log("Gathering complete!"); case RTC_GATHERING_INPROGRESS: Console.log("Gathering in progress..."); case RTC_GATHERING_NEW: Console.log("Gathering new..."); } if (s == RTC_GATHERING_COMPLETE) { finishSdp(); } } // haxe.Timer.delay(() -> { // finishSdp(); // }, 5000); clientDatachannel = client.createDatachannel("mp"); clientDatachannelUnreliable = client.createDatachannelWithOptions("unreliable", true, null, 600); var openFlags = 0; var onDatachannelOpen = (idx:Int) -> { if (!Net.isMP) { // Close client.close(); return; } openFlags |= idx; if (openFlags == 3) { if (MarbleGame.canvas.content is MPMessageGui) { var loadGui:MPMessageGui = cast MarbleGame.canvas.content; if (loadGui != null) { loadGui.setTexts("Please Wait", "Handshaking"); } } Console.log("Successfully connected!"); clients.set(client, new ClientConnection(0, client, clientDatachannel, clientDatachannelUnreliable)); // host is always 0 clientIdMap[0] = clients[client]; clientConnection = cast clients[client]; onConnectedToServer(); haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation } } var onDatachannelMessage = (dc:RTCDataChannel, b:haxe.io.Bytes) -> { onPacketReceived(clientConnection, client, clientDatachannel, new InputBitStream(b)); } var onDatachannelClose = (dc:RTCDataChannel) -> { closeFunc("Disconnected", false); } var onDatachannelError = (msg:String) -> { Console.log('Errored out due to ${msg}'); closeFunc("Connection error", false); } clientDatachannel.onOpen = (n) -> { onDatachannelOpen(1); } clientDatachannel.onMessage = (b) -> { onDatachannelMessage(clientDatachannel, b); } clientDatachannel.onClosed = () -> { onDatachannelClose(clientDatachannel); } clientDatachannel.onError = (msg) -> { onDatachannelError(msg); } clientDatachannelUnreliable.onOpen = (n) -> { onDatachannelOpen(2); } clientDatachannelUnreliable.onMessage = (b) -> { onDatachannelMessage(clientDatachannelUnreliable, b); } clientDatachannelUnreliable.onClosed = () -> { onDatachannelClose(clientDatachannelUnreliable); } clientDatachannelUnreliable.onError = (msg) -> { onDatachannelError(msg); } }); } public static function disconnect() { if (Net.isClient) { NetCommands.clientLeave(Net.clientId); Net.isMP = false; Net.isClient = false; Net.isHost = false; if (Net.client != null) Net.client.close(); Net.client = null; Net.clientDatachannel = null; Net.clientId = 0; Net.clientIdAllocs = 1; Net.clients.clear(); Net.clientIdMap.clear(); Net.clientConnection = null; Net.serverInfo = null; Net.remoteServerInfo = null; Net.connectedServerInfo = null; Net.lobbyHostReady = false; Net.lobbyClientReady = false; Net.hostReady = false; Net.hostSpectate = false; Net.clientSpectate = false; MPPlayMissionGui.allChats = []; // MultiplayerLevelSelectGui.custSelected = false; } if (Net.isHost) { NetCommands.serverClosed(); for (client => gc in clients) { client.close(); } Net.isMP = false; Net.isClient = false; Net.isHost = false; Net.clientId = 0; Net.clientIdAllocs = 1; Net.clients.clear(); Net.clientIdMap.clear(); MasterServerClient.disconnectFromMasterServer(); Net.serverInfo = null; Net.remoteServerInfo = null; Net.connectedServerInfo = null; Net.lobbyHostReady = false; Net.lobbyClientReady = false; Net.hostReady = false; Net.hostSpectate = false; Net.clientSpectate = false; MPPlayMissionGui.allChats = []; // MultiplayerLevelSelectGui.custSelected = false; } } public static function checkPacketTimeout(dt:Float) { if (!Net.isMP) return; static var accum = 0.0; static var wsAccum = 0.0; accum += dt; wsAccum += dt; if (accum > 1.0) { accum = 0; var t = Console.time(); for (dc => cc in clients) { if (cc is ClientConnection) { var conn = cast(cc, ClientConnection); if (Net.isHost && conn.state == LOADING) continue; if (Net.isClient && MarbleGame.instance.world != null && !MarbleGame.instance.world._ready) continue; // We still loading, don't disconnect if (conn.needsTimeoutWarn(t)) { conn.didWarnTimeout = true; if (Net.isClient) { NetCommands.requestPing(); } if (Net.isHost) { NetCommands.pingClient(cc, t); } } if (conn.needsTimeoutKick(t)) { if (Net.isHost) { dc.close(); } if (Net.isClient) { disconnect(); if (MarbleGame.instance.world != null) { MarbleGame.instance.quitMission(); } if (!(MarbleGame.canvas.content is MPMessageGui)) { var loadGui = new MPMessageGui("Error", "Timed out"); MarbleGame.canvas.setContent(loadGui); } } } } } } if (wsAccum >= 15.0) { wsAccum = 0; if (Net.isHost) { if (MasterServerClient.instance != null) MasterServerClient.instance.sendServerInfo(serverInfo); // Heartbeat else MasterServerClient.connectToMasterServer(() -> { MasterServerClient.instance.sendServerInfo(serverInfo); // Heartbeat }); } if (Net.isClient) { if (MasterServerClient.instance != null) MasterServerClient.instance.heartBeat(); else MasterServerClient.connectToMasterServer(() -> { MasterServerClient.instance.heartBeat(); }); } } } static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel, dcu:RTCDataChannel) { if (!Net.isMP) { c.close(); return; } var clientId = allocateClientId(); if (clientId == -1) { c.close(); return; // Failed to allocate ID } var cc = new ClientConnection(clientId, c, dc, dcu); clients.set(c, cc); clientIdMap[clientId] = clients[c]; cc.lastRecvTime = Console.time(); // So it doesnt get timed out var closing = false; var onMessage = (dc:RTCDataChannel, msgBytes:haxe.io.Bytes) -> { onPacketReceived(cc, c, dc, new InputBitStream(msgBytes)); } var onClosed = () -> { if (closing) return; closing = true; clients.remove(c); onClientLeave(cc); } var onError = (msg:String) -> { if (closing) return; closing = true; clients.remove(c); Console.log('Client ${cc.id} errored out due to: ${msg}'); onClientLeave(cc); } dc.onMessage = (msgBytes) -> { onMessage(dc, msgBytes); } dc.onClosed = () -> { onClosed(); } dc.onError = (msg) -> { onError(msg); } dcu.onMessage = (msgBytes) -> { onMessage(dcu, msgBytes); } dcu.onClosed = () -> { onClosed(); } dcu.onError = (msg) -> { onError(msg); } var b = haxe.io.Bytes.alloc(2); b.set(0, ClientIdAssign); b.set(1, clientId); dc.sendBytes(b); Console.log("Client has connected!"); // Send the ping packet to calculcate the RTT var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count cast(clients[c], ClientConnection).pingSendTime = Console.time(); dc.sendBytes(b); Console.log("Sending ping packet!"); // AudioManager.playSound(ResourceLoader.getAudio("data/sound/spawn_alternate.wav").resource); serverInfo.players = 1; for (k => v in clients) { // Recount serverInfo.players++; } MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the new player if (MarbleGame.canvas.content is MPPlayMissionGui) { cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); } if (MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1] is MPPreGameDlg) { cast(MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1], MPPreGameDlg).updatePlayerList(); } } static function onConnectedToServer() { Console.log("Connected to the server!"); // Send the ping packet to calculate the RTT var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count cast(clients[client], ClientConnection).pingSendTime = Console.time(); clientDatachannel.sendBytes(b); Console.log("Sending ping packet!"); } static function onClientLeave(cc:ClientConnection) { if (!Net.isMP || cc == null) return; NetCommands.clientDisconnected(cc.id); if (cc.id != 0) { freeClientId(cc.id); } serverInfo.players = 1; for (k => v in clients) { // Recount serverInfo.players++; } MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the player leave // AudioManager.playSound(ResourceLoader.getAudio("data/sound/infotutorial.wav").resource); if (MarbleGame.canvas.content is MPPlayMissionGui) { cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); } if (MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1] is MPPreGameDlg) { cast(MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1], MPPreGameDlg).updatePlayerList(); } } static function onClientHandshakeComplete(conn:ClientConnection) { // Send our current mission to connecting client // if (MultiplayerLevelSelectGui.custSelected) { // NetCommands.setLobbyCustLevelNameClient(conn, MultiplayerLevelSelectGui.custPath); // } else { NetCommands.sendServerSettingsClient(conn, Settings.serverSettings.name, Settings.serverSettings.description, Settings.serverSettings.quickRespawn, Settings.serverSettings.forceSpectators, Settings.serverSettings.competitiveMode, Settings.serverSettings.oldSpawns); NetCommands.setLobbyLevelIndexClient(conn, MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic); // } if (serverInfo.state == "PLAYING") { // We initiated the game, directly add in the marble // if (MultiplayerLevelSelectGui.custSelected) { // NetCommands.playCustomLevelMidJoinClient(conn, MultiplayerLevelSelectGui.custPath); // } else NetCommands.playLevelMidJoinClient(conn, MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic); MarbleGame.instance.world.addJoiningClient(conn, () -> {}); var playerInfoBytes = sendPlayerInfosBytes(); for (dc => cc in clients) { if (cc != conn) { cc.sendBytes(playerInfoBytes); NetCommands.addMidGameJoinMarbleClient(cc, conn.id); } } } if (serverInfo.state == "LOBBY") { // Connect client to lobby NetCommands.enterLobbyClient(conn); } } public static function sendPlayerInfosBytes() { var b = new haxe.io.BytesOutput(); b.writeByte(PlayerInfo); var cnt = 0; for (c in clientIdMap) cnt++; b.writeByte(cnt + 1); // all + host for (c => v in clientIdMap) { b.writeByte(c); b.writeByte(v.lobbyReady ? 1 : 0); b.writeByte(v.platform); b.writeByte(v.marbleId); b.writeByte(v.marbleCatId); b.writeByte(v.spectator ? 1 : 0); var name = v.getName(); b.writeByte(name.length); for (i in 0...name.length) { b.writeByte(StringTools.fastCodeAt(name, i)); } } // Write host data b.writeByte(0); b.writeByte(Net.lobbyHostReady ? 1 : 0); b.writeByte(getPlatform()); b.writeByte(Settings.optionsSettings.marbleIndex); b.writeByte(Settings.optionsSettings.marbleCategoryIndex); b.writeByte(Net.hostSpectate ? 1 : 0); var name = Settings.highscoreName; b.writeByte(name.length); for (i in 0...name.length) { b.writeByte(StringTools.fastCodeAt(name, i)); } return b.getBytes(); } static function onPacketReceived(conn:ClientConnection, c:RTCPeerConnection, dc:RTCDataChannel, input:InputBitStream) { if (!Net.isMP) return; // only for MP conn.lastRecvTime = Console.time(); conn.didWarnTimeout = false; var packetType = input.readByte(); switch (packetType) { case NetCommand: NetCommands.readPacket(input); case ClientIdAssign: clientId = input.readByte(); // 8 bit client id, hopefully we don't exceed this Console.log('Client ID set to ${clientId}'); NetCommands.setPlayerData(clientId, Settings.highscoreName, Settings.optionsSettings.marbleIndex, Settings.optionsSettings.marbleCategoryIndex, false); // Send our player name to the server NetCommands.transmitPlatform(clientId, getPlatform()); // send our platform too case Ping: var pingLeft = input.readByte(); Console.log("Got ping packet!"); var b = haxe.io.Bytes.alloc(2); b.set(0, PingBack); b.set(1, pingLeft); dc.sendBytes(b); case PingBack: var pingLeft = input.readByte(); Console.log("Got pingback packet!"); var now = Console.time(); conn._rttRecords.push((now - conn.pingSendTime)); if (pingLeft > 0) { conn.pingSendTime = now; var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, pingLeft - 1); dc.sendBytes(b); } else { for (r in conn._rttRecords) conn.rtt += r; conn.rtt /= conn._rttRecords.length; Console.log('Got RTT ${conn.rtt} for client ${conn.id}'); if (Net.isHost) { var b = sendPlayerInfosBytes(); for (cc in clients) { cc.sendBytes(b); } onClientHandshakeComplete(conn); } } case MarbleUpdate: var marbleUpdatePacket = new MarbleUpdatePacket(); marbleUpdatePacket.deserialize(input); var cc = marbleUpdatePacket.clientId; var client = cc != Net.clientId ? clientIdMap[cc] : Net.clientConnection; client.pingTicks = marbleUpdatePacket.pingTicks; if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { var m = MarbleGame.instance.world.lastMoves; m.enqueue(marbleUpdatePacket); } case MarbleMove: if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { var movePacket = new MarbleMovePacket(); movePacket.deserialize(input); var cc = clientIdMap[movePacket.clientId]; if (cc.state == GAME) { var startRecvId = cc.moveManager.getQueueSize(); for (move in movePacket.moves) cc.queueMove(move); var endRecvId = cc.moveManager.getQueueSize(); cc.pingTicks = movePacket.moves.length - (endRecvId - startRecvId); } } case PowerupPickup: var powerupPickupPacket = new PowerupPickupPacket(); powerupPickupPacket.deserialize(input); if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { var m = @:privateAccess MarbleGame.instance.world.powerupPredictions; m.acknowledgePowerupPickup(powerupPickupPacket, MarbleGame.instance.world.timeState, clientConnection.moveManager.getQueueSize()); } case GemSpawn: var gemSpawnPacket = new GemSpawnPacket(); gemSpawnPacket.deserialize(input); if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { MarbleGame.instance.world.spawnHuntGemsClientSide(gemSpawnPacket.gemIds, gemSpawnPacket.expireds); @:privateAccess MarbleGame.instance.world.gemPredictions.acknowledgeGemSpawn(gemSpawnPacket); } case GemPickup: var gemPickupPacket = new GemPickupPacket(); gemPickupPacket.deserialize(input); if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { @:privateAccess MarbleGame.instance.world.playGui.incrementPlayerScore(gemPickupPacket.clientId, gemPickupPacket.scoreIncr); @:privateAccess MarbleGame.instance.world.gemPredictions.acknowledgeGemPickup(gemPickupPacket); } case PlayerInfo: var count = input.readByte(); var newP = false; for (i in 0...count) { var id = input.readByte(); var cready = input.readByte() == 1; var platform = input.readByte(); var marble = input.readByte(); var marbleCat = input.readByte(); var cspectator = input.readByte() == 1; if (id != 0 && id != Net.clientId && !clientIdMap.exists(id)) { Console.log('Adding ghost connection ${id}'); addGhost(id); newP = true; } var nameLength = input.readByte(); var name = ""; for (j in 0...nameLength) { name += String.fromCharCode(input.readByte()); } if (clientIdMap.exists(id)) { clientIdMap[id].setName(name); clientIdMap[id].setMarbleId(marble, marbleCat); clientIdMap[id].lobbyReady = cready; clientIdMap[id].platform = platform; clientIdMap[id].spectator = cspectator; } if (Net.clientId == id) { Net.lobbyClientReady = cready; Net.clientSpectate = cspectator; } } if (MarbleGame.canvas.content is MPPlayMissionGui) { cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); } if (MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1] is MPPreGameDlg) { cast(MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1], MPPreGameDlg).updatePlayerList(); } case ScoreBoardInfo: var scoreboardPacket = new ScoreboardPacket(); scoreboardPacket.deserialize(input); if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { @:privateAccess MarbleGame.instance.world.playGui.updatePlayerScores(scoreboardPacket); } case ExplodableUpdate: var explodableUpdatePacket = new ExplodableUpdatePacket(); explodableUpdatePacket.deserialize(input); if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { @:privateAccess MarbleGame.instance.world.explodablePredictions.acknowledgeExplodableUpdate(explodableUpdatePacket); } case _: Console.log("unknown command: " + packetType); } } static function allocateClientId() { for (id in 0...32) { if (Net.clientIdAllocs & (1 << id) == 0) { Net.clientIdAllocs |= (1 << id); return id; } } return -1; } static function freeClientId(id:Int) { Net.clientIdAllocs &= ~(1 << id); } public static function sendPacketToAll(packetData:OutputBitStream) { var bytes = packetData.getBytes(); for (c => v in clients) { v.sendBytes(bytes); } } public static function sendPacketToIngame(packetData:OutputBitStream) { var bytes = packetData.getBytes(); for (c => v in clients) { if (v.state == GAME) v.sendBytes(bytes); } } public static function sendPacketToHost(packetData:OutputBitStream) { if (clientDatachannel.state == Open) { var bytes = packetData.getBytes(); clientDatachannel.sendBytes(bytes); } } public static function sendPacketToHostUnreliable(packetData:OutputBitStream) { if (clientDatachannelUnreliable.state == Open) { var bytes = packetData.getBytes(); clientDatachannelUnreliable.sendBytes(bytes); } } public static function sendPacketToClient(client:GameConnection, packetData:OutputBitStream) { var bytes = packetData.getBytes(); client.sendBytes(bytes); } public static function addDummyConnection() { if (Net.isHost) { addGhost(Net.clientId++); } } public static inline function getPlatform() { #if js return NetPlatform.Web; #end #if hl #if MACOS_BUNDLE return NetPlatform.MacOS; #else #if android return NetPlatform.Android; #else return NetPlatform.PC; #end #end #end } }