diff --git a/src/Marble.hx b/src/Marble.hx index 85b68e3c..477ab6d0 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1728,6 +1728,10 @@ class Marble extends GameObject { // MP Only Functions + public function clearNetFlags() { + this.netFlags = 0; + } + public function packUpdate(move:NetMove, timeState:TimeState) { var b = new OutputBitStream(); b.writeByte(NetPacketType.MarbleUpdate); @@ -1747,7 +1751,6 @@ class Marble extends GameObject { marbleUpdate.powerUpId = this.heldPowerup != null ? this.heldPowerup.netIndex : 0x1FF; marbleUpdate.netFlags = this.netFlags; marbleUpdate.gravityDirection = this.currentUp; - this.netFlags = 0; marbleUpdate.serialize(b); return b.getBytes(); } diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 46003771..8755383d 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,8 @@ package src; +import net.NetPacket.GemSpawnPacket; +import net.BitStream.OutputBitStream; +import net.MasterServerClient; import gui.MarblePickerGui; import gui.MultiplayerLevelSelectGui; import collision.CollisionPool; @@ -522,9 +525,19 @@ class MarbleWorld extends Scheduler { cc++; if (Net.isHost && cc == 0) { allClientsReady(); + Net.serverInfo.state = "PLAYING"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state } } + public function addJoiningClient(cc:GameConnection, onAdded:() -> Void) { + this.initMarble(cc, () -> { + var addedMarble = clientMarbles.get(cc); + this.restart(addedMarble); // spawn it + onAdded(); + }); + } + public function restartMultiplayerState() { if (this.isMultiplayer) { serverStartTicks = 0; @@ -654,7 +667,7 @@ class MarbleWorld extends Scheduler { AudioManager.playSound(ResourceLoader.getResource('data/sound/spawn_alternate.wav', ResourceLoader.getAudio, this.soundResources)); - if (marble == this.marble) + if (!this.isMultiplayer) this.gameMode.onRestart(); if (Net.isClient) { this.gameMode.onClientRestart(); @@ -711,6 +724,7 @@ class MarbleWorld extends Scheduler { public function allClientsReady() { NetCommands.setStartTicks(this.timeState.ticks); + this.gameMode.onRestart(); } public function updateGameState() { @@ -1105,6 +1119,29 @@ class MarbleWorld extends Scheduler { } } + public function getWorldStateForClientJoin() { + var packets = []; + // First, gem spawn packet + var bs = new OutputBitStream(); + bs.writeByte(GemSpawn); + var packet = new GemSpawnPacket(); + + var hunt = cast(this.gameMode, HuntMode); + var activeGemIds = []; + for (gemId in @:privateAccess hunt.activeGemSpawnGroup) { + if (@:privateAccess hunt.gemSpawnPoints[gemId].gem != null && @:privateAccess !hunt.gemSpawnPoints[gemId].gem.pickedUp) { + activeGemIds.push(gemId); + } + } + packet.gemIds = activeGemIds; + packet.serialize(bs); + packets.push(bs.getBytes()); + + // Powerup states + + return packets; + } + public function applyReceivedMoves() { var needsPrediction = 0; if (!lastMoves.ourMoveApplied) { @@ -1212,8 +1249,10 @@ class MarbleWorld extends Scheduler { pw.lastPickUpTime = powerupPredictions.getState(pw.netIndex); } var huntMode:HuntMode = cast this.gameMode; - for (activeGem in @:privateAccess huntMode.activeGemSpawnGroup) { - huntMode.setGemHiddenStatus(activeGem, gemPredictions.getState(activeGem)); + if (@:privateAccess huntMode.activeGemSpawnGroup != null) { + for (activeGem in @:privateAccess huntMode.activeGemSpawnGroup) { + huntMode.setGemHiddenStatus(activeGem, gemPredictions.getState(activeGem)); + } } // } // } @@ -1519,7 +1558,7 @@ class MarbleWorld extends Scheduler { for (client => marble in clientMarbles) { otherMoves.push(marble.updateServer(fixedDt, collisionWorld, pathedInteriors)); } - if (myMove != null) { + if (myMove != null && Net.isClient) { this.predictions.storeState(marble, myMove.timeState.ticks); for (client => marble in clientMarbles) { this.predictions.storeState(marble, myMove.timeState.ticks); @@ -1536,6 +1575,9 @@ class MarbleWorld extends Scheduler { // pktClone.sort((a, b) -> { // return (a.c == client.id) ? 1 : (b.c == client.id) ? -1 : 0; // }); + if (client.state != GAME) + continue; // Only send if in game + marble.clearNetFlags(); for (packet in packets) { client.sendBytes(packet); } @@ -1679,6 +1721,21 @@ class MarbleWorld extends Scheduler { this.timeState.gameplayClock += ((this.timeState.currentAttemptTime + dt) - 3.5) * timeMultiplier; } } else if (this.multiplayerStarted) { + if (Net.isClient) { + var ticksSinceTimerStart = @:privateAccess this.marble.serverTicks - (this.serverStartTicks + 109); + var ourStartTime = this.gameMode.getStartTime(); + var gameplayHigh = ourStartTime - ticksSinceTimerStart * 0.032; + var gameplayLow = ourStartTime - (ticksSinceTimerStart + 1) * 0.032; + // Clamp timer to be between these two + if (gameplayLow > this.timeState.gameplayClock) { + this.timeState.gameplayClock = gameplayLow - this.timeState.gameplayClock + gameplayHigh; + } + + if (gameplayHigh < this.timeState.gameplayClock) { + this.timeState.gameplayClock = gameplayLow + this.timeState.gameplayClock - gameplayHigh; + } + } + this.timeState.gameplayClock += dt * timeMultiplier; } if (this.timeState.gameplayClock < 0) diff --git a/src/gui/MPServerListGui.hx b/src/gui/MPServerListGui.hx index e07111fb..c6a380dc 100644 --- a/src/gui/MPServerListGui.hx +++ b/src/gui/MPServerListGui.hx @@ -133,7 +133,6 @@ class MPServerListGui extends GuiImage { }, 15000); Net.joinServer(ourServerList[curSelection].name, () -> { failed = false; - MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(false)); Net.remoteServerInfo = ourServerList[curSelection]; }); }; diff --git a/src/modes/HuntMode.hx b/src/modes/HuntMode.hx index d4f74dab..9c73ef4f 100644 --- a/src/modes/HuntMode.hx +++ b/src/modes/HuntMode.hx @@ -362,7 +362,7 @@ class HuntMode extends NullMode { var os = new OutputBitStream(); os.writeByte(GemPickup); packet.serialize(os); - Net.sendPacketToAll(os); + Net.sendPacketToIngame(os); @:privateAccess level.playGui.incrementPlayerScore(packet.clientId, packet.scoreIncr); } @@ -434,7 +434,7 @@ class HuntMode extends NullMode { var packet = new GemSpawnPacket(); packet.gemIds = spawnGroup; packet.serialize(bs); - Net.sendPacketToAll(bs); + Net.sendPacketToIngame(bs); } } } diff --git a/src/net/Net.hx b/src/net/Net.hx index 64905a1a..fd60cecd 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -79,7 +79,7 @@ class Net { public static var remoteServerInfo:RemoteServerInfo; public static function hostServer(name:String, maxPlayers:Int, privateSlots:Int, privateServer:Bool, onHosted:() -> Void) { - serverInfo = new ServerInfo(name, 1, maxPlayers, privateSlots, privateServer, Std.int(999999 * Math.random()), "Lobby", getPlatform()); + serverInfo = new ServerInfo(name, 1, maxPlayers, privateSlots, privateServer, Std.int(999999 * Math.random()), "LOBBY", getPlatform()); MasterServerClient.connectToMasterServer(() -> { isHost = true; isClient = false; @@ -292,6 +292,20 @@ class Net { } } + static function onClientHandshakeComplete(conn:ClientConnection) { + // Send our current mission to connecting client + NetCommands.setLobbyLevelIndexClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); + + if (serverInfo.state == "PLAYING") { // We initiated the game, directly add in the marble + NetCommands.playLevelMidJoinClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); + MarbleGame.instance.world.addJoiningClient(conn, () -> {}); + } + if (serverInfo.state == "LOBBY") { + // Connect client to lobby + NetCommands.enterLobbyClient(conn); + } + } + public static function sendPlayerInfosBytes() { var b = new haxe.io.BytesOutput(); b.writeByte(PlayerInfo); @@ -367,8 +381,7 @@ class Net { for (cc in clients) { cc.sendBytes(b); } - // Send our current mission to connecting client - NetCommands.setLobbyLevelIndexClient(conn, MultiplayerLevelSelectGui.currentSelectionStatic); + onClientHandshakeComplete(conn); } } @@ -459,6 +472,14 @@ class Net { } } + 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(); diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx index 13735594..a154b7e9 100644 --- a/src/net/NetCommands.hx +++ b/src/net/NetCommands.hx @@ -8,6 +8,8 @@ import net.Net.NetPacketType; import gui.MultiplayerLevelSelectGui; import src.MarbleGame; import gui.MultiplayerLoadingGui; +import src.MissionList; +import src.Console; @:build(net.RPCMacro.build()) class NetCommands { @@ -21,6 +23,24 @@ class NetCommands { @:rpc(server) public static function playLevel(levelIndex:Int) { MultiplayerLevelSelectGui.playSelectedLevel(levelIndex); + if (Net.isHost) { + Net.serverInfo.state = "WAITING"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the wait state + } + } + + @:rpc(server) public static function playLevelMidJoin(index:Int) { + if (Net.isClient) { + var difficultyMissions = MissionList.missionList['ultra']["multiplayer"]; + var curMission = difficultyMissions[index]; + MarbleGame.instance.playMission(curMission, true); + } + } + + @:rpc(server) public static function enterLobby() { + if (Net.isClient) { + MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(false)); + } } @:rpc(server) public static function setNetworkRNG(rng:Float) { @@ -64,18 +84,36 @@ class NetCommands { @:rpc(client) public static function clientIsReady(clientId:Int) { if (Net.isHost) { - Net.clientIdMap[clientId].ready(); - var allReady = true; - for (id => client in Net.clientIdMap) { - if (client.state != GameplayState.GAME) { - allReady = false; - break; + if (Net.serverInfo.state == "WAITING" && MarbleGame.instance.world != null) { + Net.clientIdMap[clientId].ready(); + var allReady = true; + for (id => client in Net.clientIdMap) { + if (client.state != GameplayState.GAME) { + allReady = false; + break; + } } - } - if (allReady) { - if (MarbleGame.instance.world != null) { - MarbleGame.instance.world.allClientsReady(); + if (allReady) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.allClientsReady(); + } } + if (Net.isHost) { + Net.serverInfo.state = "PLAYING"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state + } + } else { + // Mid game join + Console.log("Mid game join for client " + clientId); + // Send em our present world state + var packets = MarbleGame.instance.world.getWorldStateForClientJoin(); + var c = Net.clientIdMap[clientId]; + for (packet in packets) { + c.sendBytes(packet); + } + Net.clientIdMap[clientId].ready(); + // Send the start ticks + NetCommands.setStartTicksMidJoinClient(c, MarbleGame.instance.world.serverStartTicks, MarbleGame.instance.world.timeState.ticks); } } } @@ -87,11 +125,23 @@ class NetCommands { } } + @:rpc(server) public static function setStartTicksMidJoin(startTicks:Int, currentTicks:Int) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.serverStartTicks = startTicks + 1; // Extra tick so we don't get 0 + MarbleGame.instance.world.startTime = MarbleGame.instance.world.timeState.timeSinceLoad + 0.032; // 1 extra tick + MarbleGame.instance.world.timeState.ticks = currentTicks; + } + } + @:rpc(server) public static function timerRanOut() { if (Net.isClient && MarbleGame.instance.world != null) { var huntMode:HuntMode = cast MarbleGame.instance.world.gameMode; huntMode.onTimeExpire(); } + if (Net.isHost) { + Net.serverInfo.state = "WAITING"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state + } } @:rpc(server) public static function clientDisconnected(clientId:Int) { @@ -155,6 +205,11 @@ class NetCommands { v.lobbyReady = false; } Net.lobbyHostReady = false; + + if (Net.isHost) { + Net.serverInfo.state = "LOBBY"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state + } } } diff --git a/src/shapes/PowerUp.hx b/src/shapes/PowerUp.hx index 8f9804b6..9530f222 100644 --- a/src/shapes/PowerUp.hx +++ b/src/shapes/PowerUp.hx @@ -47,7 +47,7 @@ abstract class PowerUp extends DtsObject { pickupPacket.serverTicks = timeState.ticks; pickupPacket.powerupItemId = this.netIndex; pickupPacket.serialize(b); - Net.sendPacketToAll(b); + Net.sendPacketToIngame(b); } this.lastPickUpTime = timeState.currentAttemptTime;