From 1fef9a5bdc4f8a935fb380d88df7fdcfabc2f3fe Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:41:13 +0530 Subject: [PATCH] make client predict other marbles too --- server/Signalling.hx | 1 + src/Marble.hx | 121 ++++++++++++++++--- src/MarbleWorld.hx | 48 ++++++-- src/collision/SphereCollisionEntity.hx | 8 +- src/net/MoveManager.hx | 157 +++++++++++++++++++++++++ src/net/Net.hx | 86 +++++++++----- 6 files changed, 368 insertions(+), 53 deletions(-) create mode 100644 src/net/MoveManager.hx diff --git a/server/Signalling.hx b/server/Signalling.hx index c7376b08..1c3cd5d0 100644 --- a/server/Signalling.hx +++ b/server/Signalling.hx @@ -21,6 +21,7 @@ class SignallingHandler extends WebSocketHandler { case StrMessage(content): var conts = Json.parse(content); if (conts.type == "connect") { + trace('Connect received'); var other = clients.find(x -> x != this); other.send(Json.stringify(conts.sdpObj)); } diff --git a/src/Marble.hx b/src/Marble.hx index 17dd5768..71fff3de 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1,5 +1,7 @@ package src; +import net.MoveManager; +import net.MoveManager.NetMove; import shaders.marble.CrystalMarb; import shaders.marble.ClassicMarb; import shapes.HelicopterImage; @@ -286,6 +288,8 @@ class Marble extends GameObject { public var cubemapRenderer:CubemapRenderer; var connection:net.Net.ClientConnection; + var moveMotionDir:Vector; + var isNetUpdate:Bool = false; public function new() { super(); @@ -529,7 +533,7 @@ class Marble extends GameObject { var motiondir = new Vector(0, -1, 0); if (level.isReplayingMovement) return level.currentInputMoves[1].marbleAxes; - if (this.controllable) { + if (this.controllable && !this.isNetUpdate) { motiondir.transform(Matrix.R(0, 0, camera.CameraYaw)); motiondir.transform(level.newOrientationQuat.toMatrix()); var updir = this.level.currentUp; @@ -539,7 +543,11 @@ class Marble extends GameObject { motiondir = updir.cross(sidedir); return [sidedir, motiondir, updir]; } else { - return [new Vector(1, 0, 0), motiondir, new Vector(0, 0, 1)]; + if (moveMotionDir != null) + motiondir = moveMotionDir; + var updir = this.level.currentUp; + var sidedir = motiondir.cross(updir); + return [sidedir, motiondir, updir]; } } @@ -568,7 +576,7 @@ class Marble extends GameObject { for (contact in contacts) { if (contact.force != 0 && !forceObjects.contains(contact.otherObject)) { if (contact.otherObject is RoundBumper) { - if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav")) { + if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav") && !this.isNetUpdate) { AudioManager.playSound(ResourceLoader.getResource("data/sound/bumperding1.wav", ResourceLoader.getAudio, this.soundResources)); playedSounds.push("data/sound/bumperding1.wav"); } @@ -808,7 +816,10 @@ class Marble extends GameObject { } if (sv < this._jumpImpulse) { this.velocity.load(this.velocity.add(bestContact.normal.multiply((this._jumpImpulse - sv)))); - if (!level.isReplayingMovement && !playedSounds.contains("data/sound/jump.wav")) { + if (!level.isReplayingMovement + && !playedSounds.contains("data/sound/jump.wav") + && !this.isNetUpdate + && this.controllable) { AudioManager.playSound(ResourceLoader.getResource("data/sound/jump.wav", ResourceLoader.getAudio, this.soundResources)); playedSounds.push("data/sound/jump.wav"); } @@ -888,7 +899,7 @@ class Marble extends GameObject { } function bounceEmitter(speed:Float, normal:Vector) { - if (!this.controllable || level.isReplayingMovement) + if (!this.controllable || level.isReplayingMovement || this.isNetUpdate) return; if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) { this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData, @@ -923,7 +934,7 @@ class Marble extends GameObject { } function playBoundSound(time:Float, contactVel:Float) { - if (level.isReplayingMovement) + if (level.isReplayingMovement || this.isNetUpdate) return; if (minVelocityBounceSoft <= contactVel) { var hardBounceSpeed = minVelocityBounceHard; @@ -1607,20 +1618,102 @@ class Marble extends GameObject { } // MP Only Functions - public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { - var move:Move = null; - if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) { - move = recordMove(); + + public function packUpdate(move:NetMove) { + var b = new haxe.io.BytesOutput(); + b.writeByte(NetPacketType.MarbleUpdate); + b.writeUInt16(connection != null ? connection.id : 0); + MoveManager.packMove(move, b); + b.writeUInt16(this.level.ticks); // So we can get the clients to do stuff about it + b.writeFloat(this.newPos.x); + b.writeFloat(this.newPos.y); + b.writeFloat(this.newPos.z); + b.writeFloat(this.velocity.x); + b.writeFloat(this.velocity.y); + b.writeFloat(this.velocity.z); + b.writeFloat(this.omega.x); + b.writeFloat(this.omega.y); + b.writeFloat(this.omega.z); + return b.getBytes(); + } + + public function unpackUpdate(b:haxe.io.BytesInput) { + // Assume packet header is already read + var serverMove = MoveManager.unpackMove(b); + if (Net.isClient) + Net.clientConnection.moveManager.acknowledgeMove(serverMove.id); + var serverTicks = b.readUInt16(); + this.oldPos = this.newPos; + this.newPos = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + this.collider.transform.setPosition(this.newPos); + this.velocity = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + this.omega = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + + // Apply the moves we have queued + if (Net.isClient) { + this.isNetUpdate = true; + if (this.controllable) { + for (move in @:privateAccess Net.clientConnection.moveManager.queuedMoves) { + moveMotionDir = move.motionDir; + advancePhysics(move.timeState, move.move, this.level.collisionWorld, this.level.pathedInteriors); + } + } else { + var tickDiff = this.level.ticks - serverTicks; + if (tickDiff > 0) { + var timeState = this.level.timeState.clone(); + timeState.dt = 0.032; + var m = serverMove.move; + moveMotionDir = serverMove.motionDir; + for (o in 0...tickDiff) { + advancePhysics(timeState, m, this.level.collisionWorld, this.level.pathedInteriors); + } + } + } + this.isNetUpdate = false; } - if (!this.controllable && this.connection != null) { - move = new Move(); - move.d = new Vector(0, 0); + } + + public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array, packets:Array) { + var move:NetMove = null; + if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) { + if (Net.isClient) { + var axis = getMarbleAxis()[1]; + move = Net.clientConnection.moveManager.recordMove(axis, timeState); + } else if (Net.isHost) { + var axis = getMarbleAxis()[1]; + var innerMove = recordMove(); + move = new NetMove(innerMove, axis, timeState, 65535); + } + } + var moveId = 65535; + if (!this.controllable && this.connection != null && Net.isHost) { + var nextMove = this.connection.moveManager.getNextMove(); + if (nextMove == null) { + var axis = getMarbleAxis()[1]; + var innerMove = new Move(); + innerMove.d = new Vector(0, 0); + move = new NetMove(innerMove, axis, timeState, 65535); + } else { + move = nextMove; + moveMotionDir = nextMove.motionDir; + moveId = nextMove.id; + } + } + if (move == null) { + var axis = getMarbleAxis()[1]; + var innerMove = new Move(); + innerMove.d = new Vector(0, 0); + move = new NetMove(innerMove, axis, timeState, 65535); } playedSounds = []; - advancePhysics(timeState, move, collisionWorld, pathedInteriors); + advancePhysics(timeState, move.move, collisionWorld, pathedInteriors); physicsAccumulator = 0; + + if (Net.isHost) { + packets.push(packUpdate(move)); + } } public function updateClient(timeState:TimeState, pathedInteriors:Array) { diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 7e75f0d9..2dda41f4 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,6 @@ package src; +import net.MoveManager; import net.NetCommands; import net.Net; import net.Net.ClientConnection; @@ -195,6 +196,9 @@ class MarbleWorld extends Scheduler { public var startRealTime:Float = 0; public var multiplayerStarted:Bool = false; + public var ticks:Int = 0; // How many 32ms ticks have happened + + var tickAccumulator:Float = 0.0; var clientMarbles:Map = []; @@ -233,6 +237,10 @@ class MarbleWorld extends Scheduler { this.rewindManager = new RewindManager(this); this.inputRecorder = new InputRecorder(this); this.isMultiplayer = multiplayer; + if (this.isMultiplayer) { + isRecording = false; + isWatching = false; + } // Set the network RNG for hunt if (isMultiplayer && gameMode is modes.HuntMode && Net.isHost) { @@ -1055,16 +1063,13 @@ class MarbleWorld extends Scheduler { return; } - if (Key.isPressed(Key.T)) { - rollback(0.4); - } - var realDt = dt; if ((Key.isDown(Settings.controlsSettings.rewind) || MarbleGame.instance.touchInput.rewindButton.pressed || Gamepad.isDown(Settings.gamepadSettings.rewind)) && Settings.optionsSettings.rewindEnabled + && !this.isMultiplayer && !this.isWatching && this.finishTime == null) { this.rewinding = true; @@ -1072,7 +1077,8 @@ class MarbleWorld extends Scheduler { if ((Key.isReleased(Settings.controlsSettings.rewind) || !MarbleGame.instance.touchInput.rewindButton.pressed || Gamepad.isReleased(Settings.gamepadSettings.rewind)) - && this.rewinding) { + && this.rewinding + && !this.isMultiplayer) { if (this.isRecording) { this.replay.spliceReplay(timeState.currentAttemptTime); } @@ -1207,9 +1213,35 @@ class MarbleWorld extends Scheduler { } ProfilerUI.measure("updateMarbles"); - marble.update(timeState, collisionWorld, this.pathedInteriors); - for (client => marble in clientMarbles) { + if (this.isMultiplayer) { + tickAccumulator += timeState.dt; + while (tickAccumulator >= 0.032) { + var fixedDt = timeState.clone(); + fixedDt.dt = 0.032; + tickAccumulator -= 0.032; + var packets = []; + marble.updateServer(fixedDt, collisionWorld, pathedInteriors, packets); + for (client => marble in clientMarbles) { + marble.updateServer(fixedDt, collisionWorld, pathedInteriors, packets); + } + if (Net.isHost) { + for (client => marble in clientMarbles) { // Oh no! + for (packet in packets) { + client.datachannel.sendBytes(packet); + } + } + } + ticks++; + } + marble.updateClient(timeState, this.pathedInteriors); + for (client => marble in clientMarbles) { + marble.updateClient(timeState, this.pathedInteriors); + } + } else { marble.update(timeState, collisionWorld, this.pathedInteriors); + for (client => marble in clientMarbles) { + marble.update(timeState, collisionWorld, this.pathedInteriors); + } } _cubemapNeedsUpdate = true; Renderer.dirtyBuffers = true; @@ -1243,7 +1275,7 @@ class MarbleWorld extends Scheduler { } } - if (!this.rewinding && Settings.optionsSettings.rewindEnabled) + if (!this.rewinding && Settings.optionsSettings.rewindEnabled && !this.isMultiplayer) this.rewindManager.recordFrame(); if (!this.isReplayingMovement) { diff --git a/src/collision/SphereCollisionEntity.hx b/src/collision/SphereCollisionEntity.hx index 4909a42f..d373171b 100644 --- a/src/collision/SphereCollisionEntity.hx +++ b/src/collision/SphereCollisionEntity.hx @@ -89,7 +89,7 @@ class SphereCollisionEntity extends CollisionEntity { contact.collider = this; contact.friction = 1; contact.restitution = 1; - contact.velocity = this.velocity; + contact.velocity = this.velocity.clone(); contact.otherObject = this.go; contact.point = position.add(normDist); contact.normal = normDist.multiply(-1); @@ -102,9 +102,9 @@ class SphereCollisionEntity extends CollisionEntity { othercontact.collider = collisionEntity; othercontact.friction = 1; othercontact.restitution = 1; - othercontact.velocity = this.velocity; - othercontact.point = thispos.add(position).multiply(0.5); - othercontact.normal = contact.point.sub(position).normalized(); + othercontact.velocity = collisionEntity.velocity.clone(); + othercontact.point = thispos.sub(normDist); + othercontact.normal = normDist.clone(); othercontact.contactDistance = contact.point.distance(position); othercontact.force = 0; othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal)); diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx new file mode 100644 index 00000000..673421b3 --- /dev/null +++ b/src/net/MoveManager.hx @@ -0,0 +1,157 @@ +package net; + +import src.TimeState; +import src.Console; +import net.Net.ClientConnection; +import net.Net.NetPacketType; +import src.MarbleWorld; +import src.Marble.Move; +import h3d.Vector; +import src.Gamepad; +import src.Settings; +import hxd.Key; +import src.MarbleGame; +import src.Util; + +@:publicFields +class NetMove { + var motionDir:Vector; + var move:Move; + var id:Int; + var timeState:TimeState; + + public function new(move:Move, motionDir:Vector, timeState:TimeState, id:Int) { + this.move = move; + this.motionDir = motionDir; + this.id = id; + this.timeState = timeState; + } +} + +class MoveManager { + var connection:ClientConnection; + var queuedMoves:Array; + var nextMoveId:Int; + var lastMove:NetMove; + var lastAckMoveId:Int = -1; + + static var maxMoves = 45; // Taken from Torque + + public function new(connection:ClientConnection) { + queuedMoves = []; + nextMoveId = 0; + this.connection = connection; + } + + public function recordMove(motionDir:Vector, timeState:TimeState) { + if (queuedMoves.length >= maxMoves) + return queuedMoves[queuedMoves.length - 1]; + var move = new Move(); + move.d = new Vector(); + move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis); + move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis); + if (Key.isDown(Settings.controlsSettings.forward)) { + move.d.x -= 1; + } + if (Key.isDown(Settings.controlsSettings.backward)) { + move.d.x += 1; + } + if (Key.isDown(Settings.controlsSettings.left)) { + move.d.y += 1; + } + if (Key.isDown(Settings.controlsSettings.right)) { + move.d.y -= 1; + } + if (Key.isDown(Settings.controlsSettings.jump) + || MarbleGame.instance.touchInput.jumpButton.pressed + || Gamepad.isDown(Settings.gamepadSettings.jump)) { + move.jump = true; + } + if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup)) + || (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed) + || Gamepad.isDown(Settings.gamepadSettings.powerup)) { + move.powerup = true; + } + if (MarbleGame.instance.touchInput.movementInput.pressed) { + move.d.y = -MarbleGame.instance.touchInput.movementInput.value.x; + move.d.x = MarbleGame.instance.touchInput.movementInput.value.y; + } + + var netMove = new NetMove(move, motionDir, timeState.clone(), nextMoveId++); + queuedMoves.push(netMove); + + if (nextMoveId >= 65535) // 65535 is reserved for null move + nextMoveId = 0; + + var b = new haxe.io.BytesOutput(); + b.writeByte(NetPacketType.MarbleMove); + b.writeUInt16(Net.clientId); + + Net.sendPacketToHost(packMove(netMove, b)); + + return netMove; + } + + public static function packMove(m:NetMove, b:haxe.io.BytesOutput) { + b.writeUInt16(m.id); + b.writeFloat(m.move.d.x); + b.writeFloat(m.move.d.y); + var flags = 0; + if (m.move.jump) + flags |= 1; + if (m.move.powerup) + flags |= 2; + b.writeByte(flags); + b.writeFloat(m.motionDir.x); + b.writeFloat(m.motionDir.y); + b.writeFloat(m.motionDir.z); + return b; + } + + public static function unpackMove(b:haxe.io.BytesInput) { + var moveId = b.readUInt16(); + var move = new Move(); + move.d = new Vector(); + move.d.x = b.readFloat(); + move.d.y = b.readFloat(); + var flags = b.readByte(); + move.jump = (flags & 1) != 0; + move.powerup = (flags & 2) != 0; + var motionDir = new Vector(); + motionDir.x = b.readFloat(); + motionDir.y = b.readFloat(); + motionDir.z = b.readFloat(); + var netMove = new NetMove(move, motionDir, MarbleGame.instance.world.timeState.clone(), moveId); + return netMove; + } + + public function queueMove(m:NetMove) { + queuedMoves.push(m); + } + + public function getNextMove() { + if (queuedMoves.length == 0) + return lastMove; + else { + lastMove = queuedMoves[0]; + queuedMoves.shift(); + return lastMove; + } + } + + public function acknowledgeMove(m:Int) { + if (m == 65535 || m == -1) + return; + if (m <= lastAckMoveId) + return; // Already acked + if (queuedMoves.length == 0) + return; + while (m != queuedMoves[0].id) { + trace('Ignoring move ${queuedMoves[0].id}, need ${m}'); + queuedMoves.shift(); + } + if (m == queuedMoves[0].id) + queuedMoves.shift(); + lastAckMoveId = m; + } +} diff --git a/src/net/Net.hx b/src/net/Net.hx index e4a52483..762f5319 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -6,6 +6,8 @@ import datachannel.RTCDataChannel; import hx.ws.WebSocket; import src.Console; import net.NetCommands; +import src.MarbleGame; +import hx.ws.Types.MessageType; enum abstract GameplayState(Int) from Int to Int { var UNKNOWN; @@ -19,6 +21,8 @@ enum abstract NetPacketType(Int) from Int to Int { var NetCommand; var Ping; var PingBack; + var MarbleUpdate; + var MarbleMove; } @:publicFields @@ -27,6 +31,7 @@ class ClientConnection { var socket:RTCPeerConnection; var datachannel:RTCDataChannel; var state:GameplayState; + var moveManager:MoveManager; var rtt:Float; var pingSendTime:Float; var _rttRecords:Array = []; @@ -37,6 +42,7 @@ class ClientConnection { this.id = id; this.state = GameplayState.LOBBY; this.rtt = 0; + this.moveManager = new MoveManager(this); } public function ready() { @@ -60,6 +66,7 @@ class Net { public static var networkRNG:Float; public static var clients:Map = []; public static var clientIdMap:Map = []; + public static var clientConnection:ClientConnection; public static function hostServer() { // host = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); @@ -76,41 +83,45 @@ class Net { switch (m) { case StrMessage(content): var conts = Json.parse(content); - var peer = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); + var peer = new RTCPeerConnection(["stun:stun.l.google.com:19302"], "0.0.0.0"); peer.setRemoteDescription(conts.sdp, conts.type); + addClient(peer); - var candidates = []; - peer.onLocalCandidate = (c) -> { - if (c != "") - candidates.push('a=${c}'); - } - peer.onGatheringStateChange = (s) -> { - if (s == RTC_GATHERING_COMPLETE) { - var sdpObj = StringTools.trim(peer.localDescription); - sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); - masterWs.send(Json.stringify({ - type: "connect", - sdpObj: { - sdp: sdpObj, - type: "offer" - } - })); - } - } - peer.onDataChannel = (dc) -> { - onClientConnect(peer, dc); - }; - case _: {} + case BytesMessage(content): {} } } isMP = true; } + static function addClient(peer:RTCPeerConnection) { + var candidates = []; + peer.onLocalCandidate = (c) -> { + if (c != "") + candidates.push('a=${c}'); + } + peer.onGatheringStateChange = (s) -> { + if (s == RTC_GATHERING_COMPLETE) { + var sdpObj = StringTools.trim(peer.localDescription); + sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); + masterWs.send(Json.stringify({ + type: "connect", + sdpObj: { + sdp: sdpObj, + type: "answer" + } + })); + } + } + peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> { + onClientConnect(peer, dc); + } + } + public static function joinServer(connectedCb:() -> Void) { masterWs = new WebSocket("ws://localhost:8080"); - client = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); + client = new RTCPeerConnection(["stun:stun.l.google.com:19302"], "0.0.0.0"); var candidates = []; client.onLocalCandidate = (c) -> { @@ -119,6 +130,7 @@ class Net { } client.onGatheringStateChange = (s) -> { if (s == RTC_GATHERING_COMPLETE) { + Console.log("Local Description Set!"); var sdpObj = StringTools.trim(client.localDescription); sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); masterWs.send(Json.stringify({ @@ -134,6 +146,7 @@ class Net { masterWs.onmessage = (m) -> { switch (m) { case StrMessage(content): + Console.log("Remote Description Received!"); var conts = Json.parse(content); client.setRemoteDescription(conts.sdp, conts.type); case _: {} @@ -142,8 +155,10 @@ class Net { clientDatachannel = client.createDatachannel("mp"); clientDatachannel.onOpen = (n) -> { + Console.log("Successfully connected!"); clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0 clientIdMap[0] = clients[client]; + clientConnection = clients[client]; onConnectedToServer(); haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation } @@ -172,7 +187,7 @@ class Net { var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count - clients[c].pingSendTime = Sys.time(); + clients[c].pingSendTime = Console.time(); dc.sendBytes(b); Console.log("Sending ping packet!"); } @@ -183,7 +198,7 @@ class Net { var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count - clients[client].pingSendTime = Sys.time(); + clients[client].pingSendTime = Console.time(); clientDatachannel.sendBytes(b); Console.log("Sending ping packet!"); } @@ -210,7 +225,7 @@ class Net { var pingLeft = input.readByte(); Console.log("Got pingback packet!"); var conn = clients[c]; - var now = Sys.time(); + var now = Console.time(); conn._rttRecords.push((now - conn.pingSendTime)); if (pingLeft > 0) { conn.pingSendTime = now; @@ -225,6 +240,23 @@ class Net { Console.log('Got RTT ${conn.rtt} for client ${conn.id}'); } + case MarbleUpdate: + var marbleClientId = input.readUInt16(); + if (marbleClientId == clientId) { + if (MarbleGame.instance.world != null) + MarbleGame.instance.world.marble.unpackUpdate(input); + } else { + var cc = clientIdMap[marbleClientId]; + if (MarbleGame.instance.world != null) + @:privateAccess MarbleGame.instance.world.clientMarbles[cc].unpackUpdate(input); + } + + case MarbleMove: + var marbleClientId = input.readUInt16(); + var cc = clientIdMap[marbleClientId]; + var m = MoveManager.unpackMove(input); + cc.moveManager.queueMove(m); + case _: trace("unknown command: " + packetType); }