diff --git a/src/Console.hx b/src/Console.hx index 48443e19..2a81ac5e 100644 --- a/src/Console.hx +++ b/src/Console.hx @@ -56,6 +56,10 @@ class Console { return Std.int((haxe.Timer.stamp() - timeSinceStart) * 1000) / 1000; } + public static inline function time() { + return haxe.Timer.stamp(); + } + function addEntry(type:String, msg:String) { var e = new ConsoleEntry(getTime(), type, msg); entries.push(e); diff --git a/src/DifBuilder.hx b/src/DifBuilder.hx index 1c044de4..2ce06a75 100644 --- a/src/DifBuilder.hx +++ b/src/DifBuilder.hx @@ -302,6 +302,7 @@ class DifBuilder { var difresource = ResourceLoader.loadInterior(path); difresource.acquire(); var dif = difresource.resource; + dumbDownDif(dif); var geo = so == -1 ? dif.interiors[0] : dif.subObjects[so]; var triangles = []; var textures = []; @@ -748,4 +749,55 @@ class DifBuilder { worker.run(); }); } + + // Keeps only relevant parts of the dif to reduce memory footprint + static function dumbDownDif(dif:Dif) { + dif.aiSpecialNodes = null; + dif.forceFields = null; + dif.triggers = null; + dif.gameEntities = null; + dif.interiorPathfollowers = null; + dif.triggers = null; + dif.vehicleCollision = null; + for (itr in dif.interiors.concat(dif.subObjects)) { + itr.alarmAmbientColor = null; + itr.alarmLMapIndices = null; + itr.animatedLights = null; + itr.baseAmbientColor = null; + itr.bspNodes = null; + itr.bspSolidLeaves = null; + itr.convexHullEmitStrings = null; + itr.convexHulls = null; + itr.coordBinIndices = null; + itr.boundingSphere = null; + itr.coordBins = null; + itr.edges = null; + itr.edges2 = null; + itr.hullEmitStringIndices = null; + itr.hullIndices = null; + itr.hullPlaneIndices = null; + itr.hullSurfaceIndices = null; + itr.lightMaps = null; + itr.lightStates = null; + itr.nameBuffer = null; + itr.normalIndices = null; + itr.normalLMapIndices = null; + itr.nullSurfaces = null; + itr.pointVisibilities = null; + itr.polyListPlanes = null; + itr.polyListPoints = null; + itr.polyListStrings = null; + itr.portals = null; + itr.solidLeafSurfaces = null; + itr.stateDataBuffers = null; + itr.zones = null; + itr.zoneSurfaces = null; + itr.zoneStaticMeshes = null; + itr.windingIndices = null; + itr.texNormals = null; + itr.texMatrices = null; + itr.texMatIndices = null; + itr.stateDatas = null; + } + } } diff --git a/src/Main.hx b/src/Main.hx index 29b3ae0a..b9c7be5b 100644 --- a/src/Main.hx +++ b/src/Main.hx @@ -22,6 +22,7 @@ import h3d.Vector; import src.ProfilerUI; import src.Gamepad; import src.Http; +import datachannel.RTC; class Main extends hxd.App { var marbleGame:MarbleGame; @@ -72,6 +73,7 @@ class Main extends hxd.App { #end try { + RTC.init(); Http.init(); haxe.MainLoop.add(() -> Http.loop()); Settings.init(); @@ -126,6 +128,7 @@ class Main extends hxd.App { // marbleGame.update(1 / 60); // timeAccumulator -= 1 / 60; // } + RTC.processEvents(); marbleGame.update(dt); // } catch (e) { // Console.error(e.message); diff --git a/src/Marble.hx b/src/Marble.hx index 2ab5bd83..126c6d86 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1,5 +1,14 @@ package src; +import net.Net; +import gui.MarbleSelectGui; +import net.NetPacket.MarbleNetFlags; +import net.BitStream.OutputBitStream; +import net.ClientConnection; +import net.ClientConnection.GameConnection; +import net.NetPacket.MarbleUpdatePacket; +import net.MoveManager; +import net.MoveManager.NetMove; import collision.CollisionPool; import collision.CollisionHull; import dif.Plane; @@ -63,14 +72,7 @@ import src.ResourceLoaderWorker; import src.InteriorObject; import src.Console; import src.Gamepad; - -class Move { - public var d:Vector; - public var jump:Bool; - public var powerup:Bool; - - public function new() {} -} +import net.Move; enum Mode { Start; @@ -314,6 +316,7 @@ class Marble extends GameObject { public var cubemapRenderer:CubemapRenderer; + var connection:GameConnection; var moveMotionDir:Vector; var lastMove:Move; var isNetUpdate:Bool = false; @@ -328,6 +331,7 @@ class Marble extends GameObject { this.velocity = new Vector(); this.omega = new Vector(); this.camera = new CameraController(cast this); + this.isCollideable = true; this.bounceEmitterData = new ParticleData(); this.bounceEmitterData.identifier = "MarbleBounceParticle"; @@ -362,11 +366,13 @@ class Marble extends GameObject { this.helicopterSound.pause = true; } - public function init(level:MarbleWorld, onFinish:Void->Void) { + public function init(level:MarbleWorld, connection:GameConnection, onFinish:Void->Void) { this.level = level; if (this.level != null) this.collisionWorld = this.level.collisionWorld; + this.connection = connection; + var isUltra = level.mission.game.toLowerCase() == "ultra"; this.posStore = new Vector(); @@ -375,9 +381,16 @@ class Marble extends GameObject { this.netCorrected = false; var marbleDts = new DtsObject(); - Console.log("Marble: " + Settings.optionsSettings.marbleModel + " (" + Settings.optionsSettings.marbleSkin + ")"); - marbleDts.dtsPath = Settings.optionsSettings.marbleModel; - marbleDts.matNameOverride.set("base.marble", Settings.optionsSettings.marbleSkin + ".marble"); + if (connection == null) { + Console.log("Marble: " + Settings.optionsSettings.marbleModel + " (" + Settings.optionsSettings.marbleSkin + ")"); + marbleDts.dtsPath = Settings.optionsSettings.marbleModel; + marbleDts.matNameOverride.set("base.marble", Settings.optionsSettings.marbleSkin + ".marble"); + } else { + var marbleData = MarbleSelectGui.marbleData[0][connection.getMarbleId()]; // FIXME category support + Console.log("Marble: " + marbleData.dts + " (" + marbleData.skin + ")"); + marbleDts.dtsPath = marbleData.dts; + marbleDts.matNameOverride.set("base.marble", marbleData.skin + ".marble"); + } marbleDts.showSequences = false; marbleDts.useInstancing = false; marbleDts.init(null, () -> {}); // SYNC @@ -858,7 +871,7 @@ class Marble extends GameObject { } if (sv < this._jumpImpulse) { this.velocity.load(this.velocity.add(bestContact.normal.multiply((this._jumpImpulse - sv)))); - if (!playedSounds.contains("data/sound/jump.wav")) { + if (!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"); } @@ -937,6 +950,8 @@ class Marble extends GameObject { } function bounceEmitter(speed:Float, normal:Vector) { + if (!this.controllable || this.isNetUpdate) + return; if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) { this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData, this.getAbsPos().getPosition()); this.bounceEmitDelay = 0.3; @@ -969,6 +984,8 @@ class Marble extends GameObject { } function playBoundSound(time:Float, contactVel:Float) { + if (this.isNetUpdate) + return; if (minVelocityBounceSoft <= contactVel) { var hardBounceSpeed = minVelocityBounceHard; var bounceSoundNum = Math.floor(Math.random() * 4); @@ -1486,6 +1503,21 @@ class Marble extends GameObject { var piTime = timeRemaining; + if (this.isNetUpdate) { + lastMove = m; + } + + if (m == null) { + m = new Move(); + m.d = new Vector(); + } + + if (this.blastTicks < (30000 >> 5)) + this.blastTicks += 1; + + if (Net.isClient) + this.serverTicks++; + _bounceYet = false; var contactTime = 0.0; @@ -1493,7 +1525,8 @@ class Marble extends GameObject { var passedTime = timeState.currentAttemptTime; - var oldPos = this.getAbsPos().getPosition().clone(); + oldPos = this.collider.transform.getPosition(); + prevRot = this.getRotationQuat().clone(); if (this.controllable) { for (interior in pathedInteriors) { @@ -1502,6 +1535,14 @@ class Marble extends GameObject { } } + // Blast + if (m != null && m.blast) { + this.useBlast(timeState); + if (level.isRecording) { + level.replay.recordMarbleStateFlags(false, false, false, true); + } + } + do { if (timeRemaining <= 0) break; @@ -1566,7 +1607,7 @@ class Marble extends GameObject { velocity.w = 0; - var pos = this.getAbsPos().getPosition(); + var pos = this.collider.transform.getPosition(); this.prevPos = pos.clone(); var tdiff = timeStep; @@ -1605,24 +1646,36 @@ class Marble extends GameObject { var quat = new Quat(); quat.initRotation(omega.x * timeStep, omega.y * timeStep, omega.z * timeStep); quat.multiply(quat, rot); - this.setRotationQuat(quat); + if (!Net.isMP) + this.setRotationQuat(quat); var totMatrix = quat.toMatrix(); newPos.w = 1; // Fix shit blowing up totMatrix.setPosition(newPos); - this.setPosition(newPos.x, newPos.y, newPos.z); + if (!Net.isMP) + this.setPosition(newPos.x, newPos.y, newPos.z); this.collider.setTransform(totMatrix); this.collisionWorld.updateTransform(this.collider); this.collider.velocity = this.velocity; - if (this.heldPowerup != null && m.powerup && !this.outOfBounds) { + if (this.heldPowerup != null + && (m.powerup || (Net.isClient && this.serverUsePowerup && !this.controllable)) + && !this.outOfBounds) { var pTime = timeState.clone(); pTime.dt = timeStep; pTime.currentAttemptTime = passedTime; + var netUpdate = this.isNetUpdate; + if (this.serverUsePowerup) + this.isNetUpdate = false; this.heldPowerup.use(this, pTime); + this.isNetUpdate = netUpdate; this.heldPowerup = null; + this.serverUsePowerup = false; + if (!this.isNetUpdate) { + this.netFlags |= MarbleNetFlags.PickupPowerup | MarbleNetFlags.UsePowerup; + } if (this.level.isRecording) { this.level.replay.recordPowerupPickup(null); } @@ -1654,49 +1707,333 @@ class Marble extends GameObject { } this.queuedContacts = []; - var newPos = this.getAbsPos().getPosition().clone(); + newPos = this.collider.transform.getPosition(); // this.getAbsPos().getPosition().clone(); - if (this.controllable && this.prevPos != null) { + if (this.prevPos != null && this.level != null) { var tempTimeState = timeState.clone(); tempTimeState.currentAttemptTime = passedTime; this.level.callCollisionHandlers(cast this, tempTimeState, oldPos, newPos); } this.updateRollSound(timeState, contactTime / timeState.dt, this._slipAmount); + + // if (this.megaMarbleUseTick > 0) { + // if (Net.isHost) { + // if ((timeState.ticks - this.megaMarbleUseTick) <= 312 && this.megaMarbleUseTick > 0) { + // this._radius = 0.675; + // this.collider.radius = 0.675; + // } else if ((timeState.ticks - this.megaMarbleUseTick) > 312) { + // this.collider.radius = this._radius = 0.3; + // if (!this.isNetUpdate && this.controllable) + // AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null, + // false); + // this.megaMarbleUseTick = 0; + // this.netFlags |= MarbleNetFlags.DoMega; + // } + // } + // if (Net.isClient) { + // if (this.serverTicks - this.megaMarbleUseTick <= 312 && this.megaMarbleUseTick > 0) { + // this._radius = 0.675; + // this.collider.radius = 0.675; + // } else { + // this.collider.radius = this._radius = 0.3; + // if (!this.isNetUpdate && this.controllable) + // AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null, + // false); + // this.megaMarbleUseTick = 0; + // } + // } + // } + // if (Net.isClient && this.megaMarbleUseTick == 0) { + // this.collider.radius = this._radius = 0.3; + // } + + if (Net.isMP) { + if (m.jump && this.outOfBounds) { + this.level.cancel(this.oobSchedule); + this.level.restart(cast this); + } + } + } + + // MP Only Functions + public inline function clearNetFlags() { + this.netFlags = 0; + } + + public function packUpdate(move:NetMove, timeState:TimeState) { + var b = new OutputBitStream(); + b.writeByte(NetPacketType.MarbleUpdate); + var marbleUpdate = new MarbleUpdatePacket(); + marbleUpdate.clientId = connection != null ? connection.id : 0; + marbleUpdate.serverTicks = timeState.ticks; + marbleUpdate.position = this.newPos; + marbleUpdate.velocity = this.velocity; + marbleUpdate.omega = this.omega; + marbleUpdate.move = move; + marbleUpdate.moveQueueSize = this.connection != null ? this.connection.moveManager.getQueueSize() : 255; + marbleUpdate.blastAmount = this.blastTicks; + marbleUpdate.blastTick = this.blastUseTick; + marbleUpdate.heliTick = this.helicopterUseTick; + marbleUpdate.megaTick = this.megaMarbleUseTick; + marbleUpdate.oob = this.outOfBounds; + marbleUpdate.powerUpId = this.heldPowerup != null ? this.heldPowerup.netIndex : 0x1FF; + marbleUpdate.netFlags = this.netFlags; + marbleUpdate.gravityDirection = this.currentUp; + marbleUpdate.serialize(b); + return b.getBytes(); + } + + public function unpackUpdate(p:MarbleUpdatePacket) { + // Assume packet header is already read + // Check if we aren't colliding with a marble + // for (marble in this.level.collisionWorld.marbleEntities) { + // if (marble != this.collider && marble.transform.getPosition().distance(p.position) < marble.radius + this._radius) { + // Console.log("Marble updated inside another one!"); + // return false; + // } + // } + this.serverTicks = p.serverTicks; + this.recvServerTick = p.serverTicks; + // this.oldPos = this.newPos; + // this.newPos = p.position; + this.collider.transform.setPosition(p.position); + this.velocity = p.velocity; + this.omega = p.omega; + this.blastTicks = p.blastAmount; + this.blastUseTick = p.blastTick; + this.helicopterUseTick = p.heliTick; + this.megaMarbleUseTick = p.megaTick; + this.serverUsePowerup = p.netFlags & MarbleNetFlags.UsePowerup > 0; + // this.currentUp = p.gravityDirection; + this.level.setUp(cast this, p.gravityDirection, this.level.timeState); + if (this.outOfBounds && !p.oob && this.controllable) + @:privateAccess this.level.playGui.setCenterText(''); + this.outOfBounds = p.oob; + this.camera.oob = p.oob; + if (p.powerUpId == 0x1FF) { + if (!this.serverUsePowerup) + this.level.deselectPowerUp(cast this); + else + Console.log("Using powerup"); + } else { + this.level.pickUpPowerUp(cast this, this.level.powerUps[p.powerUpId]); + } + if (p.moveQueueSize == 0 && this.connection != null) { + // Pad null move on client + this.connection.moveManager.duplicateLastMove(); + } + // if (Net.isClient && !this.controllable && (this.serverTicks - this.blastUseTick) < 12) { + // var ticksSince = (this.serverTicks - this.blastUseTick); + // if (ticksSince >= 0) { + // this.blastWave.doSequenceOnceBeginTime = this.level.timeState.timeSinceLoad - ticksSince * 0.032; + // this.blastUseTime = this.level.timeState.currentAttemptTime - ticksSince * 0.032; + // } + // } + + // if (this.controllable && Net.isClient) { + // // We are client, need to do something about the queue + // var mm = Net.clientConnection.moveManager; + // // trace('Queue size: ${mm.getQueueSize()}, server: ${p.moveQueueSize}'); + // if (mm.getQueueSize() / p.moveQueueSize < 2) { + // mm.stall = true; + // } else { + // mm.stall = false; + // } + // } + return true; + } + + function calculateNetSmooth() { + if (this.netCorrected) { + this.netCorrected = false; + this.netSmoothOffset.load(this.lastRenderPos.sub(this.oldPos)); + // this.oldPos.load(this.posStore); + } + } + + public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { + var move:NetMove = null; + if (this.controllable && this.mode != Finish) { + if (Net.isClient) { + var axis = getMarbleAxis()[1]; + move = Net.clientConnection.recordMove(cast this, axis, timeState, recvServerTick); + } else if (Net.isHost) { + var axis = getMarbleAxis()[1]; + var innerMove = recordMove(); + if (MarbleGame.instance.paused) { + innerMove.d.x = 0; + innerMove.d.y = 0; + innerMove.blast = innerMove.jump = innerMove.powerup = false; + } else { + var qx = Std.int((innerMove.d.x * 16) + 16); + var qy = Std.int((innerMove.d.y * 16) + 16); + innerMove.d.x = (qx - 16) / 16.0; + innerMove.d.y = (qy - 16) / 16.0; + } + move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535); + } + } + var moveId = 65535; + if (!this.controllable && this.connection != null && Net.isHost) { + var nextMove = this.connection.getNextMove(); + // trace('Moves left: ${@:privateAccess this.connection.moveManager.queuedMoves.length}'); + if (nextMove == null) { + var axis = moveMotionDir != null ? moveMotionDir : getMarbleAxis()[1]; + var innerMove = lastMove; + if (innerMove == null) { + innerMove = new Move(); + innerMove.d = new Vector(0, 0); + } + move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535); + } else { + move = nextMove; + moveMotionDir = nextMove.motionDir; + moveId = nextMove.id; + lastMove = move.move; + } + } + if (move == null && !this.controllable) { + var axis = moveMotionDir != null ? moveMotionDir : new Vector(0, -1, 0); + var innerMove = lastMove; + if (innerMove == null) { + innerMove = new Move(); + innerMove.d = new Vector(0, 0); + } + move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535); + } + + if (move != null) { + playedSounds = []; + advancePhysics(timeState, move.move, collisionWorld, pathedInteriors); + physicsAccumulator = 0; + } else { + physicsAccumulator = 0; + newPos.load(oldPos); + } + + return move; + // if (Net.isHost) { + // packets.push({b: packUpdate(move, timeState), c: this.connection != null ? this.connection.id : 0}); + // } + } + + public function updateClient(timeState:TimeState, pathedInteriors:Array) { + calculateNetSmooth(); + this.level.updateBlast(cast this, timeState); + + var newDt = 2.3 * (timeState.dt / 0.4); + var smooth = 1.0 / (newDt * (newDt * 0.235 * newDt) + newDt + 1.0 + 0.48 * newDt * newDt); + this.netSmoothOffset.scale(smooth); + var smoothScale = this.netSmoothOffset.lengthSq(); + if (smoothScale < 0.1 || smoothScale > 10.0) + this.netSmoothOffset.set(0, 0, 0); + + if (oldPos != null && newPos != null) { + var deltaT = physicsAccumulator / 0.032; + if (Net.isClient && !this.controllable) + deltaT *= 0.75; // Don't overshoot + var renderPos = Util.lerpThreeVectors(this.oldPos, this.newPos, deltaT); + if (Net.isClient) { + renderPos.load(renderPos.add(this.netSmoothOffset)); + } + this.setPosition(renderPos.x, renderPos.y, renderPos.z); + this.lastRenderPos.load(renderPos); + + var rot = this.getRotationQuat(); + var quat = new Quat(); + quat.initRotation(omega.x * timeState.dt, omega.y * timeState.dt, omega.z * timeState.dt); + quat.multiply(quat, rot); + this.setRotationQuat(quat); + + var adt = timeState.clone(); + adt.dt = physicsAccumulator; + for (pi in pathedInteriors) { + pi.update(adt); + } + } + physicsAccumulator += timeState.dt; + + if (this.controllable && this.level != null && !this.level.rewinding) { + // this.camera.startCenterCamera(); + this.camera.update(timeState.currentAttemptTime, timeState.dt); + } + + updatePowerupStates(timeState); + + // if (isMegaMarbleEnabled(timeState)) { + // this._marbleScale = this._defaultScale * 2.25; + // } else { + // this._marbleScale = this._defaultScale; + // } + + // var s = this._renderScale * this._renderScale; + // if (s <= this._marbleScale * this._marbleScale) + // s = 0.1; + // else + // s = 0.4; + + // s = timeState.dt / s * 2.302585124969482; + // s = 1.0 / (s * (s * 0.235 * s) + s + 1.0 + 0.48 * s * s); + // this._renderScale *= s; + // s = 1 - s; + // this._renderScale += s * this._marbleScale; + // var marbledts = cast(this.getChildAt(0), DtsObject); + // marbledts.setScale(this._renderScale); + + this.trailEmitter(); + if (bounceEmitDelay > 0) + bounceEmitDelay -= timeState.dt; + if (bounceEmitDelay < 0) + bounceEmitDelay = 0; + } + + public function recordMove() { + 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 (@:privateAccess !MarbleGame.instance.world.playGui.isChatFocused()) { + 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 (Key.isDown(Settings.controlsSettings.blast) + || (MarbleGame.instance.touchInput.blastbutton.pressed) + || Gamepad.isDown(Settings.gamepadSettings.blast)) + move.blast = 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; + } + // } + return move; } public function update(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { - var move = new Move(); - move.d = new Vector(); - if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching) { - 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 move:Move = null; + if (this.controllable && !this.level.isWatching) { + move = recordMove(); } if (this.level.isWatching) { @@ -1711,6 +2048,10 @@ class Marble extends GameObject { this.level.replay.recordMarbleInput(move.d.x, move.d.y); } } + if (!this.controllable && (this.connection != null || this.level == null)) { + move = new Move(); + move.d = new Vector(0, 0); + } playedSounds = []; advancePhysics(timeState, move, collisionWorld, pathedInteriors); @@ -1738,7 +2079,7 @@ class Marble extends GameObject { this.camera.update(timeState.currentAttemptTime, timeState.dt); } - updatePowerupStates(timeState.currentAttemptTime, timeState.dt); + updatePowerupStates(timeState); if (this._radius != 0.6666 && timeState.currentAttemptTime - this.megaMarbleEnableTime < 10) { this._prevRadius = this._radius; @@ -1766,30 +2107,30 @@ class Marble extends GameObject { // this.camera.target.load(this.getAbsPos().getPosition().toPoint()); } - public function updatePowerupStates(currentTime:Float, dt:Float) { - if (currentTime - this.shockAbsorberEnableTime < 5) { + public function updatePowerupStates(timeState:TimeState) { + if (timeState.currentAttemptTime - this.shockAbsorberEnableTime < 5) { this.shockabsorberSound.pause = false; } else { this.shockabsorberSound.pause = true; } - if (currentTime - this.superBounceEnableTime < 5) { + if (timeState.currentAttemptTime - this.superBounceEnableTime < 5) { this.superbounceSound.pause = false; } else { this.superbounceSound.pause = true; } - if (currentTime - this.shockAbsorberEnableTime < 5) { + if (timeState.currentAttemptTime - this.shockAbsorberEnableTime < 5) { this.forcefield.setPosition(0, 0, 0); - } else if (currentTime - this.superBounceEnableTime < 5) { + } else if (timeState.currentAttemptTime - this.superBounceEnableTime < 5) { this.forcefield.setPosition(0, 0, 0); } else { this.forcefield.x = 1e8; this.forcefield.y = 1e8; this.forcefield.z = 1e8; } - if (currentTime - this.helicopterEnableTime < 5) { + if (timeState.currentAttemptTime - this.helicopterEnableTime < 5) { this.helicopter.setPosition(x, y, z); - this.helicopter.setRotationQuat(this.level.getOrientationQuat(currentTime)); + this.helicopter.setRotationQuat(this.level.getOrientationQuat(timeState.currentAttemptTime)); this.helicopterSound.pause = false; } else { this.helicopter.setPosition(1e8, 1e8, 1e8); @@ -1807,18 +2148,18 @@ class Marble extends GameObject { } } - public function useBlast() { - if (this.level.blastAmount < 0.2 || this.level.game != "ultra") + public function useBlast(timeState:TimeState) { + if (this.blastAmount < 0.2 || this.level.game != "ultra") return; - var impulse = this.currentUp.multiply(Math.max(Math.sqrt(this.level.blastAmount), this.level.blastAmount) * 10); + var impulse = this.currentUp.multiply(Math.max(Math.sqrt(this.blastAmount), this.blastAmount) * 10); this.applyImpulse(impulse); AudioManager.playSound(ResourceLoader.getResource('data/sound/blast.wav', ResourceLoader.getAudio, this.soundResources)); - this.level.particleManager.createEmitter(this.level.blastAmount > 1 ? blastMaxParticleOptions : blastParticleOptions, - this.level.blastAmount > 1 ? blastMaxEmitterData : blastEmitterData, this.getAbsPos().getPosition(), () -> { + this.level.particleManager.createEmitter(this.blastAmount > 1 ? blastMaxParticleOptions : blastParticleOptions, + this.blastAmount > 1 ? blastMaxEmitterData : blastEmitterData, this.getAbsPos().getPosition(), () -> { this.getAbsPos().getPosition().add(this.currentUp.multiply(-this._radius * 0.4)); }, new Vector(1, 1, 1).add(new Vector(Math.abs(this.currentUp.x), Math.abs(this.currentUp.y), Math.abs(this.currentUp.z)).multiply(-0.8))); - this.level.blastAmount = 0; + this.blastAmount = 0; } public function getForce(position:Vector, tick:Int) { @@ -1909,11 +2250,23 @@ class Marble extends GameObject { } } + public inline function setMode(mode:Mode) { + this.mode = mode; + } + public function setMarblePosition(x:Float, y:Float, z:Float) { this.collider.transform.setPosition(new Vector(x, y, z)); this.setPosition(x, y, z); } + public inline function getConnectionId() { + if (this.connection == null) { + return Net.isHost ? 0 : Net.clientId; + } else { + return this.connection.id; + } + } + public override function reset() { this.velocity = new Vector(); this.collider.velocity = new Vector(); @@ -1922,6 +2275,11 @@ class Marble extends GameObject { this.shockAbsorberEnableTime = Math.NEGATIVE_INFINITY; this.helicopterEnableTime = Math.NEGATIVE_INFINITY; this.megaMarbleEnableTime = Math.NEGATIVE_INFINITY; + this.blastUseTick = 0; + this.blastTicks = 0; + this.helicopterUseTick = 0; + this.megaMarbleUseTick = 0; + this.netFlags = MarbleNetFlags.DoBlast | MarbleNetFlags.DoMega | MarbleNetFlags.DoHelicopter | MarbleNetFlags.PickupPowerup | MarbleNetFlags.GravityChange | MarbleNetFlags.UsePowerup; this.lastContactNormal = new Vector(0, 0, 1); this.contactEntities = []; this.cloak = false; @@ -1933,6 +2291,15 @@ class Marble extends GameObject { this.teleporting = false; this.teleportDisableTime = null; this.teleportEnableTime = null; + this.physicsAccumulator = 0; + this.prevRot = this.getRotationQuat().clone(); + this.oldPos = this.getAbsPos().getPosition(); + this.newPos = this.getAbsPos().getPosition(); + this.posStore = new Vector(); + this.netSmoothOffset = new Vector(); + this.lastRenderPos = new Vector(); + this.netCorrected = false; + this.serverUsePowerup = false; if (this._radius != this._prevRadius) { this._radius = this._prevRadius; this.collider.radius = this._radius; @@ -1942,6 +2309,15 @@ class Marble extends GameObject { } public override function dispose() { + if (this.rollSound != null) + this.rollSound.stop(); + if (this.rollMegaSound != null) + this.rollMegaSound.stop(); + if (this.slipSound != null) + this.slipSound.stop(); + if (this.helicopterSound != null) + this.helicopterSound.stop(); + this.helicopter.remove(); super.dispose(); removeChildren(); camera = null; diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index b0436dd7..9063b119 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -1,5 +1,6 @@ package src; +import gui.MPPlayMissionGui; import gui.MainMenuGui; #if !js import gui.ReplayCenterGui; @@ -27,6 +28,9 @@ import src.Console; import src.Debug; import src.Gamepad; import src.Analytics; +import net.Net; +import net.MasterServerClient; +import net.NetCommands; @:publicFields class MarbleGame { @@ -181,6 +185,8 @@ class MarbleGame { } public function update(dt:Float) { + MasterServerClient.process(); + Net.checkPacketTimeout(dt); if (world != null) { if (world._disposed) { world = null; @@ -190,7 +196,7 @@ class MarbleGame { if (Util.isTouchDevice()) { touchInput.update(); } - if (!paused) { + if (!paused || world.isMultiplayer) { world.update(dt * Debug.timeScale); } if (((Key.isPressed(Key.ESCAPE) #if js && paused #end) || Gamepad.isPressed(["start"])) @@ -236,7 +242,10 @@ class MarbleGame { quitMission(); })); } else { - quitMission(); + quitMission(Net.isClient); + if (Net.isMP && Net.isClient) { + Net.disconnect(); + } } }, (sender) -> { canvas.popDialog(exitGameDlg); @@ -266,8 +275,13 @@ class MarbleGame { return world; } - public function quitMission() { + public function quitMission(weDisconnecting:Bool = false) { Console.log("Quitting mission"); + if (Net.isMP) { + if (Net.isHost) { + NetCommands.endGame(); + } + } world.setCursorLock(false); if (!Settings.levelStatistics.exists(world.mission.path)) { Settings.levelStatistics.set(world.mission.path, { @@ -287,13 +301,18 @@ class MarbleGame { canvas.setContent(new MainMenuGui()); #end } else { - if (!world.mission.isClaMission && !world.mission.isCustom) { - PlayMissionGui.currentCategoryStatic = world.mission.type; + if (Net.isMP) { + var lobby = new MPPlayMissionGui(Net.isHost); + canvas.setContent(lobby); + } else { + if (!world.mission.isClaMission && !world.mission.isCustom) { + PlayMissionGui.currentCategoryStatic = world.mission.type; + } + var pmg = new PlayMissionGui(); + PlayMissionGui.currentSelectionStatic = world.mission.index; + PlayMissionGui.currentGameStatic = world.mission.game; + canvas.setContent(pmg); } - var pmg = new PlayMissionGui(); - PlayMissionGui.currentSelectionStatic = world.mission.index; - PlayMissionGui.currentGameStatic = world.mission.game; - canvas.setContent(pmg); } world.dispose(); world = null; @@ -301,13 +320,13 @@ class MarbleGame { Settings.save(); } - public function playMission(mission:Mission) { + public function playMission(mission:Mission, multiplayer:Bool = false) { canvas.clearContent(); if (world != null) { world.dispose(); } Analytics.trackLevelPlay(mission.title, mission.path); - world = new MarbleWorld(scene, scene2d, mission, toRecord); + world = new MarbleWorld(scene, scene2d, mission, toRecord, multiplayer); world.init(); } diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index f76f2fa4..de6cac77 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,29 @@ package src; +import net.NetPacket.ScoreboardPacket; +import net.NetPacket.PowerupPickupPacket; +import net.Move; +import net.NetPacket.GemSpawnPacket; +import net.BitStream.OutputBitStream; +import net.MasterServerClient; +import gui.MarbleSelectGui; +import gui.MPPlayMissionGui; +import collision.CollisionPool; +import net.GemPredictionStore; +import modes.HuntMode; +import net.NetPacket.MarbleNetFlags; +import net.PowerupPredictionStore; +import net.MarblePredictionStore; +import net.MarblePredictionStore.MarblePrediction; +import net.MarbleUpdateQueue; +import haxe.Exception; +import net.NetPacket.MarbleUpdatePacket; +import net.NetPacket.MarbleMovePacket; +import net.MoveManager; +import net.NetCommands; +import net.Net; +import net.ClientConnection; +import net.ClientConnection.GameConnection; import modes.GameMode; import modes.GameMode.GameModeFactory; import rewind.RewindManager; @@ -150,7 +174,6 @@ class MarbleWorld extends Scheduler { public var finishYaw:Float; public var totalGems:Int = 0; public var gemCount:Int = 0; - public var blastAmount:Float = 0; public var cursorLock:Bool = true; @@ -185,6 +208,23 @@ class MarbleWorld extends Scheduler { public var rewindManager:RewindManager; public var rewinding:Bool = false; + // Multiplayer + public var isMultiplayer:Bool; + + public var serverStartTicks:Int; + public var startTime:Float = 1e8; + public var multiplayerStarted:Bool = false; + + var tickAccumulator:Float = 0.0; + var maxPredictionTicks:Int = 16; + + var clientMarbles:Map = []; + var predictions:MarblePredictionStore; + var powerupPredictions:PowerupPredictionStore; + var gemPredictions:GemPredictionStore; + + public var lastMoves:MarbleUpdateQueue; + // Loading var resourceLoadFuncs:Array<(() -> Void)->Void> = []; @@ -207,7 +247,7 @@ class MarbleWorld extends Scheduler { var _instancesNeedsUpdate:Bool = false; var lock:Bool = false; - public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission, record:Bool = false) { + public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission, record:Bool = false, multiplayer:Bool = false) { this.scene = scene; this.scene2d = scene2d; this.mission = mission; @@ -216,6 +256,15 @@ class MarbleWorld extends Scheduler { this.replay = new Replay(mission.path, mission.isClaMission ? mission.id : 0); this.isRecording = record; this.rewindManager = new RewindManager(cast this); + this.isMultiplayer = multiplayer; + if (this.isMultiplayer) { + isRecording = false; + isWatching = false; + lastMoves = new MarbleUpdateQueue(); + predictions = new MarblePredictionStore(); + powerupPredictions = new PowerupPredictionStore(); + gemPredictions = new GemPredictionStore(); + } } public function init() { @@ -266,7 +315,12 @@ class MarbleWorld extends Scheduler { scanMission(this.mission.root); this.gameMode.missionScan(this.mission); this.resourceLoadFuncs.push(fwd -> this.initScene(fwd)); - this.resourceLoadFuncs.push(fwd -> this.initMarble(fwd)); + if (this.isMultiplayer) { + for (client in Net.clientIdMap) { + this.resourceLoadFuncs.push(fwd -> this.initMarble(client, fwd)); // Others + } + } + this.resourceLoadFuncs.push(fwd -> this.initMarble(null, fwd)); this.resourceLoadFuncs.push(fwd -> { this.addSimGroup(this.mission.root); this._loadingLength = resourceLoadFuncs.length; @@ -289,6 +343,19 @@ class MarbleWorld extends Scheduler { // Add the sky at the last so that cubemap reflections work this.playGui.init(this.scene2d, this.mission.game.toLowerCase(), () -> { this.scene.addChild(this.sky); + + if (this.isMultiplayer) { + // Add us + // if (Net.isHost) { + // this.playGui.addPlayer(0, Settings.highscoreName.substr(0, 15), true); + // } else { + // this.playGui.addPlayer(Net.clientId, Settings.highscoreName.substr(0, 15), true); + // } + // for (client in Net.clientIdMap) { + // this.playGui.addPlayer(client.id, client.name.substr(0, 15), false); + // } + } + this._ready = true; var musicFileName = 'data/sound/music/' + this.mission.missionInfo.music; AudioManager.playMusic(ResourceLoader.getResource(musicFileName, ResourceLoader.getAudio, this.soundResources), this.mission.missionInfo.music); @@ -379,7 +446,7 @@ class MarbleWorld extends Scheduler { worker.run(); } - public function initMarble(onFinish:Void->Void) { + public function initMarble(client:GameConnection, onFinish:Void->Void) { Console.log("Initializing marble"); var worker = new ResourceLoaderWorker(onFinish); var marblefiles = [ @@ -424,7 +491,13 @@ class MarbleWorld extends Scheduler { marblefiles.push("sound/blast.wav"); } // Hacky - marblefiles.push(StringTools.replace(Settings.optionsSettings.marbleModel, "data/", "")); + if (client == null) { + marblefiles.push(StringTools.replace(Settings.optionsSettings.marbleModel, "data/", "")); + } else { + var marbleDts = MarbleSelectGui.marbleData[0][client.getMarbleId()].dts; // FIXME + marblefiles.push(StringTools.replace(marbleDts, "data/", "")); + } + if (Settings.optionsSettings.marbleCategoryIndex == 0) marblefiles.push("shapes/balls/" + Settings.optionsSettings.marbleSkin + ".marble.png"); else @@ -438,8 +511,9 @@ class MarbleWorld extends Scheduler { } worker.addTask(fwd -> { var marble = new Marble(); - marble.controllable = true; - this.addMarble(marble, fwd); + if (client == null) + marble.controllable = true; + this.addMarble(marble, client, fwd); }); worker.run(); } @@ -451,6 +525,71 @@ class MarbleWorld extends Scheduler { interior.onLevelStart(); for (shape in this.dtsObjects) shape.onLevelStart(); + if (this.isMultiplayer && Net.isClient) + NetCommands.clientIsReady(Net.clientId); + if (this.isMultiplayer && Net.isHost) { + NetCommands.clientIsReady(-1); + + // Sort all the marbles so that they are updated in a deterministic order + this.marbles.sort((a, b) -> @:privateAccess { + var aId = a.connection != null ? a.connection.id : 0; // Must be a host + var bId = b.connection != null ? b.connection.id : 0; // Must be a host + return (aId > bId) ? 1 : (aId < bId) ? -1 : 0; + }); + } + var cc = 0; + for (client in Net.clients) + 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 + // this.playGui.addPlayer(cc.id, cc.getName(), false); + // this.playGui.redrawPlayerList(); + + // Sort all the marbles so that they are updated in a deterministic order + this.marbles.sort((a, b) -> @:privateAccess { + var aId = a.getConnectionId(); + var bId = b.getConnectionId(); + return (aId > bId) ? 1 : (aId < bId) ? -1 : 0; + }); + onAdded(); + }); + } + + public function addJoiningClientGhost(cc:GameConnection, onAdded:() -> Void) { + this.initMarble(cc, () -> { + var addedMarble = clientMarbles.get(cc); + this.restart(addedMarble); // spawn it + // this.playGui.addPlayer(cc.id, cc.getName(), false); + // this.playGui.redrawPlayerList(); + + // Sort all the marbles so that they are updated in a deterministic order + this.marbles.sort((a, b) -> @:privateAccess { + var aId = a.getConnectionId(); + var bId = b.getConnectionId(); + return (aId > bId) ? 1 : (aId < bId) ? -1 : 0; + }); + onAdded(); + }); + } + + public function restartMultiplayerState() { + if (this.isMultiplayer) { + serverStartTicks = 0; + startTime = 1e8; + lastMoves = new MarbleUpdateQueue(); + predictions = new MarblePredictionStore(); + powerupPredictions = new PowerupPredictionStore(); + gemPredictions = new GemPredictionStore(); + } } public function restart(marble:Marble, full:Bool = false) { @@ -480,7 +619,7 @@ class MarbleWorld extends Scheduler { this.timeState.ticks = 0; this.bonusTime = 0; this.marble.outOfBounds = false; - this.blastAmount = 0; + this.marble.blastAmount = 0; this.marble.outOfBoundsTime = null; this.finishTime = null; if (this.alarmSound != null) { @@ -541,14 +680,12 @@ class MarbleWorld extends Scheduler { } } } + this.cancel(this.oobSchedule); this.cancel(this.marble.oobSchedule); var startquat = this.gameMode.getSpawnTransform(); - this.marble.setPosition(startquat.position.x, startquat.position.y, startquat.position.z + 3); - var oldtransform = this.marble.collider.transform.clone(); - oldtransform.setPosition(startquat.position); - this.marble.collider.setTransform(oldtransform); + this.marble.setMarblePosition(startquat.position.x, startquat.position.y, startquat.position.z); this.marble.reset(); var euler = startquat.orientation.toEuler(); @@ -562,6 +699,17 @@ class MarbleWorld extends Scheduler { this.marble.mode = Start; sky.follow = marble.camera; + if (isMultiplayer) { + for (client => marble in clientMarbles) { + this.cancel(marble.oobSchedule); + var marbleStartQuat = this.gameMode.getSpawnTransform(); + marble.setMarblePosition(marbleStartQuat.position.x, marbleStartQuat.position.y, marbleStartQuat.position.z); + marble.reset(); + marble.setMode(Start); + } + // this.playGui.resetPlayerScores(); + } + var missionInfo:MissionElementScriptObject = cast this.mission.root.elements.filter((element) -> element._type == MissionElementType.ScriptObject && element._name == "MissionInfo")[0]; if (missionInfo.starthelptext != null) @@ -584,7 +732,11 @@ class MarbleWorld extends Scheduler { Console.log("State Start"); this.clearSchedule(); - this.gameMode.onRestart(); + if (!this.isMultiplayer) + this.gameMode.onRestart(); + if (Net.isClient) { + this.gameMode.onClientRestart(); + } return 0; } @@ -605,17 +757,17 @@ class MarbleWorld extends Scheduler { marble.camera.nextCameraYaw = marble.camera.CameraYaw; marble.camera.nextCameraPitch = marble.camera.CameraPitch; marble.camera.oob = false; - // if (isMultiplayer) { - // marble.megaMarbleUseTick = 0; - // marble.helicopterUseTick = 0; - // marble.collider.radius = marble._radius = 0.3; - // // @:privateAccess marble.netFlags |= MarbleNetFlags.DoHelicopter | MarbleNetFlags.DoMega | MarbleNetFlags.GravityChange; - // } else { - @:privateAccess marble.helicopterEnableTime = -1e8; - @:privateAccess marble.megaMarbleEnableTime = -1e8; - @:privateAccess marble.shockAbsorberEnableTime = -1e8; - @:privateAccess marble.superBounceEnableTime = -1e8; - // } + if (isMultiplayer) { + marble.megaMarbleUseTick = 0; + marble.helicopterUseTick = 0; + marble.collider.radius = marble._radius = 0.3; + @:privateAccess marble.netFlags |= MarbleNetFlags.DoHelicopter | MarbleNetFlags.DoMega | MarbleNetFlags.GravityChange; + } else { + @:privateAccess marble.helicopterEnableTime = -1e8; + @:privateAccess marble.megaMarbleEnableTime = -1e8; + @:privateAccess marble.shockAbsorberEnableTime = -1e8; + @:privateAccess marble.superBounceEnableTime = -1e8; + } if (this.isRecording) { this.replay.recordCameraState(marble.camera.CameraYaw, marble.camera.CameraPitch); this.replay.recordMarbleInput(0, 0); @@ -630,36 +782,53 @@ class MarbleWorld extends Scheduler { if (marble == this.marble) this.playGui.setCenterText(''); - // if (!this.isMultiplayer) - this.clearSchedule(); + if (!this.isMultiplayer) + this.clearSchedule(); marble.outOfBounds = false; this.gameMode.onRespawn(marble); if (marble == this.marble && @:privateAccess !marble.isNetUpdate) AudioManager.playSound(ResourceLoader.getResource('data/sound/spawn.wav', ResourceLoader.getAudio, this.soundResources)); } + public function allClientsReady() { + NetCommands.setStartTicks(this.timeState.ticks); + this.gameMode.onRestart(); + } + public function updateGameState() { if (this.marble.outOfBounds) return; // We will update state manually - if (this.timeState.currentAttemptTime < 0.5 && this.finishTime == null) { - this.playGui.setCenterText('none'); - this.marble.mode = Start; - } - if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 2) && this.finishTime == null) { - this.playGui.setCenterText('ready'); - this.marble.mode = Start; - } - if ((this.timeState.currentAttemptTime >= 2) && (this.timeState.currentAttemptTime < 3.5) && this.finishTime == null) { - this.playGui.setCenterText('set'); - this.marble.mode = Start; - } - if ((this.timeState.currentAttemptTime >= 3.5) && (this.timeState.currentAttemptTime < 5.5) && this.finishTime == null) { - this.playGui.setCenterText('go'); - this.marble.mode = Play; - } - if (this.timeState.currentAttemptTime >= 5.5 && this.finishTime == null) { - this.playGui.setCenterText('none'); - this.marble.mode = Play; + if (!this.isMultiplayer) { + if (this.timeState.currentAttemptTime < 0.5 && this.finishTime == null) { + this.playGui.setCenterText('none'); + this.marble.mode = Start; + } + if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 2) && this.finishTime == null) { + this.playGui.setCenterText('ready'); + this.marble.mode = Start; + } + if ((this.timeState.currentAttemptTime >= 2) && (this.timeState.currentAttemptTime < 3.5) && this.finishTime == null) { + this.playGui.setCenterText('set'); + this.marble.mode = Start; + } + if ((this.timeState.currentAttemptTime >= 3.5) && (this.timeState.currentAttemptTime < 5.5) && this.finishTime == null) { + this.playGui.setCenterText('go'); + this.marble.mode = Play; + } + if (this.timeState.currentAttemptTime >= 5.5 && this.finishTime == null) { + this.playGui.setCenterText('none'); + this.marble.mode = Play; + } + } else { + if (!this.multiplayerStarted && this.finishTime == null) { + if ((Net.isHost && (this.timeState.timeSinceLoad >= startTime)) // 3.5 == 109 ticks + || (Net.isClient && this.serverStartTicks != 0 && @:privateAccess this.marble.serverTicks >= this.serverStartTicks + 109)) { + this.multiplayerStarted = true; + this.marble.setMode(Play); + for (client => marble in this.clientMarbles) + marble.setMode(Play); + } + } } } @@ -929,6 +1098,13 @@ class MarbleWorld extends Scheduler { var worker = new ResourceLoaderWorker(() -> { obj.idInLevel = this.dtsObjects.length; // Set the id of the thing this.dtsObjects.push(obj); + if (obj is PowerUp) { + var pw:PowerUp = cast obj; + pw.netIndex = this.powerUps.length; + this.powerUps.push(cast obj); + if (Net.isClient) + powerupPredictions.alloc(); + } if (obj is ForceObject) { this.forceObjects.push(cast obj); } @@ -978,10 +1154,10 @@ class MarbleWorld extends Scheduler { }); } - public function addMarble(marble:Marble, onFinish:Void->Void) { + public function addMarble(marble:Marble, client:GameConnection, onFinish:Void->Void) { marble.level = cast this; if (marble.controllable) { - marble.init(cast this, () -> { + marble.init(cast this, client, () -> { this.marbles.push(marble); this.scene.addChild(marble.camera); this.marble = marble; @@ -989,12 +1165,21 @@ class MarbleWorld extends Scheduler { // sky.follow = marble; sky.follow = marble.camera; this.collisionWorld.addMovingEntity(marble.collider); + this.collisionWorld.addMarbleEntity(marble.collider); this.scene.addChild(marble); onFinish(); }); } else { - this.collisionWorld.addMovingEntity(marble.collider); - this.scene.addChild(marble); + marble.init(cast this, client, () -> { + this.marbles.push(marble); + marble.collisionWorld = this.collisionWorld; + this.collisionWorld.addMovingEntity(marble.collider); + this.collisionWorld.addMarbleEntity(marble.collider); + this.scene.addChild(marble); + if (client != null) + clientMarbles.set(client, marble); + onFinish(); + }); } } @@ -1016,6 +1201,300 @@ 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); + if (@:privateAccess hunt.activeGemSpawnGroup != null) { + 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()); + } + + // Marble states + for (marb in this.marbles) { + var oldFlags = @:privateAccess marb.netFlags; + @:privateAccess marb.netFlags = MarbleNetFlags.DoBlast | MarbleNetFlags.DoMega | MarbleNetFlags.DoHelicopter | MarbleNetFlags.PickupPowerup | MarbleNetFlags.GravityChange | MarbleNetFlags.UsePowerup; + + var innerMove = @:privateAccess marb.lastMove; + if (innerMove == null) { + innerMove = new Move(); + innerMove.d = new Vector(0, 0); + } + var motionDir = @:privateAccess marb.moveMotionDir; + if (motionDir == null) { + motionDir = marb.getMarbleAxis()[1]; + } + + var move = new NetMove(innerMove, motionDir, timeState, timeState.ticks, 65535); + + packets.push(@:privateAccess marb.packUpdate(move, timeState)); + + @:privateAccess marb.netFlags = oldFlags; + } + + // Powerup states + for (powerup in this.powerUps) { + if (powerup.currentOpacity != 1.0) { // it must be picked up or something + if (@:privateAccess powerup.pickupClient != -1) { + var b = new OutputBitStream(); + b.writeByte(NetPacketType.PowerupPickup); + var pickupPacket = new PowerupPickupPacket(); + pickupPacket.clientId = @:privateAccess powerup.pickupClient; + pickupPacket.serverTicks = @:privateAccess powerup.pickupTicks; + pickupPacket.powerupItemId = powerup.netIndex; + pickupPacket.serialize(b); + packets.push(b.getBytes()); + } + } + } + + // Scoreboard! + // var b = new OutputBitStream(); + // b.writeByte(NetPacketType.ScoreBoardInfo); + // var sbPacket = new ScoreboardPacket(); + // for (player in @:privateAccess this.playGui.playerList) { + // sbPacket.scoreBoard.set(player.id, player.score); + // } + // sbPacket.serialize(b); + // packets.push(b.getBytes()); + + return packets; + } + + public function applyReceivedMoves() { + var needsPrediction = 0; + if (!lastMoves.ourMoveApplied) { + var ourMove = lastMoves.myMarbleUpdate; + if (ourMove != null) { + var ourMoveStruct = Net.clientConnection.acknowledgeMove(ourMove.move, timeState); + lastMoves.ourMoveApplied = true; + for (client => arr in lastMoves.otherMarbleUpdates) { + var lastMove = null; + while (arr.packets.length > 0) { + var p = arr.packets[0]; + if (p.serverTicks <= ourMove.serverTicks) { + lastMove = arr.packets.shift(); + } else { + break; + } + } + if (lastMove != null) { + // clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + // needsPrediction |= 1 << client; + // arr.insert(0, lastMove); + var clientMarble = clientMarbles[Net.clientIdMap[client]]; + if (clientMarble != null) { + if (ourMove.serverTicks == lastMove.serverTicks) { + if (ourMoveStruct != null) { + var otherPred = predictions.retrieveState(clientMarble, ourMoveStruct.timeState.ticks); + if (otherPred != null) { + if (otherPred.getError(lastMove) > 0.01) { + // Debug.drawSphere(@:privateAccess clientMarbles[Net.clientIdMap[client]].newPos, 0.2, 0.5); + // trace('Prediction error: ${otherPred.getError(lastMove)}'); + // trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + clientMarble.unpackUpdate(lastMove); + needsPrediction |= 1 << client; + arr.packets.insert(0, lastMove); + predictions.clearStatesAfterTick(clientMarbles[Net.clientIdMap[client]], ourMoveStruct.timeState.ticks); + } + } else { + // Debug.drawSphere(@:privateAccess clientMarbles[Net.clientIdMap[client]].newPos, 0.2, 0.5); + // trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + clientMarble.unpackUpdate(lastMove); + needsPrediction |= 1 << client; + arr.packets.insert(0, lastMove); + predictions.clearStatesAfterTick(clientMarble, ourMoveStruct.timeState.ticks); + } + } else { + // Debug.drawSphere(@:privateAccess clientMarbles[Net.clientIdMap[client]].newPos, 0.2, 0.5); + // trace('Desync in General'); + clientMarble.unpackUpdate(lastMove); + needsPrediction |= 1 << client; + arr.packets.insert(0, lastMove); + // predictions.clearStatesAfterTick(clientMarbles[Net.clientIdMap[client]], ourMoveStruct.timeState.ticks); + } + } + } + } + } + // marble.unpackUpdate(ourMove); + // needsPrediction |= 1 << Net.clientId; + if (ourMoveStruct != null) { + var ourPred = predictions.retrieveState(marble, ourMoveStruct.timeState.ticks); + if (ourPred != null) { + if (ourPred.getError(ourMove) > 0.01) { + // trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + predictions.clearStatesAfterTick(marble, ourMoveStruct.timeState.ticks); + } + } else { + // trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + predictions.clearStatesAfterTick(marble, ourMoveStruct.timeState.ticks); + } + } else { + // trace('Desync in General'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + // predictions.clearStatesAfterTick(marble, ourMoveStruct.timeState.ticks); + } + } + } + return needsPrediction; + } + + public function applyClientPrediction(marbleNeedsPrediction:Int) { + // First acknowledge the marble's last move so we can get that over with + var ourLastMove = lastMoves.myMarbleUpdate; + if (ourLastMove == null || marbleNeedsPrediction == 0) + return -1; + var ackLag = @:privateAccess Net.clientConnection.getQueuedMovesLength(); + + var ourLastMoveTime = ourLastMove.serverTicks; + + var ourQueuedMoves = @:privateAccess Net.clientConnection.getQueuedMoves().copy(); + + var qm = ourQueuedMoves[0]; + var advanceTimeState = qm != null ? qm.timeState.clone() : timeState.clone(); + advanceTimeState.dt = 0.032; + advanceTimeState.ticks = ourLastMoveTime; + + // if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { // Only for our clients pls + // if (qm != null) { + // var mvs = qm.powerupStates.copy(); + for (pw in marble.level.powerUps) { + // var val = mvs.shift(); + // if (pw.lastPickUpTime != val) + // Console.log('Revert powerup pickup: ${pw.lastPickUpTime} -> ${val}'); + + if (pw.pickupClient != -1 && marbleNeedsPrediction & (1 << pw.pickupClient) > 0) + pw.lastPickUpTime = powerupPredictions.getState(pw.netIndex); + } + var huntMode:HuntMode = cast this.gameMode; + if (@:privateAccess huntMode.activeGemSpawnGroup != null) { + for (activeGem in @:privateAccess huntMode.activeGemSpawnGroup) { + var g = @:privateAccess huntMode.gemSpawnPoints[activeGem].gem; + if (g != null && g.pickUpClient != -1 && marbleNeedsPrediction & (1 << g.pickUpClient) > 0) + huntMode.setGemHiddenStatus(activeGem, gemPredictions.getState(activeGem)); + } + } + // } + // } + + ackLag = ourQueuedMoves.length; + + // Tick the remaining moves (ours) + @:privateAccess this.marble.isNetUpdate = true; + var totalTicksToDo = ourQueuedMoves.length; + var endTick = ourLastMoveTime + totalTicksToDo; + var currentTick = ourLastMoveTime; + //- Std.int(ourLastMove.moveQueueSize - @:privateAccess Net.clientConnection.moveManager.ackRTT); // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT)) - offset; + + var marblesToTick = new Map(); + + for (client => arr in lastMoves.otherMarbleUpdates) { + if (marbleNeedsPrediction & (1 << client) > 0 && arr.packets.length > 0) { + var m = arr.packets[0]; + // if (m.serverTicks == ourLastMoveTime) { + var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + if (@:privateAccess marbleToUpdate.newPos == null) + continue; + // Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + + // var distFromUs = @:privateAccess marbleToUpdate.newPos.distance(this.marble.newPos); + // if (distFromUs < 5) // { + m.calculationTicks = ourQueuedMoves.length; + @:privateAccess marbleToUpdate.posStore.load(marbleToUpdate.newPos); + @:privateAccess marbleToUpdate.netCorrected = true; + // } else { + // m.calculationTicks = Std.int(Math.max(1, ourQueuedMoves.length - (distFromUs - 5) / 3)); + // } + // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT - ourLastMove.moveQueueSize) / 2); + + marblesToTick.set(client, m); + arr.packets.shift(); + // } + } + } + + Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + // var syncTickStates = new Map(); + + @:privateAccess this.marble.posStore.load(this.marble.newPos); + @:privateAccess this.marble.netCorrected = true; + + for (move in ourQueuedMoves) { + var m = move.move; + Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { + @:privateAccess this.marble.moveMotionDir = move.motionDir; + @:privateAccess this.marble.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); + this.predictions.storeState(this.marble, move.timeState.ticks); + } + // var collidings = @:privateAccess this.marble.contactEntities.filter(x -> x is SphereCollisionEntity); + + for (client => m in marblesToTick) { + if (m.calculationTicks > 0) { + var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + + var mv = m.move.move; + @:privateAccess marbleToUpdate.isNetUpdate = true; + @:privateAccess marbleToUpdate.moveMotionDir = m.move.motionDir; + @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, mv, this.collisionWorld, this.pathedInteriors); + this.predictions.storeState(marbleToUpdate, move.timeState.ticks); + @:privateAccess marbleToUpdate.isNetUpdate = false; + m.calculationTicks--; + } + } + advanceTimeState.currentAttemptTime += 0.032; + advanceTimeState.ticks++; + currentTick++; + } + + lastMoves.ourMoveApplied = true; + @:privateAccess this.marble.isNetUpdate = false; + return advanceTimeState.ticks; + + return -1; + } + + public function spawnHuntGemsClientSide(gemIds:Array) { + if (this.isMultiplayer && Net.isClient) { + var huntMode:HuntMode = cast this.gameMode; + huntMode.setActiveSpawnSphere(gemIds); + // radar.blink(); + } + } + + public function removePlayer(cc:GameConnection) { + var otherMarble = this.clientMarbles[cc]; + if (otherMarble != null) { + cancel(otherMarble.oobSchedule); + this.predictions.removeMarbleFromPrediction(otherMarble); + this.scene.removeChild(otherMarble); + this.collisionWorld.removeMarbleEntity(otherMarble.collider); + this.collisionWorld.removeMovingEntity(otherMarble.collider); + // this.playGui.removePlayer(cc.id); + this.clientMarbles.remove(cc); + otherMarble.dispose(); + this.marbles.remove(otherMarble); + } + } + public function update(dt:Float) { if (!_ready) { return; @@ -1027,6 +1506,7 @@ class MarbleWorld extends Scheduler { || MarbleGame.instance.touchInput.rewindButton.pressed || Gamepad.isDown(Settings.gamepadSettings.rewind)) && Settings.optionsSettings.rewindEnabled + && !this.isMultiplayer && !this.isWatching && this.finishTime == null) { this.rewinding = true; @@ -1034,6 +1514,7 @@ class MarbleWorld extends Scheduler { if ((Key.isReleased(Settings.controlsSettings.rewind) || !MarbleGame.instance.touchInput.rewindButton.pressed || Gamepad.isReleased(Settings.gamepadSettings.rewind)) + && !this.isMultiplayer && this.rewinding) { if (this.isRecording) { this.replay.spliceReplay(timeState.currentAttemptTime); @@ -1104,14 +1585,14 @@ class MarbleWorld extends Scheduler { || Gamepad.isPressed(Settings.gamepadSettings.blast) && !this.isWatching && this.game == "ultra") { - this.marble.useBlast(); + this.marble.useBlast(timeState); if (this.isRecording) { this.replay.recordMarbleStateFlags(false, false, false, true); } } if (this.isWatching && this.replay.currentPlaybackFrame.marbleStateFlags.has(UsedBlast)) - this.marble.useBlast(); + this.marble.useBlast(timeState); // Replay gravity if (this.isWatching) { @@ -1124,7 +1605,8 @@ class MarbleWorld extends Scheduler { } this.updateGameState(); - this.updateBlast(timeState); + if (!this.isMultiplayer) + this.updateBlast(this.marble, timeState); ProfilerUI.measure("updateDTS"); for (obj in dtsObjects) { obj.update(timeState); @@ -1133,8 +1615,81 @@ class MarbleWorld extends Scheduler { obj.update(timeState); } ProfilerUI.measure("updateMarbles"); - for (marble in marbles) { - marble.update(timeState, collisionWorld, this.pathedInteriors); + if (this.isMultiplayer) { + tickAccumulator += timeState.dt; + while (tickAccumulator >= 0.032) { + // Apply the server side ticks + var lastPredTick = -1; + if (Net.isClient) { + var marbleNeedsTicking = applyReceivedMoves(); + // Catch up + lastPredTick = applyClientPrediction(marbleNeedsTicking); + } + + // Do the clientside prediction sim + var fixedDt = timeState.clone(); + fixedDt.dt = 0.032; + tickAccumulator -= 0.032; + var packets = []; + var otherMoves = []; + var myMove = null; + + for (marble in marbles) { + var move = marble.updateServer(fixedDt, collisionWorld, pathedInteriors); + if (marble == this.marble) + myMove = move; + else + otherMoves.push(move); + } + + if (myMove != null && Net.isClient) { + this.predictions.storeState(marble, myMove.timeState.ticks); + for (client => marble in clientMarbles) { + this.predictions.storeState(marble, myMove.timeState.ticks); + } + } + if (Net.isHost) { + packets.push(marble.packUpdate(myMove, fixedDt)); + for (othermarble in marbles) { + if (othermarble != this.marble) { + var mv = otherMoves.shift(); + packets.push(othermarble.packUpdate(mv, fixedDt)); + } + } + // for (client => othermarble in clientMarbles) { // Oh no! + // var mv = otherMoves.shift(); + // packets.push(marble.packUpdate(myMove, fixedDt)); + // packets.push(othermarble.packUpdate(mv, fixedDt)); + // } + var allRecv = true; + for (client => marble in clientMarbles) { // Oh no! + // var pktClone = packets.copy(); + // pktClone.sort((a, b) -> { + // return (a.c == client.id) ? 1 : (b.c == client.id) ? -1 : 0; + // }); + if (client.state != GAME) { + allRecv = false; + continue; // Only send if in game + } + marble.clearNetFlags(); + for (packet in packets) { + client.sendBytes(packet); + } + } + if (allRecv) + this.marble.clearNetFlags(); + } + timeState.ticks++; + } + timeState.subframe = tickAccumulator / 0.032; + marble.updateClient(timeState, this.pathedInteriors); + for (client => marble in clientMarbles) { + marble.updateClient(timeState, this.pathedInteriors); + } + } else { + for (marble in marbles) { + marble.update(timeState, collisionWorld, this.pathedInteriors); + } } if (this.rewinding) { // Update camera separately @@ -1151,12 +1706,14 @@ class MarbleWorld extends Scheduler { ProfilerUI.measure("updateAudio"); AudioManager.update(this.scene); - if (this.marble.outOfBounds - && this.finishTime == null - && (Key.isDown(Settings.controlsSettings.powerup) || Gamepad.isDown(Settings.gamepadSettings.powerup)) - && !this.isWatching) { - this.restart(this.marble); - return; + if (!this.isMultiplayer) { + if (this.marble.outOfBounds + && this.finishTime == null + && (Key.isDown(Settings.controlsSettings.powerup) || Gamepad.isDown(Settings.gamepadSettings.powerup)) + && !this.isWatching) { + this.restart(this.marble); + return; + } } if (!this.isWatching) { @@ -1165,7 +1722,7 @@ class MarbleWorld extends Scheduler { } } - if (!this.rewinding && Settings.optionsSettings.rewindEnabled) + if (!this.rewinding && Settings.optionsSettings.rewindEnabled && !this.isMultiplayer) this.rewindManager.recordFrame(); _instancesNeedsUpdate = true; @@ -1271,12 +1828,31 @@ class MarbleWorld extends Scheduler { if (alarmSound != null) alarmSound.pause = false; } - if (this.timeState.currentAttemptTime >= 3.5) { + if (!this.isMultiplayer) { + if (this.timeState.currentAttemptTime >= 3.5) { + this.timeState.gameplayClock += dt * timeMultiplier; + } else if (this.timeState.currentAttemptTime + dt >= 3.5) { + 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 (gameplayHigh < this.timeState.gameplayClock || gameplayLow > this.timeState.gameplayClock) { + var clockTicks = Math.floor((ourStartTime - this.timeState.gameplayClock) / 0.032); + var clockTickTime = ourStartTime - clockTicks * 0.032; + var delta = clockTickTime - this.timeState.gameplayClock; + this.timeState.gameplayClock = gameplayHigh - delta; + } + } + this.timeState.gameplayClock += dt * timeMultiplier; - } else if (this.timeState.currentAttemptTime + dt >= 3.5) { - this.timeState.gameplayClock += ((this.timeState.currentAttemptTime + dt) - 3.5) * timeMultiplier; } - if (this.timeState.gameplayClock < 0) + if (this.timeState.gameplayClock < 0 && !Net.isClient) this.gameMode.onTimeExpire(); } this.timeState.currentAttemptTime += dt; @@ -1327,12 +1903,12 @@ class MarbleWorld extends Scheduler { this.replay.recordTimeState(timeState.currentAttemptTime, timeState.gameplayClock, this.bonusTime); } - function updateBlast(timestate:TimeState) { + public function updateBlast(marble:Marble, timestate:TimeState) { if (this.game == "ultra") { - if (this.blastAmount < 1) { - this.blastAmount = Util.clamp(this.blastAmount + (timeState.dt / 25), 0, 1); + if (marble.blastAmount < 1) { + marble.blastAmount = Util.clamp(marble.blastAmount + (timeState.dt / 25), 0, 1); } - this.playGui.setBlastValue(this.blastAmount); + this.playGui.setBlastValue(marble.blastAmount); } } @@ -1561,6 +2137,23 @@ class MarbleWorld extends Scheduler { } } + function mpFinish() { + // playGui.setGuiVisibility(false); + Console.log("State End"); + #if js + var pointercontainer = js.Browser.document.querySelector("#pointercontainer"); + pointercontainer.hidden = false; + #end + if (Util.isTouchDevice()) { + MarbleGame.instance.touchInput.setControlsEnabled(false); + } + this.setCursorLock(false); + if (Net.isHost) { + MarbleGame.instance.quitMission(); + } + return 0; + } + function showFinishScreen() { if (this.isWatching) return 0; @@ -1652,6 +2245,8 @@ class MarbleWorld extends Scheduler { return false; Console.log("PowerUp pickup: " + powerUp.identifier); marble.heldPowerup = powerUp; + if (@:privateAccess !marble.isNetUpdate) + @:privateAccess marble.netFlags |= MarbleNetFlags.PickupPowerup; if (this.marble == marble) { this.playGui.setPowerupImage(powerUp.identifier); MarbleGame.instance.touchInput.powerupButton.setEnabled(true); @@ -1664,6 +2259,7 @@ class MarbleWorld extends Scheduler { public function deselectPowerUp(marble:Marble) { marble.heldPowerup = null; + @:privateAccess marble.netFlags |= MarbleNetFlags.PickupPowerup; if (this.marble == marble) { this.playGui.setPowerupImage(""); MarbleGame.instance.touchInput.powerupButton.setEnabled(false); @@ -1683,6 +2279,10 @@ class MarbleWorld extends Scheduler { /** Get the current interpolated orientation quaternion. */ public function getOrientationQuat(time:Float) { + if (time < this.orientationChangeTime) + return this.oldOrientationQuat; + if (time > this.orientationChangeTime + 0.3) + return this.newOrientationQuat; var completion = Util.clamp((time - this.orientationChangeTime) / 0.3, 0, 1); var q = this.oldOrientationQuat.clone(); q.slerp(q, this.newOrientationQuat, completion); @@ -1692,6 +2292,9 @@ class MarbleWorld extends Scheduler { public function setUp(marble:Marble, vec:Vector, timeState:TimeState, instant:Bool = false) { if (marble.currentUp == vec) return; + if (isMultiplayer && Net.isHost) { + @:privateAccess marble.netFlags |= MarbleNetFlags.GravityChange; + } marble.currentUp = vec; if (marble == this.marble) { var currentQuat = this.getOrientationQuat(timeState.currentAttemptTime); @@ -1750,7 +2353,7 @@ class MarbleWorld extends Scheduler { marble.outOfBounds = true; marble.outOfBoundsTime = this.timeState.clone(); marble.camera.oob = true; - if (!this.isWatching) { + if (!this.isWatching && !this.isMultiplayer) { Settings.playStatistics.oobs++; if (!Settings.levelStatistics.exists(mission.path)) { Settings.levelStatistics.set(mission.path, { @@ -1766,17 +2369,21 @@ class MarbleWorld extends Scheduler { } // sky.follow = null; // this.oobCameraPosition = camera.position.clone(); - playGui.setCenterText('outofbounds'); - AudioManager.playSound(ResourceLoader.getResource('data/sound/whoosh.wav', ResourceLoader.getAudio, this.soundResources)); - // if (this.replay.mode != = 'playback') - this.oobSchedule = this.schedule(this.timeState.currentAttemptTime + 2, () -> { - playGui.setCenterText('none'); - return null; - }); - marble.oobSchedule = this.schedule(this.timeState.currentAttemptTime + 2.5, () -> { - this.restart(marble); - return null; - }); + if (marble == this.marble) { + playGui.setCenterText('outofbounds'); + AudioManager.playSound(ResourceLoader.getResource('data/sound/whoosh.wav', ResourceLoader.getAudio, this.soundResources)); + // if (this.replay.mode != = 'playback') + this.oobSchedule = this.schedule(this.timeState.currentAttemptTime + 2, () -> { + playGui.setCenterText('none'); + return null; + }); + } + if (!this.isMultiplayer || Net.isHost) { + marble.oobSchedule = this.schedule(this.timeState.currentAttemptTime + 2.5, () -> { + this.restart(marble); + return null; + }); + } } /** Sets a new active checkpoint. */ @@ -1800,7 +2407,7 @@ class MarbleWorld extends Scheduler { this.currentCheckpointTrigger = trigger; this.checkpointCollectedGems.clear(); this.checkpointUp = this.marble.currentUp.clone(); - this.cheeckpointBlast = this.blastAmount; + this.cheeckpointBlast = this.marble.blastAmount; // Remember all gems that were collected up to this point for (gem in this.gems) { if (gem.pickedUp) @@ -1855,7 +2462,7 @@ class MarbleWorld extends Scheduler { @:privateAccess this.marble.shockAbsorberEnableTime = -1e8; @:privateAccess this.marble.helicopterEnableTime = -1e8; @:privateAccess this.marble.megaMarbleEnableTime = -1e8; - this.blastAmount = this.cheeckpointBlast; + this.marble.blastAmount = this.cheeckpointBlast; if (this.isRecording) { this.replay.recordCameraState(this.marble.camera.CameraYaw, this.marble.camera.CameraPitch); this.replay.recordMarbleInput(0, 0); @@ -1971,6 +2578,8 @@ class MarbleWorld extends Scheduler { this.playGui.dispose(); scene.removeChildren(); + CollisionPool.freeMemory(); + for (interior in this.interiors) { interior.dispose(); } diff --git a/src/MissionList.hx b/src/MissionList.hx index 61f78be8..3d8dfa32 100644 --- a/src/MissionList.hx +++ b/src/MissionList.hx @@ -1,8 +1,11 @@ +package src; + import haxe.Json; import mis.MisParser; import src.ResourceLoader; import src.Mission; import src.Console; +import src.MissionList; @:publicFields class MissionList { diff --git a/src/gui/AchievementsGui.hx b/src/gui/AchievementsGui.hx index e2a331f3..02d2b819 100644 --- a/src/gui/AchievementsGui.hx +++ b/src/gui/AchievementsGui.hx @@ -6,6 +6,7 @@ import src.ResourceLoader; import src.MarbleGame; import src.Settings; import src.Mission; +import src.MissionList; class AchievementsGui extends GuiImage { public function new() { diff --git a/src/gui/Canvas.hx b/src/gui/Canvas.hx index 9414e126..c2a428c2 100644 --- a/src/gui/Canvas.hx +++ b/src/gui/Canvas.hx @@ -10,6 +10,7 @@ import gui.GuiControl.MouseState; class Canvas extends GuiControl { var scene2d:Scene; var marbleGame:MarbleGame; + var content:GuiControl; public function new(scene, marbleGame:MarbleGame) { super(); @@ -25,6 +26,7 @@ class Canvas extends GuiControl { public function setContent(content:GuiControl) { this.dispose(); + this.content = content; this.addChild(content); this.render(scene2d); } diff --git a/src/gui/Graphics.hx b/src/gui/Graphics.hx new file mode 100644 index 00000000..7a2389af --- /dev/null +++ b/src/gui/Graphics.hx @@ -0,0 +1,904 @@ +package gui; + +import h2d.RenderContext; +import h2d.impl.BatchDrawState; +import hxd.Math; +import hxd.impl.Allocator; +import h2d.Drawable; + +private typedef GraphicsPoint = hxd.poly2tri.Point; + +@:dox(hide) +class GPoint { + public var x:Float; + public var y:Float; + public var r:Float; + public var g:Float; + public var b:Float; + public var a:Float; + + public function new() {} + + public function load(x, y, r, g, b, a) { + this.x = x; + this.y = y; + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } +} + +private class GraphicsContent extends h3d.prim.Primitive { + var tmp:hxd.FloatBuffer; + var index:hxd.IndexBuffer; + var state:BatchDrawState; + + var bufferDirty:Bool; + var indexDirty:Bool; + #if track_alloc + var allocPos:hxd.impl.AllocPos; + #end + + var bufferSize:Int; + var ibufferSize:Int; + + public function new() { + state = new BatchDrawState(); + #if track_alloc + this.allocPos = new hxd.impl.AllocPos(); + #end + } + + public inline function addIndex(i) { + index.push(i); + state.add(1); + indexDirty = true; + } + + public inline function add(x:Float, y:Float, u:Float, v:Float, r:Float, g:Float, b:Float, a:Float) { + tmp.push(x); + tmp.push(y); + tmp.push(u); + tmp.push(v); + tmp.push(r); + tmp.push(g); + tmp.push(b); + tmp.push(a); + bufferDirty = true; + } + + public function setTile(tile:h2d.Tile) { + state.setTile(tile); + } + + public function next() { + var nvect = tmp.length >> 3; + if (nvect < 1 << 15) + return false; + tmp = new hxd.FloatBuffer(); + index = new hxd.IndexBuffer(); + var tex = state.currentTexture; + state = new BatchDrawState(); + state.setTexture(tex); + super.dispose(); + return true; + } + + override function alloc(engine:h3d.Engine) { + if (index.length <= 0) + return; + var alloc = Allocator.get(); + buffer = alloc.ofFloats(tmp, 8, RawFormat); + bufferSize = tmp.length; + #if track_alloc + @:privateAccess buffer.allocPos = allocPos; + #end + indexes = alloc.ofIndexes(index); + ibufferSize = index.length; + bufferDirty = false; + indexDirty = false; + } + + public function doRender(ctx:h2d.RenderContext) { + if (index.length == 0) + return; + flush(); + state.drawIndexed(ctx, buffer, indexes, 0, tmp.length >> 3); + } + + public function flush() { + if (buffer == null || buffer.isDisposed()) { + alloc(h3d.Engine.getCurrent()); + } else { + var allocator = Allocator.get(); + if (bufferDirty) { + if (tmp.length > bufferSize) { + allocator.disposeBuffer(buffer); + + buffer = new h3d.Buffer(tmp.length >> 3, 8, [RawFormat, Dynamic]); + buffer.uploadVector(tmp, 0, tmp.length >> 3); + bufferSize = tmp.length; + } else { + buffer.uploadVector(tmp, 0, tmp.length >> 3); + } + bufferDirty = false; + } + if (indexDirty) { + if (index.length > ibufferSize) { + allocator.disposeIndexBuffer(indexes); + indexes = allocator.ofIndexes(index); + ibufferSize = index.length; + } else { + indexes.upload(index, 0, index.length); + } + indexDirty = false; + } + } + } + + override function dispose() { + state.clear(); + // disposeBuffers(); + + // super.dispose(); + } + + function disposeBuffers() { + if (buffer != null) { + Allocator.get().disposeBuffer(buffer); + buffer = null; + } + if (indexes != null) { + Allocator.get().disposeIndexBuffer(indexes); + indexes = null; + } + } + + public function clear() { + dispose(); + tmp = new hxd.FloatBuffer(); + index = new hxd.IndexBuffer(); + } + + public function disposeForReal() { + state.clear(); + disposeBuffers(); + super.dispose(); + } +} + +/** + A simple interface to draw arbitrary 2D geometry. + + Usage notes: + * While Graphics allows for multiple unique textures, each texture swap causes a new drawcall, + and due to that it's recommended to minimize the amount of used textures per Graphics instance, + ideally limiting to only one texture. + * Due to how Graphics operate, removing them from the active `h2d.Scene` will cause a loss of all data. +**/ +class Graphics extends Drawable { + var content:GraphicsContent; + var tmpPoints:Array; + var pindex:Int; + var curR:Float; + var curG:Float; + var curB:Float; + var curA:Float; + var lineSize:Float; + var lineR:Float; + var lineG:Float; + var lineB:Float; + var lineA:Float; + var doFill:Bool; + + var xMin:Float; + var yMin:Float; + var xMax:Float; + var yMax:Float; + var xMinSize:Float; + var yMinSize:Float; + var xMaxSize:Float; + var yMaxSize:Float; + + var ma:Float = 1.; + var mb:Float = 0.; + var mc:Float = 0.; + var md:Float = 1.; + var mx:Float = 0.; + var my:Float = 0.; + + /** + The Tile used as source of Texture to render. + **/ + public var tile:h2d.Tile; + + /** + Adds bevel cut-off at line corners. + + The value is a percentile in range of 0...1, dictating at which point edges get beveled based on their angle. + Value of 0 being not beveled and 1 being always beveled. + **/ + public var bevel = 0.25; // 0 = not beveled, 1 = always beveled + + /** + Create a new Graphics instance. + @param parent An optional parent `h2d.Object` instance to which Graphics adds itself if set. + **/ + public function new(?parent) { + super(parent); + content = new GraphicsContent(); + tile = h2d.Tile.fromColor(0xFFFFFF); + clear(); + } + + override function onRemove() { + super.onRemove(); + clear(); + content.disposeForReal(); + } + + /** + Clears the Graphics contents. + **/ + public function clear() { + content.clear(); + tmpPoints = []; + pindex = 0; + lineSize = 0; + xMin = Math.POSITIVE_INFINITY; + yMin = Math.POSITIVE_INFINITY; + yMax = Math.NEGATIVE_INFINITY; + xMax = Math.NEGATIVE_INFINITY; + xMinSize = Math.POSITIVE_INFINITY; + yMinSize = Math.POSITIVE_INFINITY; + yMaxSize = Math.NEGATIVE_INFINITY; + xMaxSize = Math.NEGATIVE_INFINITY; + } + + override function getBoundsRec(relativeTo, out, forSize) { + super.getBoundsRec(relativeTo, out, forSize); + if (tile != null) { + if (forSize) + addBounds(relativeTo, out, xMinSize, yMinSize, xMaxSize - xMinSize, yMaxSize - yMinSize); + else + addBounds(relativeTo, out, xMin, yMin, xMax - xMin, yMax - yMin); + } + } + + function isConvex(points:Array) { + var first = true, sign = false; + for (i in 0...points.length) { + var p1 = points[i]; + var p2 = points[(i + 1) % points.length]; + var p3 = points[(i + 2) % points.length]; + var s = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0; + if (first) { + first = false; + sign = s; + } else if (sign != s) + return false; + } + return true; + } + + function flushLine(start) { + var pts = tmpPoints; + var last = pts.length - 1; + var prev = pts[last]; + var p = pts[0]; + + content.setTile(h2d.Tile.fromColor(0xFFFFFF)); + var closed = p.x == prev.x && p.y == prev.y; + var count = pts.length; + if (!closed) { + var prevLast = pts[last - 1]; + if (prevLast == null) + prevLast = p; + var gp = new GPoint(); + gp.load(prev.x * 2 - prevLast.x, prev.y * 2 - prevLast.y, 0, 0, 0, 0); + pts.push(gp); + var pNext = pts[1]; + if (pNext == null) + pNext = p; + var gp = new GPoint(); + gp.load(p.x * 2 - pNext.x, p.y * 2 - pNext.y, 0, 0, 0, 0); + prev = gp; + } else if (p != prev) { + count--; + last--; + prev = pts[last]; + } + + for (i in 0...count) { + var next = pts[(i + 1) % pts.length]; + + var nx1 = prev.y - p.y; + var ny1 = p.x - prev.x; + var ns1 = Math.invSqrt(nx1 * nx1 + ny1 * ny1); + + var nx2 = p.y - next.y; + var ny2 = next.x - p.x; + var ns2 = Math.invSqrt(nx2 * nx2 + ny2 * ny2); + + var nx = nx1 * ns1 + nx2 * ns2; + var ny = ny1 * ns1 + ny2 * ns2; + var ns = Math.invSqrt(nx * nx + ny * ny); + + nx *= ns; + ny *= ns; + + var size = nx * nx1 * ns1 + ny * ny1 * ns1; // N.N1 + + // *HACK* we should instead properly detect limits when the angle is too small + if (size < 0.1) + size = 0.1; + + var d = lineSize * 0.5 / size; + nx *= d; + ny *= d; + + if (size > bevel) { + content.add(p.x + nx, p.y + ny, 0, 0, p.r, p.g, p.b, p.a); + content.add(p.x - nx, p.y - ny, 0, 0, p.r, p.g, p.b, p.a); + + var pnext = i == last ? start : pindex + 2; + + if (i < count - 1 || closed) { + content.addIndex(pindex); + content.addIndex(pindex + 1); + content.addIndex(pnext); + + content.addIndex(pindex + 1); + content.addIndex(pnext); + content.addIndex(pnext + 1); + } + pindex += 2; + } else { + // bevel + var n0x = next.x - p.x; + var n0y = next.y - p.y; + var sign = n0x * nx + n0y * ny; + + var nnx = -ny; + var nny = nx; + + var size = nnx * nx1 * ns1 + nny * ny1 * ns1; + var d = lineSize * 0.5 / size; + nnx *= d; + nny *= d; + + var pnext = i == last ? start : pindex + 3; + + if (sign > 0) { + content.add(p.x + nx, p.y + ny, 0, 0, p.r, p.g, p.b, p.a); + content.add(p.x - nnx, p.y - nny, 0, 0, p.r, p.g, p.b, p.a); + content.add(p.x + nnx, p.y + nny, 0, 0, p.r, p.g, p.b, p.a); + + content.addIndex(pindex); + content.addIndex(pnext); + content.addIndex(pindex + 2); + + content.addIndex(pindex + 2); + content.addIndex(pnext); + content.addIndex(pnext + 1); + } else { + content.add(p.x + nnx, p.y + nny, 0, 0, p.r, p.g, p.b, p.a); + content.add(p.x - nx, p.y - ny, 0, 0, p.r, p.g, p.b, p.a); + content.add(p.x - nnx, p.y - nny, 0, 0, p.r, p.g, p.b, p.a); + + content.addIndex(pindex + 1); + content.addIndex(pnext); + content.addIndex(pindex + 2); + + content.addIndex(pindex + 1); + content.addIndex(pnext); + content.addIndex(pnext + 1); + } + + content.addIndex(pindex); + content.addIndex(pindex + 1); + content.addIndex(pindex + 2); + + pindex += 3; + } + + prev = p; + p = next; + } + content.setTile(tile); + } + + static var EARCUT = null; + + function flushFill(i0) { + if (tmpPoints.length < 3) + return; + + var pts = tmpPoints; + var p0 = pts[0]; + var p1 = pts[pts.length - 1]; + var last = null; + // closed poly + if (hxd.Math.abs(p0.x - p1.x) < 1e-9 && hxd.Math.abs(p0.y - p1.y) < 1e-9) + last = pts.pop(); + + if (isConvex(pts)) { + for (i in 1...pts.length - 1) { + content.addIndex(i0); + content.addIndex(i0 + i); + content.addIndex(i0 + i + 1); + } + } else { + var ear = EARCUT; + if (ear == null) + EARCUT = ear = new hxd.earcut.Earcut(); + for (i in ear.triangulate(pts)) + content.addIndex(i + i0); + } + + if (last != null) + pts.push(last); + } + + function flush() { + if (tmpPoints.length == 0) + return; + if (doFill) { + flushFill(pindex); + pindex += tmpPoints.length; + if (content.next()) + pindex = 0; + } + if (lineSize > 0) { + flushLine(pindex); + if (content.next()) + pindex = 0; + } + tmpPoints = []; + } + + /** + Begins a solid color fill. + + Beginning new fill will finish previous fill operation without need to call `Graphics.endFill`. + + @param color An RGB color with which to fill the drawn shapes. + @param alpha A transparency of the fill color. + **/ + public function beginFill(color:Int = 0, alpha = 1.) { + flush(); + tile = h2d.Tile.fromColor(0xFFFFFF); + content.setTile(tile); + setColor(color, alpha); + doFill = true; + } + + /** + Position a virtual tile at the given position and scale. Every draw will display a part of this tile relative + to these coordinates. + + Note that in by default, Tile is not wrapped, and in order to render tiling texture, `Drawable.tileWrap` have to be set. + Additionally, both `Tile.dx` and `Tile.dy` are ignored (use `dx`/`dy` arguments instead) + as well as tile defined size of the tile through `Tile.width` and `Tile.height` (use `scaleX`/`scaleY` relative to texture size). + + Beginning new fill will finish previous fill operation without need to call `Graphics.endFill`. + + @param dx An X offset of the Tile relative to Graphics. + @param dy An Y offset of the Tile relative to Graphics. + @param scaleX A horizontal scale factor applied to the Tile texture. + @param scaleY A vertical scale factor applied to the Tile texture. + @param tile The tile to fill with. If null, uses previously used Tile with `beginTileFill` or throws an error. + Previous tile is remembered across `Graphics.clear` calls. + **/ + public function beginTileFill(?dx:Float, ?dy:Float, ?scaleX:Float, ?scaleY:Float, ?tile:h2d.Tile) { + if (tile == null) + tile = this.tile; + if (tile == null) + throw "Tile not specified"; + flush(); + this.tile = tile; + content.setTile(tile); + setColor(0xFFFFFF); + doFill = true; + + if (dx == null) + dx = 0; + if (dy == null) + dy = 0; + if (scaleX == null) + scaleX = 1; + if (scaleY == null) + scaleY = 1; + dx -= tile.x; + dy -= tile.y; + + var tex = tile.getTexture(); + var pixWidth = 1 / tex.width; + var pixHeight = 1 / tex.height; + ma = pixWidth / scaleX; + mb = 0; + mc = 0; + md = pixHeight / scaleY; + mx = -dx * ma; + my = -dy * md; + } + + /** + Draws a Tile at given position. + See `Graphics.beginTileFill` for limitations. + + This methods ends current fill operation. + @param x The X position of the tile. + @param y The Y position of the tile. + @param tile The tile to draw. + **/ + public function drawTile(x:Float, y:Float, tile:h2d.Tile) { + beginTileFill(x, y, tile); + drawRect(x, y, tile.width, tile.height); + endFill(); + } + + /** + Sets an outline style. Changing the line style ends the currently drawn line. + + @param size Width of the outline. Setting size to 0 will remove the outline. + @param color An outline RGB color. + @param alpha An outline transparency. + **/ + public function lineStyle(size:Float = 0, color = 0, alpha = 1.) { + flush(); + this.lineSize = size; + lineA = alpha; + lineR = ((color >> 16) & 0xFF) / 255.; + lineG = ((color >> 8) & 0xFF) / 255.; + lineB = (color & 0xFF) / 255.; + } + + /** + Ends the current line and starts new one at given position. + **/ + public inline function moveTo(x, y) { + flush(); + lineTo(x, y); + } + + /** + Ends the current fill operation. + **/ + public function endFill() { + flush(); + doFill = false; + } + + /** + Changes current fill color. + Does not interrupt current fill operation and can be utilized to customize color per vertex. + During tile fill operation, color serves as a tile color multiplier. + @param color The new fill color. + @param alpha The new fill transparency. + **/ + public inline function setColor(color:Int, alpha:Float = 1.) { + curA = alpha; + curR = ((color >> 16) & 0xFF) / 255.; + curG = ((color >> 8) & 0xFF) / 255.; + curB = (color & 0xFF) / 255.; + } + + /** + Draws a rectangle with given parameters. + @param x The rectangle top-left corner X position. + @param y The rectangle top-left corner Y position. + @param w The rectangle width. + @param h The rectangle height. + **/ + public function drawRect(x:Float, y:Float, w:Float, h:Float) { + flush(); + lineTo(x, y); + lineTo(x + w, y); + lineTo(x + w, y + h); + lineTo(x, y + h); + lineTo(x, y); + var e = 0.01; // see #776 + tmpPoints[0].x += e; + tmpPoints[0].y += e; + tmpPoints[1].y += e; + tmpPoints[3].x += e; + tmpPoints[4].x += e; + tmpPoints[4].y += e; + flush(); + } + + /** + Draws a rounded rectangle with given parameters. + @param x The rectangle top-left corner X position. + @param y The rectangle top-left corner Y position. + @param w The rectangle width. + @param h The rectangle height. + @param radius Radius of the rectangle corners. + @param nsegments Amount of segments used for corners. When `0` segment count calculated automatically. + **/ + public function drawRoundedRect(x:Float, y:Float, w:Float, h:Float, radius:Float, nsegments = 0) { + if (radius <= 0) { + return drawRect(x, y, w, h); + } + x += radius; + y += radius; + w -= radius * 2; + h -= radius * 2; + flush(); + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(radius * hxd.Math.degToRad(90) / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = hxd.Math.degToRad(90) / (nsegments - 1); + inline function corner(x, y, angleStart) { + for (i in 0...nsegments) { + var a = i * angle + hxd.Math.degToRad(angleStart); + lineTo(x + Math.cos(a) * radius, y + Math.sin(a) * radius); + } + } + lineTo(x, y - radius); + lineTo(x + w, y - radius); + corner(x + w, y, 270); + lineTo(x + w + radius, y + h); + corner(x + w, y + h, 0); + lineTo(x, y + h + radius); + corner(x, y + h, 90); + lineTo(x - radius, y); + corner(x, y, 180); + flush(); + } + + /** + Draws a circle centered at given position. + @param cx X center position of the circle. + @param cy Y center position of the circle. + @param radius Radius of the circle. + @param nsegments Amount of segments used to draw the circle. When `0`, amount of segments calculated automatically. + **/ + public function drawCircle(cx:Float, cy:Float, radius:Float, nsegments = 0) { + flush(); + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(radius * 3.14 * 2 / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = Math.PI * 2 / nsegments; + for (i in 0...nsegments + 1) { + var a = i * angle; + lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius); + } + flush(); + } + + /** + Draws an ellipse centered at given position. + @param cx X center position of the ellipse. + @param cy Y center position of the ellipse. + @param radiusX Horizontal radius of an ellipse. + @param radiusY Vertical radius of an ellipse. + @param rotationAngle Ellipse rotation in radians. + @param nsegments Amount of segments used to draw an ellipse. When `0`, amount of segments calculated automatically. + **/ + public function drawEllipse(cx:Float, cy:Float, radiusX:Float, radiusY:Float, rotationAngle:Float = 0, nsegments = 0) { + flush(); + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(radiusY * 3.14 * 2 / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = Math.PI * 2 / nsegments; + var x1, y1; + for (i in 0...nsegments + 1) { + var a = i * angle; + x1 = Math.cos(a) * Math.cos(rotationAngle) * radiusX - Math.sin(a) * Math.sin(rotationAngle) * radiusY; + y1 = Math.cos(rotationAngle) * Math.sin(a) * radiusY + Math.cos(a) * Math.sin(rotationAngle) * radiusX; + lineTo(cx + x1, cy + y1); + } + flush(); + } + + /** + Draws a pie centered at given position. + @param cx X center position of the pie. + @param cy Y center position of the pie. + @param radius Radius of the pie. + @param angleStart Starting angle of the pie in radians. + @param angleLength The pie size in clockwise direction with `2*PI` being full circle. + @param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically. + **/ + public function drawPie(cx:Float, cy:Float, radius:Float, angleStart:Float, angleLength:Float, nsegments = 0) { + if (Math.abs(angleLength) >= Math.PI * 2) { + return drawCircle(cx, cy, radius, nsegments); + } + flush(); + lineTo(cx, cy); + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(radius * angleLength / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = angleLength / (nsegments - 1); + for (i in 0...nsegments) { + var a = i * angle + angleStart; + lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius); + } + lineTo(cx, cy); + flush(); + } + + /** + Draws a double-edged pie centered at given position. + @param cx X center position of the pie. + @param cy Y center position of the pie. + @param radius The outer radius of the pie. + @param innerRadius The inner radius of the pie. + @param angleStart Starting angle of the pie in radians. + @param angleLength The pie size in clockwise direction with `2*PI` being full circle. + @param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically. + **/ + public function drawPieInner(cx:Float, cy:Float, radius:Float, innerRadius:Float, angleStart:Float, angleLength:Float, nsegments = 0) { + flush(); + if (Math.abs(angleLength) >= Math.PI * 2 + 1e-3) + angleLength = Math.PI * 2 + 1e-3; + + var cs = Math.cos(angleStart); + var ss = Math.sin(angleStart); + var ce = Math.cos(angleStart + angleLength); + var se = Math.sin(angleStart + angleLength); + + lineTo(cx + cs * innerRadius, cy + ss * innerRadius); + + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(radius * angleLength / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = angleLength / (nsegments - 1); + for (i in 0...nsegments) { + var a = i * angle + angleStart; + lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius); + } + lineTo(cx + ce * innerRadius, cy + se * innerRadius); + for (i in 0...nsegments) { + var a = (nsegments - 1 - i) * angle + angleStart; + lineTo(cx + Math.cos(a) * innerRadius, cy + Math.sin(a) * innerRadius); + } + flush(); + } + + /** + Draws a rectangular pie centered at given position. + @param cx X center position of the pie. + @param cy Y center position of the pie. + @param width Width of the pie. + @param height Height of the pie. + @param angleStart Starting angle of the pie in radians. + @param angleLength The pie size in clockwise direction with `2*PI` being solid rectangle. + @param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically. + **/ + public function drawRectanglePie(cx:Float, cy:Float, width:Float, height:Float, angleStart:Float, angleLength:Float, nsegments = 0) { + if (Math.abs(angleLength) >= Math.PI * 2) { + return drawRect(cx - (width / 2), cy - (height / 2), width, height); + } + flush(); + lineTo(cx, cy); + if (nsegments == 0) + nsegments = Math.ceil(Math.abs(Math.max(width, height) * angleLength / 4)); + if (nsegments < 3) + nsegments = 3; + var angle = angleLength / (nsegments - 1); + var square2 = Math.sqrt(2); + for (i in 0...nsegments) { + var a = i * angle + angleStart; + + var _width = Math.cos(a) * (width / 2 + 1) * square2; + var _height = Math.sin(a) * (height / 2 + 1) * square2; + + _width = Math.abs(_width) >= width / 2 ? (Math.cos(a) < 0 ? width / 2 * -1 : width / 2) : _width; + _height = Math.abs(_height) >= height / 2 ? (Math.sin(a) < 0 ? height / 2 * -1 : height / 2) : _height; + + lineTo(cx + _width, cy + _height); + } + lineTo(cx, cy); + flush(); + } + + /** + * Draws a quadratic Bezier curve using the current line style from the current drawing position to (cx, cy) and using the control point that (bx, by) specifies. + * IvanK Lib port ( http://lib.ivank.net ) + */ + public function curveTo(bx:Float, by:Float, cx:Float, cy:Float) { + var ax = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].x; + var ay = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].y; + var t = 2 / 3; + cubicCurveTo(ax + t * (bx - ax), ay + t * (by - ay), cx + t * (bx - cx), cy + t * (by - cy), cx, cy); + } + + /** + * Draws a cubic Bezier curve from the current drawing position to the specified anchor point. + * IvanK Lib port ( http://lib.ivank.net ) + * @param bx control X for start point + * @param by control Y for start point + * @param cx control X for end point + * @param cy control Y for end point + * @param dx end X + * @param dy end Y + * @param nsegments = 40 + */ + public function cubicCurveTo(bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, nsegments = 40) { + var ax = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].x; + var ay = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].y; + var tobx = bx - ax, toby = by - ay; + var tocx = cx - bx, tocy = cy - by; + var todx = dx - cx, tody = dy - cy; + var step = 1 / nsegments; + + for (i in 1...nsegments) { + var d = i * step; + var px = ax + d * tobx, py = ay + d * toby; + var qx = bx + d * tocx, qy = by + d * tocy; + var rx = cx + d * todx, ry = cy + d * tody; + var toqx = qx - px, toqy = qy - py; + var torx = rx - qx, tory = ry - qy; + + var sx = px + d * toqx, sy = py + d * toqy; + var tx = qx + d * torx, ty = qy + d * tory; + var totx = tx - sx, toty = ty - sy; + lineTo(sx + d * totx, sy + d * toty); + } + lineTo(dx, dy); + } + + /** + Draws a straight line from the current drawing position to the given position. + **/ + public inline function lineTo(x:Float, y:Float) { + addVertex(x, y, curR, curG, curB, curA, x * ma + y * mc + mx, x * mb + y * md + my); + } + + /** + Advanced usage. Adds new vertex to the current polygon with given parameters and current line style. + @param x Vertex X position + @param y Vertex Y position + @param r Red tint value of the vertex when performing fill operation. + @param g Green tint value of the vertex when performing fill operation. + @param b Blue tint value of the vertex when performing fill operation. + @param a Alpha of the vertex when performing fill operation. + @param u Normalized horizontal Texture position from the current Tile fill operation. + @param v Normalized vertical Texture position from the current Tile fill operation. + **/ + public function addVertex(x:Float, y:Float, r:Float, g:Float, b:Float, a:Float, u:Float = 0., v:Float = 0.) { + var half = lineSize / 2.0; + if (x - half < xMin) + xMin = x - half; + if (y - half < yMin) + yMin = y - half; + if (x + half > xMax) + xMax = x + half; + if (y + half > yMax) + yMax = y + half; + if (x < xMinSize) + xMinSize = x; + if (y < yMinSize) + yMinSize = y; + if (x > xMaxSize) + xMaxSize = x; + if (y > yMaxSize) + yMaxSize = y; + if (doFill) + content.add(x, y, u, v, r, g, b, a); + var gp = new GPoint(); + gp.load(x, y, lineR, lineG, lineB, lineA); + tmpPoints.push(gp); + } + + override function draw(ctx:RenderContext) { + if (!ctx.beginDrawBatchState(this)) + return; + content.doRender(ctx); + } + + override function sync(ctx:RenderContext) { + super.sync(ctx); + flush(); + content.flush(); + } +} diff --git a/src/gui/GuiMLTextListCtrl.hx b/src/gui/GuiMLTextListCtrl.hx new file mode 100644 index 00000000..01d63b9b --- /dev/null +++ b/src/gui/GuiMLTextListCtrl.hx @@ -0,0 +1,305 @@ +package gui; + +import h2d.filter.Filter; +import h2d.HtmlText; +import h2d.Flow; +import h3d.Engine; +import h2d.Tile; +import h2d.Bitmap; +import h3d.mat.Texture; +import shaders.GuiClipFilter; +import h2d.Graphics; +import gui.GuiControl.MouseState; +import h2d.Scene; +import h2d.Text; +import h2d.Font; +import src.MarbleGame; +import src.Settings; + +class GuiMLTextListCtrl extends GuiControl { + public var texts:Array; + public var onSelectedFunc:Int->Void; + + var font:Font; + var textObjs:Array; + var g:Graphics; + var _prevSelected:Int = -1; + + public var selectedColor:Int = 0x206464; + public var selectedFillColor:Int = 0xC8C8C8; + + public var textYOffset:Int = 0; + + public var scroll:Float = 0; + + public var scrollable:Bool = false; + + var filter:Filter = null; + + var flow:Flow; + var _imageLoader:String->Tile; + + public function new(font:Font, texts:Array, imageLoader:String->Tile, ?filter:Filter = null) { + super(); + this.font = font; + this.texts = texts; + this._manualScroll = true; + this.textObjs = []; + this.filter = filter; + this._imageLoader = imageLoader; + for (text in texts) { + var tobj = new HtmlText(font); + tobj.lineHeightMode = TextOnly; + tobj.loadImage = imageLoader; + tobj.text = text; + tobj.textColor = 0; + if (filter != null) + tobj.filter = filter; + textObjs.push(tobj); + } + this.g = new Graphics(); + } + + public function setTexts(texts:Array) { + var renderRect = this.getRenderRectangle(); + for (textObj in this.textObjs) { + textObj.remove(); + } + this.textObjs = []; + for (text in texts) { + var tobj = new HtmlText(font); + tobj.loadImage = this._imageLoader; + tobj.lineHeightMode = TextOnly; + tobj.text = text; + tobj.textColor = 0; + if (filter != null) + tobj.filter = filter; + textObjs.push(tobj); + + if (this.scrollable) { + if (this.flow != null) { + if (this.flow.contains(tobj)) + this.flow.removeChild(tobj); + + this.flow.addChild(tobj); + + this.flow.getProperties(tobj).isAbsolute = true; + } + } + } + this.texts = texts; + this._prevSelected = -1; + if (this.onSelectedFunc != null) + this.onSelectedFunc(-1); + + redrawSelectionRect(renderRect); + + for (i in 0...textObjs.length) { + var text = textObjs[i]; + text.setPosition(Math.floor((!scrollable ? renderRect.position.x : 0) + 5), + Math.floor((!scrollable ? renderRect.position.y : 0) + + (i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - this.scroll))); + + if (_prevSelected == i) { + text.textColor = selectedColor; + } + } + } + + public override function render(scene2d:Scene, ?parent:h2d.Flow) { + var renderRect = this.getRenderRectangle(); + var htr = this.getHitTestRect(false); + + if (parent != null) { + if (parent.contains(g)) + parent.removeChild(g); + parent.addChild(g); + + var off = this.getOffsetFromParent(); + parent.getProperties(g).isAbsolute = true; + + g.setPosition(off.x, off.y - this.scroll); + } + + if (scrollable) { + this.flow = new Flow(); + + this.flow.maxWidth = cast htr.extent.x; + this.flow.maxHeight = cast htr.extent.y; + this.flow.multiline = true; + this.flow.layout = Stack; + this.flow.overflow = FlowOverflow.Hidden; + + if (parent != null) { + if (parent.contains(this.flow)) { + parent.removeChild(this.flow); + } + parent.addChild(this.flow); + var off = this.getOffsetFromParent(); + var props = parent.getProperties(this.flow); + props.isAbsolute = true; + + this.flow.setPosition(off.x, off.y); + } + } + + for (i in 0...textObjs.length) { + var text = textObjs[i]; + if (!scrollable) { + if (scene2d.contains(text)) + scene2d.removeChild(text); + scene2d.addChild(text); + } else { + if (this.flow.contains(text)) + this.flow.removeChild(text); + this.flow.addChild(text); + + this.flow.getProperties(text).isAbsolute = true; + } + + text.setPosition(Math.floor((!scrollable ? renderRect.position.x : 0) + 5), + Math.floor((!scrollable ? renderRect.position.y : 0) + + (i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - this.scroll))); + + if (_prevSelected == i) { + text.textColor = selectedColor; + } + } + + redrawSelectionRect(htr); + super.render(scene2d, parent); + } + + public function calculateFullHeight() { + return (this.texts.length * (font.size + 4 * Settings.uiScale)); + } + + public override function dispose() { + super.dispose(); + for (text in textObjs) { + text.remove(); + } + this.g.remove(); + if (this.scrollable) { + this.flow.remove(); + } + } + + public override function onRemove() { + super.onRemove(); + for (text in textObjs) { + if (MarbleGame.canvas.scene2d.contains(text)) { + MarbleGame.canvas.scene2d.removeChild(text); // Refresh "layer" + } + text.remove(); + } + if (MarbleGame.canvas.scene2d.contains(g)) + MarbleGame.canvas.scene2d.removeChild(g); + g.remove(); + } + + public override function onMouseMove(mouseState:MouseState) { + var mousePos = mouseState.position; + var renderRect = this.getRenderRectangle(); + var yStart = renderRect.position.y; + var dy = mousePos.y - yStart; + var hoverIndex = Math.floor(dy / (font.size + 4 * Settings.uiScale)); + if (hoverIndex >= this.texts.length) { + hoverIndex = -1; + } + + // Update the texts + for (i in 0...textObjs.length) { + var selected = i == hoverIndex || i == this._prevSelected; + var text = textObjs[i]; + text.textColor = selected ? selectedColor : 0; + // fill color = 0xC8C8C8 + } + // obviously in renderRect + } + + public override function onMouseLeave(mouseState:MouseState) { + for (i in 0...textObjs.length) { + if (i == this._prevSelected) + continue; + var text = textObjs[i]; + text.textColor = 0; + // fill color = 0xC8C8C8 + } + } + + public override function onMousePress(mouseState:MouseState) { + super.onMousePress(mouseState); + + var mousePos = mouseState.position; + var renderRect = this.getRenderRectangle(); + var yStart = renderRect.position.y; + var dy = mousePos.y - yStart; + var selectedIndex = Math.floor((dy + this.scroll) / (font.size + 4 * Settings.uiScale)); + if (selectedIndex >= this.texts.length) { + selectedIndex = -1; + } + if (_prevSelected != selectedIndex) { + _prevSelected = selectedIndex; + + redrawSelectionRect(renderRect); + } + + if (onSelectedFunc != null) { + onSelectedFunc(selectedIndex); + } + } + + function redrawSelectionRect(renderRect:Rect) { + if (_prevSelected != -1) { + g.clear(); + g.beginFill(selectedFillColor); + + var off = this.getOffsetFromParent(); + // Check if we are between the top and bottom, render normally in that case + var topY = 2 * Settings.uiScale + (_prevSelected * (font.size + 4 * Settings.uiScale)) + g.y; + var bottomY = 2 * Settings.uiScale + (_prevSelected * (font.size + 4 * Settings.uiScale)) + g.y + font.size + 4 * Settings.uiScale; + var topRectY = off.y; + var bottomRectY = off.y + renderRect.extent.y; + + if (topY >= topRectY && bottomY <= bottomRectY) + g.drawRect(0, 5 * Settings.uiScale + + (_prevSelected * (font.size + 4 * Settings.uiScale)) + - 3 * Settings.uiScale, renderRect.extent.x, + font.size + + 4 * Settings.uiScale); + // We need to do math the draw the partially visible top selected + if (topY <= topRectY && bottomY >= topRectY) { + g.drawRect(0, this.scroll, renderRect.extent.x, topY + font.size + 4 * Settings.uiScale - off.y); + } + // Same for the bottom + if (topY <= bottomRectY && bottomY >= bottomRectY) { + g.drawRect(0, this.scroll + + renderRect.extent.y + - font.size + - 4 * Settings.uiScale + + (topY + font.size + 4 * Settings.uiScale - bottomRectY), + renderRect.extent.x, off.y + + renderRect.extent.y + - (topY)); + } + g.endFill(); + } else { + g.clear(); + } + } + + public override function onScroll(scrollX:Float, scrollY:Float) { + super.onScroll(scrollX, scrollY); + var renderRect = this.getRenderRectangle(); + + this.scroll = scrollY; + var hittestrect = this.getHitTestRect(false); + for (i in 0...textObjs.length) { + var text = textObjs[i]; + text.y = Math.floor((i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - scrollY)); + g.y = -scrollY; + } + redrawSelectionRect(hittestrect); + } +} diff --git a/src/gui/GuiTextInput.hx b/src/gui/GuiTextInput.hx index cd08b4a2..77d33f3c 100644 --- a/src/gui/GuiTextInput.hx +++ b/src/gui/GuiTextInput.hx @@ -82,4 +82,9 @@ class GuiTextInput extends GuiControl { } #end } + + public function setCaretColor(col:Int) { + text.cursorTile = h2d.Tile.fromColor(col, Std.int(1 / hxd.Window.getInstance().windowToPixelRatio), text.font.size); + text.cursorTile.dy = 2 / hxd.Window.getInstance().windowToPixelRatio; + } } diff --git a/src/gui/GuiTextListCtrl.hx b/src/gui/GuiTextListCtrl.hx index 255795c9..81cd2039 100644 --- a/src/gui/GuiTextListCtrl.hx +++ b/src/gui/GuiTextListCtrl.hx @@ -25,6 +25,7 @@ class GuiTextListCtrl extends GuiControl { public var selectedColor:Int = 0x206464; public var selectedFillColor:Int = 0xC8C8C8; + public var textColor:Int = 0; public var textYOffset:Int = 0; @@ -34,16 +35,17 @@ class GuiTextListCtrl extends GuiControl { var flow:Flow; - public function new(font:Font, texts:Array) { + public function new(font:Font, texts:Array, textColor:Int = 0) { super(); this.font = font; this.texts = texts; this._manualScroll = true; this.textObjs = []; + this.textColor = textColor; for (text in texts) { var tobj = new Text(font); tobj.text = text; - tobj.textColor = 0; + tobj.textColor = textColor; textObjs.push(tobj); } this.g = new Graphics(); @@ -58,7 +60,7 @@ class GuiTextListCtrl extends GuiControl { for (text in texts) { var tobj = new Text(font); tobj.text = text; - tobj.textColor = 0; + tobj.textColor = textColor; textObjs.push(tobj); if (this.scrollable) { @@ -195,7 +197,7 @@ class GuiTextListCtrl extends GuiControl { for (i in 0...textObjs.length) { var selected = i == hoverIndex || i == this._prevSelected; var text = textObjs[i]; - text.textColor = selected ? selectedColor : 0; + text.textColor = selected ? selectedColor : textColor; // fill color = 0xC8C8C8 } // obviously in renderRect @@ -206,7 +208,7 @@ class GuiTextListCtrl extends GuiControl { if (i == this._prevSelected) continue; var text = textObjs[i]; - text.textColor = 0; + text.textColor = textColor; // fill color = 0xC8C8C8 } } diff --git a/src/gui/JoinServerGui.hx b/src/gui/JoinServerGui.hx index d2554391..f021101c 100644 --- a/src/gui/JoinServerGui.hx +++ b/src/gui/JoinServerGui.hx @@ -1,5 +1,8 @@ package gui; +import net.MasterServerClient; +import net.MasterServerClient.RemoteServerInfo; +import net.Net; import h2d.filter.DropShadow; import hxd.res.BitmapFont; import src.MarbleGame; @@ -28,48 +31,6 @@ class JoinServerGui extends GuiImage { return [normal, hover, pressed]; } - this.horizSizing = Width; - this.vertSizing = Height; - this.position = new Vector(); - this.extent = new Vector(640, 480); - - var window = new GuiImage(ResourceLoader.getResource("data/ui/mp/join/window.png", ResourceLoader.getImage, this.imageResources).toTile()); - window.horizSizing = Center; - window.vertSizing = Center; - window.position = new Vector(-60, 5); - window.extent = new Vector(759, 469); - - var hostBtn = new GuiButton(loadButtonImages("data/ui/mp/join/host")); - hostBtn.position = new Vector(521, 379); - hostBtn.extent = new Vector(93, 45); - hostBtn.pressedAction = (e) -> { - MarbleGame.canvas.setContent(new MPPlayMissionGui()); - } - window.addChild(hostBtn); - - var joinBtn = new GuiButton(loadButtonImages("data/ui/mp/join/join")); - joinBtn.position = new Vector(628, 379); - joinBtn.extent = new Vector(93, 45); - window.addChild(joinBtn); - - var refreshBtn = new GuiButton(loadButtonImages("data/ui/mp/join/refresh/refresh-1")); - refreshBtn.position = new Vector(126, 379); - refreshBtn.extent = new Vector(45, 45); - window.addChild(refreshBtn); - - var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings")); - serverSettingsBtn.position = new Vector(171, 379); - serverSettingsBtn.extent = new Vector(45, 45); - window.addChild(serverSettingsBtn); - - var exitBtn = new GuiButton(loadButtonImages("data/ui/mp/join/leave")); - exitBtn.position = new Vector(32, 379); - exitBtn.extent = new Vector(93, 45); - exitBtn.pressedAction = (e) -> { - MarbleGame.canvas.setContent(new MainMenuGui()); - } - window.addChild(exitBtn); - var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); @:privateAccess markerFelt32b.loader = ResourceLoader.loader; @@ -90,6 +51,113 @@ class JoinServerGui extends GuiImage { } } + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + var window = new GuiImage(ResourceLoader.getResource("data/ui/mp/join/window.png", ResourceLoader.getImage, this.imageResources).toTile()); + window.horizSizing = Center; + window.vertSizing = Center; + window.position = new Vector(-60, 5); + window.extent = new Vector(759, 469); + + var serverListContainer = new GuiControl(); + serverListContainer.position = new Vector(30, 80); + serverListContainer.extent = new Vector(475, 290); + window.addChild(serverListContainer); + + var curSelection = -1; + var serverList = new GuiTextListCtrl(markerFelt18, [], 0xFFFFFF); + serverList.position = new Vector(0, 0); + serverList.extent = new Vector(475, 63); + serverList.onSelectedFunc = (sel) -> { + curSelection = sel; + } + serverListContainer.addChild(serverList); + + var serverDisplays = []; + + var ourServerList:Array = []; + var platformToString = ["unknown", "pc", "mac", "web", "android"]; + + function updateServerListDisplay() { + serverDisplays = ourServerList.map(x -> '${x.name}'); + serverList.setTexts(serverDisplays); + } + + MasterServerClient.connectToMasterServer(() -> { + MasterServerClient.instance.getServerList((servers) -> { + ourServerList = servers; + updateServerListDisplay(); + }); + }); + + var maxPlayers = 8; + var privateSlots = 0; + var privateGame = false; + + var hostBtn = new GuiButton(loadButtonImages("data/ui/mp/join/host")); + hostBtn.position = new Vector(521, 379); + hostBtn.extent = new Vector(93, 45); + hostBtn.pressedAction = (e) -> { + Net.hostServer('${Settings.highscoreName}\'s Server', maxPlayers, privateSlots, privateGame, () -> { + MarbleGame.canvas.setContent(new MPPlayMissionGui(true)); + }); + } + window.addChild(hostBtn); + + var joinBtn = new GuiButton(loadButtonImages("data/ui/mp/join/join")); + joinBtn.position = new Vector(628, 379); + joinBtn.extent = new Vector(93, 45); + joinBtn.pressedAction = (e) -> { + if (curSelection != -1) { + var selectedServerVersion = ourServerList[curSelection].version; + // if (selectedServerVersion != MarbleGame.currentVersion) { + // var pup = new MessageBoxOkDlg("You are using a different version of the game than the server. Please update your game."); + // MarbleGame.canvas.pushDialog(pup); + // return; + // } + + // MarbleGame.canvas.setContent(new MultiplayerLoadingGui("Connecting")); + var failed = true; + haxe.Timer.delay(() -> { + if (failed) { + // if (MarbleGame.canvas.content is MultiplayerLoadingGui) { + // var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content; + // if (loadGui != null) { + // loadGui.setErrorStatus("Failed to connect to server. Please try again."); + Net.disconnect(); + // } + // } + } + }, 15000); + Net.joinServer(ourServerList[curSelection].name, false, () -> { + failed = false; + Net.remoteServerInfo = ourServerList[curSelection]; + }); + } + } + window.addChild(joinBtn); + + var refreshBtn = new GuiButton(loadButtonImages("data/ui/mp/join/refresh/refresh-1")); + refreshBtn.position = new Vector(126, 379); + refreshBtn.extent = new Vector(45, 45); + window.addChild(refreshBtn); + + var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings")); + serverSettingsBtn.position = new Vector(171, 379); + serverSettingsBtn.extent = new Vector(45, 45); + window.addChild(serverSettingsBtn); + + var exitBtn = new GuiButton(loadButtonImages("data/ui/mp/join/leave")); + exitBtn.position = new Vector(32, 379); + exitBtn.extent = new Vector(93, 45); + exitBtn.pressedAction = (e) -> { + MarbleGame.canvas.setContent(new MainMenuGui()); + } + window.addChild(exitBtn); + var titleText = new GuiText(markerFelt32); titleText.position = new Vector(30, 20); titleText.extent = new Vector(647, 30); diff --git a/src/gui/MPPlayMissionGui.hx b/src/gui/MPPlayMissionGui.hx index a1b08326..9d153282 100644 --- a/src/gui/MPPlayMissionGui.hx +++ b/src/gui/MPPlayMissionGui.hx @@ -13,11 +13,19 @@ import h3d.Vector; import src.Util; import src.Settings; import src.Mission; +import src.MissionList; +import net.ClientConnection.NetPlatform; +import net.Net; +import net.NetCommands; class MPPlayMissionGui extends GuiImage { static var currentSelectionStatic:Int = -1; static var currentCategoryStatic:String = "beginner"; + static var setLevelFn:(String, Int) -> Void; + static var playSelectedLevel:(String, Int) -> Void; + static var setLevelStr:String->Void; + var currentSelection:Int = 0; var currentCategory:String = "beginner"; var currentList:Array; @@ -36,7 +44,7 @@ class MPPlayMissionGui extends GuiImage { var previewToken:Int = 0; #end - public function new() { + public function new(isHost:Bool = true) { MissionList.buildMissionList(); function chooseBg() { var rand = Math.random(); @@ -268,7 +276,8 @@ class MPPlayMissionGui extends GuiImage { playBtn.position = new Vector(565, 514); playBtn.extent = new Vector(93, 44); playBtn.pressedAction = (sender) -> { - MarbleGame.instance.playMission(currentList[currentSelection]); + NetCommands.toggleReadiness(Net.isClient ? Net.clientId : 0); + // MarbleGame.instance.playMission(currentList[currentSelection], true); } window.addChild(playBtn); @@ -484,6 +493,15 @@ class MPPlayMissionGui extends GuiImage { #end } + playSelectedLevel = (cat:String, index:Int) -> { + // if (custSelected) { + // NetCommands.playCustomLevel(MPCustoms.missionList[custSelectedIdx].path); + // } else { + var curMission = MissionList.missionList["multiplayer"][cat][index]; // mission[index]; + MarbleGame.instance.playMission(curMission, true); + // } + } + currentList = MissionList.missionList["multiplayer"]["beginner"]; setCategoryFunc(currentCategoryStatic, null, false); @@ -504,4 +522,66 @@ class MPPlayMissionGui extends GuiImage { if (Key.isPressed(Key.RIGHT)) setSelectedFunc(currentSelection + 1); } + + inline function platformToString(platform:NetPlatform) { + return switch (platform) { + case Unknown: return "unknown"; + case Android: return "android"; + case MacOS: return "mac"; + case PC: return "pc"; + case Web: return "web"; + } + } + + public function updateLobbyNames() { + return; + var playerListArr = []; + if (Net.isHost) { + playerListArr.push({ + name: Settings.highscoreName, + state: Net.lobbyHostReady, + platform: Net.getPlatform() + }); + } + if (Net.isClient) { + playerListArr.push({ + name: Settings.highscoreName, + state: Net.lobbyClientReady, + platform: Net.getPlatform() + }); + } + if (Net.clientIdMap != null) { + for (c => v in Net.clientIdMap) { + playerListArr.push({ + name: v.name, + state: v.lobbyReady, + platform: v.platform + }); + } + } + + // if (!showingCustoms) + // playerList.setTexts(playerListArr.map(player -> { + // return '${player.name}'; + // })); + + var pubCount = 1; // Self + var privCount = 0; + for (cid => cc in Net.clientIdMap) { + if (cc.isPrivate) { + privCount++; + } else { + pubCount++; + } + } + + if (Net.isHost) { + // updatePlayerCountFn(pubCount, privCount, Net.serverInfo.maxPlayers - Net.serverInfo.privateSlots, Net.serverInfo.privateSlots); + } + } + + public function updatePlayerCount(pub:Int, priv:Int, publicTotal:Int, privateTotal:Int) { + return; + // updatePlayerCountFn(pub, priv, publicTotal, privateTotal); + } } diff --git a/src/gui/MarbleSelectGui.hx b/src/gui/MarbleSelectGui.hx index d53d7e34..e18a53cf 100644 --- a/src/gui/MarbleSelectGui.hx +++ b/src/gui/MarbleSelectGui.hx @@ -13,6 +13,493 @@ import src.Settings; import src.ResourceLoaderWorker; class MarbleSelectGui extends GuiImage { + public static var marbleData = [ + [ + { + name: "Staff's Original", + dts: "data/shapes/balls/ball-superball.dts", + skin: "base", + shader: "Default" + }, + { + name: "3D Marble", + dts: "data/shapes/balls/3dMarble.dts", + skin: "base", + shader: "Default" + }, + { + name: "Mid P", + dts: "data/shapes/balls/midp.dts", + skin: "base", + shader: "Default" + }, + { + name: "Spade", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin4", + shader: "Default" + }, + { + name: "GMD Logo", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin5", + shader: "Default" + }, + { + name: "Textured Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin6", + shader: "Default" + }, + { + name: "Golden Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin7", + shader: "Default" + }, + { + name: "Rainbow Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin8", + shader: "Default" + }, + { + name: "Brown Swirls", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin9", + shader: "Default" + }, + { + name: "Caution Stripes", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin10", + shader: "Default" + }, + { + name: "Earth", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin11", + shader: "Default" + }, + { + name: "Golf Ball", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin12", + shader: "Default" + }, + { + name: "Jupiter", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin13", + shader: "Default" + }, + { + name: "MB Gold Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin14", + shader: "Default" + }, + { + name: "MBP on the Marble!", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin15", + shader: "Default" + }, + { + name: "Moshe", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin16", + shader: "Default" + }, + { + name: "Strong Bad", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin17", + shader: "Default" + }, + { + name: "Venus", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin18", + shader: "Default" + }, + { + name: "Water", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin19", + shader: "Default" + }, + { + name: "Evil Eye", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin20", + shader: "Default" + }, + { + name: "Desert and Sky", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin21", + shader: "Default" + }, + { + name: "Dirt Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin22", + shader: "Default" + }, + { + name: "Friction Textured Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin23", + shader: "Default" + }, + { + name: "Grass", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin24", + shader: "Default" + }, + { + name: "Mars", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin25", + shader: "Default" + }, + { + name: "Phil's Golf Ball", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin26", + shader: "Default" + }, + { + name: "Molten", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin27", + shader: "Default" + }, + { + name: "Lightning", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin28", + shader: "Default" + }, + { + name: "Phil'sEmpire", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin29", + shader: "Default" + }, + { + name: "Matan's Red Dragon", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin30", + shader: "Default" + }, + { + name: "Metallic Marble", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin31", + shader: "Default" + }, + { + name: "Sun", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin32", + shader: "Default" + }, + { + name: "Underwater", + dts: "data/shapes/balls/ball-superball.dts", + skin: "skin33", + shader: "Default" + }, + { + name: "GarageGames logo", + dts: "data/shapes/balls/garageGames.dts", + skin: "base", + shader: "Default" + }, + { + name: "Big Marble 1", + dts: "data/shapes/balls/bm1.dts", + skin: "base", + shader: "Default" + }, + { + name: "Big Marble 2", + dts: "data/shapes/balls/bm2.dts", + skin: "base", + shader: "Default" + }, + { + name: "Big Marble 3", + dts: "data/shapes/balls/bm3.dts", + skin: "base", + shader: "Default" + }, + { + name: "Small Marble 1", + dts: "data/shapes/balls/sm1.dts", + skin: "base", + shader: "Default" + }, + { + name: "Small Marble 2", + dts: "data/shapes/balls/sm2.dts", + skin: "base", + shader: "Default" + }, + { + name: "Small Marble 3", + dts: "data/shapes/balls/sm3.dts", + skin: "base", + shader: "Default" + } + ], + [ + { + name: "Deep Blue", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin1", + shader: "ClassicGlassPureSphere" + }, + { + name: "Blood Red", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin2", + shader: "Default" + }, + { + name: "Gang Green", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin6", + shader: "ClassicGlassPureSphere" + }, + { + name: "Pink Candy", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin27", + shader: "Default" + }, + { + name: "Chocolate", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin5", + shader: "ClassicGlassPureSphere" + }, + { + name: "Grape", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin4", + shader: "ClassicGlassPureSphere" + }, + { + name: "Lemon", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin28", + shader: "Default" + }, + { + name: "Lime Green", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin8", + shader: "Default" + }, + { + name: "Blueberry", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin7", + shader: "ClassicGlassPureSphere" + }, + { + name: "Tangerine", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin3", + shader: "ClassicGlassPureSphere" + }, + { + name: "8 Ball", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin9", + shader: "ClassicMarb3" + }, + { + name: "Ace of Hearts", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin22", + shader: "ClassicMarb3" + }, + { + name: "Football", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin16", + shader: "ClassicMarb3" + }, + { + name: "9 Ball", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin29", + shader: "ClassicMarb3" + }, + { + name: "Ace of Spades", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin24", + shader: "ClassicMarb3" + }, + { + name: "GarageGames", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin10", + shader: "ClassicMarb2" + }, + { + name: "Bob", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin30", + shader: "ClassicMarb3" + }, + { + name: "Skully", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin18", + shader: "Default" + }, + { + name: "Jack-o-Lantern", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin34", + shader: "Default" + }, + { + name: "Walled Up", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin25", + shader: "ClassicMarb3" + }, + { + name: "Sunny Side Up", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin11", + shader: "ClassicMetal" + }, + { + name: "Lunar", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin31", + shader: "ClassicMetal" + }, + { + name: "Battery", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin14", + shader: "ClassicMarb3" + }, + { + name: "Static", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin32", + shader: "ClassicMarb2" + }, + { + name: "Earth", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin20", + shader: "ClassicMarbGlass20" + }, + { + name: "Red and X", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin13", + shader: "ClassicMarb3" + }, + { + name: "Orange Spiral", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin12", + shader: "ClassicGlassPureSphere" + }, + { + name: "Blue Spiral", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin15", + shader: "ClassicGlassPureSphere" + }, + { + name: "Sliced Marble", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin21", + shader: "ClassicMarb3" + }, + { + name: "Orange Checkers", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin19", + shader: "ClassicMarb3" + }, + { + name: "Torque", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin33", + shader: "ClassicMarb3" + }, + { + name: "Fred", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin17", + shader: "ClassicMarb3" + }, + { + name: "Pirate", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin26", + shader: "ClassicMarbGlass18" + }, + { + name: "Shuriken", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin23", + shader: "ClassicMarb3" + }, + { + name: "Eyeball", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin35", + shader: "Default" + }, + { + name: "Woody", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin36", + shader: "Default" + }, + { + name: "Dat Nostalgia", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin37", + shader: "Default" + }, + { + name: "Graffiti", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin38", + shader: "Default" + }, + { + name: "Asteroid", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin39", + shader: "Default" + }, + { + name: "Disco Ball", + dts: "data/shapes/balls/pack1/pack1marble.dts", + skin: "uskin40", + shader: "Default" + } + ], + ]; + public function new() { var img = ResourceLoader.getImage("data/ui/marbleSelect/marbleSelect.png"); super(img.resource.toTile()); @@ -21,494 +508,6 @@ class MarbleSelectGui extends GuiImage { this.position = new Vector(73, -59); this.extent = new Vector(493, 361); - var marbleData = [ - [ - { - name: "Staff's Original", - dts: "data/shapes/balls/ball-superball.dts", - skin: "base", - shader: "Default" - }, - { - name: "3D Marble", - dts: "data/shapes/balls/3dMarble.dts", - skin: "base", - shader: "Default" - }, - { - name: "Mid P", - dts: "data/shapes/balls/midp.dts", - skin: "base", - shader: "Default" - }, - { - name: "Spade", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin4", - shader: "Default" - }, - { - name: "GMD Logo", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin5", - shader: "Default" - }, - { - name: "Textured Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin6", - shader: "Default" - }, - { - name: "Golden Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin7", - shader: "Default" - }, - { - name: "Rainbow Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin8", - shader: "Default" - }, - { - name: "Brown Swirls", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin9", - shader: "Default" - }, - { - name: "Caution Stripes", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin10", - shader: "Default" - }, - { - name: "Earth", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin11", - shader: "Default" - }, - { - name: "Golf Ball", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin12", - shader: "Default" - }, - { - name: "Jupiter", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin13", - shader: "Default" - }, - { - name: "MB Gold Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin14", - shader: "Default" - }, - { - name: "MBP on the Marble!", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin15", - shader: "Default" - }, - { - name: "Moshe", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin16", - shader: "Default" - }, - { - name: "Strong Bad", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin17", - shader: "Default" - }, - { - name: "Venus", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin18", - shader: "Default" - }, - { - name: "Water", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin19", - shader: "Default" - }, - { - name: "Evil Eye", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin20", - shader: "Default" - }, - { - name: "Desert and Sky", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin21", - shader: "Default" - }, - { - name: "Dirt Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin22", - shader: "Default" - }, - { - name: "Friction Textured Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin23", - shader: "Default" - }, - { - name: "Grass", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin24", - shader: "Default" - }, - { - name: "Mars", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin25", - shader: "Default" - }, - { - name: "Phil's Golf Ball", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin26", - shader: "Default" - }, - { - name: "Molten", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin27", - shader: "Default" - }, - { - name: "Lightning", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin28", - shader: "Default" - }, - { - name: "Phil'sEmpire", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin29", - shader: "Default" - }, - { - name: "Matan's Red Dragon", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin30", - shader: "Default" - }, - { - name: "Metallic Marble", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin31", - shader: "Default" - }, - { - name: "Sun", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin32", - shader: "Default" - }, - { - name: "Underwater", - dts: "data/shapes/balls/ball-superball.dts", - skin: "skin33", - shader: "Default" - }, - { - name: "GarageGames logo", - dts: "data/shapes/balls/garageGames.dts", - skin: "base", - shader: "Default" - }, - { - name: "Big Marble 1", - dts: "data/shapes/balls/bm1.dts", - skin: "base", - shader: "Default" - }, - { - name: "Big Marble 2", - dts: "data/shapes/balls/bm2.dts", - skin: "base", - shader: "Default" - }, - { - name: "Big Marble 3", - dts: "data/shapes/balls/bm3.dts", - skin: "base", - shader: "Default" - }, - { - name: "Small Marble 1", - dts: "data/shapes/balls/sm1.dts", - skin: "base", - shader: "Default" - }, - { - name: "Small Marble 2", - dts: "data/shapes/balls/sm2.dts", - skin: "base", - shader: "Default" - }, - { - name: "Small Marble 3", - dts: "data/shapes/balls/sm3.dts", - skin: "base", - shader: "Default" - } - ], - [ - { - name: "Deep Blue", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin1", - shader: "ClassicGlassPureSphere" - }, - { - name: "Blood Red", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin2", - shader: "Default" - }, - { - name: "Gang Green", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin6", - shader: "ClassicGlassPureSphere" - }, - { - name: "Pink Candy", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin27", - shader: "Default" - }, - { - name: "Chocolate", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin5", - shader: "ClassicGlassPureSphere" - }, - { - name: "Grape", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin4", - shader: "ClassicGlassPureSphere" - }, - { - name: "Lemon", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin28", - shader: "Default" - }, - { - name: "Lime Green", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin8", - shader: "Default" - }, - { - name: "Blueberry", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin7", - shader: "ClassicGlassPureSphere" - }, - { - name: "Tangerine", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin3", - shader: "ClassicGlassPureSphere" - }, - { - name: "8 Ball", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin9", - shader: "ClassicMarb3" - }, - { - name: "Ace of Hearts", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin22", - shader: "ClassicMarb3" - }, - { - name: "Football", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin16", - shader: "ClassicMarb3" - }, - { - name: "9 Ball", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin29", - shader: "ClassicMarb3" - }, - { - name: "Ace of Spades", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin24", - shader: "ClassicMarb3" - }, - { - name: "GarageGames", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin10", - shader: "ClassicMarb2" - }, - { - name: "Bob", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin30", - shader: "ClassicMarb3" - }, - { - name: "Skully", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin18", - shader: "Default" - }, - { - name: "Jack-o-Lantern", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin34", - shader: "Default" - }, - { - name: "Walled Up", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin25", - shader: "ClassicMarb3" - }, - { - name: "Sunny Side Up", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin11", - shader: "ClassicMetal" - }, - { - name: "Lunar", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin31", - shader: "ClassicMetal" - }, - { - name: "Battery", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin14", - shader: "ClassicMarb3" - }, - { - name: "Static", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin32", - shader: "ClassicMarb2" - }, - { - name: "Earth", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin20", - shader: "ClassicMarbGlass20" - }, - { - name: "Red and X", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin13", - shader: "ClassicMarb3" - }, - { - name: "Orange Spiral", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin12", - shader: "ClassicGlassPureSphere" - }, - { - name: "Blue Spiral", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin15", - shader: "ClassicGlassPureSphere" - }, - { - name: "Sliced Marble", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin21", - shader: "ClassicMarb3" - }, - { - name: "Orange Checkers", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin19", - shader: "ClassicMarb3" - }, - { - name: "Torque", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin33", - shader: "ClassicMarb3" - }, - { - name: "Fred", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin17", - shader: "ClassicMarb3" - }, - { - name: "Pirate", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin26", - shader: "ClassicMarbGlass18" - }, - { - name: "Shuriken", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin23", - shader: "ClassicMarb3" - }, - { - name: "Eyeball", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin35", - shader: "Default" - }, - { - name: "Woody", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin36", - shader: "Default" - }, - { - name: "Dat Nostalgia", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin37", - shader: "Default" - }, - { - name: "Graffiti", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin38", - shader: "Default" - }, - { - name: "Asteroid", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin39", - shader: "Default" - }, - { - name: "Disco Ball", - dts: "data/shapes/balls/pack1/pack1marble.dts", - skin: "uskin40", - shader: "Default" - } - - ], - ]; - var categoryNames = ["Official Marbles", "MBUltra"]; var curSelection:Int = Settings.optionsSettings.marbleIndex; diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index 693b3f18..211dc909 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -24,6 +24,7 @@ import src.ResourceLoader; import h3d.Vector; import src.Util; import src.MarbleGame; +import src.MissionList; class PlayMissionGui extends GuiImage { static var currentSelectionStatic:Int = -1; diff --git a/src/gui/ReplayCenterGui.hx b/src/gui/ReplayCenterGui.hx index 90f62447..dc8139af 100644 --- a/src/gui/ReplayCenterGui.hx +++ b/src/gui/ReplayCenterGui.hx @@ -10,6 +10,7 @@ import h3d.Vector; import src.Util; import src.MarbleGame; import src.Settings; +import src.MissionList; class ReplayCenterGui extends GuiImage { public function new() { diff --git a/src/gui/SearchGui.hx b/src/gui/SearchGui.hx index a608c35a..66c7c893 100644 --- a/src/gui/SearchGui.hx +++ b/src/gui/SearchGui.hx @@ -8,6 +8,7 @@ import hxd.res.BitmapFont; import h3d.Vector; import src.ResourceLoader; import src.Settings; +import src.MissionList; class SearchGui extends GuiImage { public function new(game:String, isCustom:Bool) { diff --git a/src/gui/StatisticsGui.hx b/src/gui/StatisticsGui.hx index 556814b2..6f2d709f 100644 --- a/src/gui/StatisticsGui.hx +++ b/src/gui/StatisticsGui.hx @@ -8,6 +8,7 @@ import src.Settings; import src.Settings.PlayStatistics; import src.Mission; import src.Util; +import src.MissionList; class StatisticsGui extends GuiImage { public function new(game:String) { diff --git a/src/modes/HuntMode.hx b/src/modes/HuntMode.hx index cedb8119..e76bae06 100644 --- a/src/modes/HuntMode.hx +++ b/src/modes/HuntMode.hx @@ -255,6 +255,28 @@ class HuntMode extends NullMode { } } + public inline function setGemHiddenStatus(gemId:Int, status:Bool) { + var gemSpawn = gemSpawnPoints[gemId]; + if (gemSpawn.gem != null) { + gemSpawn.gem.pickedUp = status; + gemSpawn.gem.setHide(status); + gemSpawn.gemBeam.setHide(status); + if (status) + this.activeGems.push(gemSpawn.gem); + else + this.activeGems.remove(gemSpawn.gem); + } else { + throw new haxe.Exception("Setting gem status for non existent gem!"); + } + } + + public function setActiveSpawnSphere(gems:Array) { + hideExisting(); + for (gem in gems) { + spawnGem(gem); + } + } + function getGemWeight(gem:Gem) { if (gem.gemColor == "red") return 0; diff --git a/src/modes/NullMode.hx b/src/modes/NullMode.hx index ba0f7bdf..e117e8af 100644 --- a/src/modes/NullMode.hx +++ b/src/modes/NullMode.hx @@ -28,6 +28,7 @@ class NullMode implements GameMode { // If there's a start pad, start there position = startPad.getAbsPos().getPosition(); quat = startPad.getRotationQuat().clone(); + position.z += 3; } else { position = new Vector(0, 0, 300); } diff --git a/src/net/BitStream.hx b/src/net/BitStream.hx new file mode 100644 index 00000000..7210db53 --- /dev/null +++ b/src/net/BitStream.hx @@ -0,0 +1,154 @@ +package net; + +import haxe.io.FPHelper; +import haxe.io.BytesOutput; +import haxe.io.BytesInput; +import haxe.io.Bytes; + +class InputBitStream { + var data:Bytes; + var position:Int; + var shift:Int; + + public function new(data:Bytes) { + this.data = data; + this.position = 0; + this.shift = 0; + } + + function readBits(bits:Int = 8) { + if (this.shift + bits >= 8) { + var extra = (this.shift + bits) % 8; + var remain = bits - extra; + var first = data.get(position) >> shift; + var result = first; + this.position++; + if (extra > 0) { + var second = (data.get(position) & (0xFF >> (8 - extra))) << remain; + result |= second; + } + this.shift = extra; + return result; + } else { + var result = (data.get(position) >> shift) & (0xFF >> (8 - bits)); + shift += bits; + + return result; + } + } + + public function readInt(bits:Int = 32) { + var value = 0; + var shift = 0; + while (bits > 0) { + value |= readBits(bits < 8 ? bits : 8) << shift; + shift += 8; + bits -= 8; + } + return value; + } + + public function readFlag() { + return readInt(1) != 0; + } + + public function readByte() { + return readInt(8); + } + + public function readUInt16() { + return readInt(16); + } + + public function readInt32() { + return readInt(32); + } + + public function readFloat() { + return FPHelper.i32ToFloat(readInt32()); + } + + public function readString() { + var length = readUInt16(); + var str = ""; + for (i in 0...length) { + str += String.fromCharCode(readByte()); + } + return str; + } +} + +class OutputBitStream { + var data:BytesOutput; + var position:Int; + var shift:Int; + var lastByte:Int; + + public function new(data:BytesOutput = null) { + this.data = data; + if (this.data == null) + this.data = new BytesOutput(); + this.position = 0; + this.shift = 0; + this.lastByte = 0; + } + + function writeBits(value:Int, bits:Int) { + value = value & (0xFF >> (8 - bits)); + if (this.shift + bits >= 8) { + var extra = (shift + bits) % 8; + var remain = bits - extra; + + var first = value & (0xFF >> (8 - remain)); + lastByte |= first << shift; + + var second = (value >> remain) & (0xFF >> (8 - extra)); + this.data.writeByte(this.lastByte); + this.lastByte = second; + this.shift = extra; + } else { + lastByte |= (value << this.shift) & (0xFF >> (8 - bits - this.shift)); + this.shift += bits; + } + } + + public function writeInt(value:Int, bits:Int = 32) { + while (bits > 0) { + this.writeBits(value & 0xFF, bits < 8 ? bits : 8); + value >>= 8; + bits -= 8; + } + } + + public function writeFlag(value:Bool) { + writeInt(value ? 1 : 0, 1); + } + + public function writeByte(value:Int) { + writeInt(value, 8); + } + + public function writeUInt16(value:Int) { + writeInt(value, 16); + } + + public function writeInt32(value:Int) { + writeInt(value, 32); + } + + public function getBytes() { + this.data.writeByte(this.lastByte); + return this.data.getBytes(); + } + + public function writeFloat(value:Float) { + writeInt(FPHelper.floatToI32(value), 32); + } + + public function writeString(value:String) { + writeUInt16(value.length); + for (i in 0...value.length) { + writeByte(StringTools.fastCodeAt(value, i)); + } + } +} diff --git a/src/net/ClientConnection.hx b/src/net/ClientConnection.hx new file mode 100644 index 00000000..2b9c763c --- /dev/null +++ b/src/net/ClientConnection.hx @@ -0,0 +1,138 @@ +package net; + +import haxe.io.Bytes; +import datachannel.RTCPeerConnection; +import datachannel.RTCDataChannel; +import net.MoveManager; +import src.TimeState; + +enum abstract GameplayState(Int) from Int to Int { + var UNKNOWN; + var LOBBY; + var GAME; +} + +enum abstract NetPlatform(Int) from Int to Int { + var Unknown; + var PC; + var MacOS; + var Web; + var Android; +} + +@:publicFields +class ClientConnection extends GameConnection { + var socket:RTCPeerConnection; + var datachannel:RTCDataChannel; + var datachannelUnreliable:RTCDataChannel; + var rtt:Float; + var pingSendTime:Float; + var _rttRecords:Array = []; + var lastRecvTime:Float; + var didWarnTimeout:Bool = false; + + public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel, datachannelUnreliable:RTCDataChannel) { + super(id); + this.socket = socket; + this.datachannel = datachannel; + this.datachannelUnreliable = datachannelUnreliable; + this.state = GameplayState.LOBBY; + this.rtt = 0; + this.name = "Unknown"; + } + + override function sendBytes(b:Bytes) { + datachannel.sendBytes(b); + } + + override function sendBytesUnreliable(b:Bytes) { + datachannelUnreliable.sendBytes(b); + } + + public inline function needsTimeoutWarn(t:Float) { + return (t - lastRecvTime) > 10 && !didWarnTimeout; + } + + public inline function needsTimeoutKick(t:Float) { + return (t - lastRecvTime) > 15 && didWarnTimeout; + } +} + +@:publicFields +class DummyConnection extends GameConnection { + public function new(id:Int) { + super(id); + this.state = GameplayState.GAME; + this.lobbyReady = true; + } +} + +@:publicFields +abstract class GameConnection { + var id:Int; + var state:GameplayState; + var moveManager:MoveManager; + var name:String; + var lobbyReady:Bool; + var platform:NetPlatform; + var marbleId:Int; + var isPrivate:Bool; + + function new(id:Int) { + this.id = id; + this.moveManager = new MoveManager(this); + this.lobbyReady = false; + } + + public function ready() { + state = GameplayState.GAME; + } + + public function toggleLobbyReady() { + lobbyReady = !lobbyReady; + } + + public function queueMove(m:NetMove) { + moveManager.queueMove(m); + } + + public inline function acknowledgeMove(m:NetMove, timeState:TimeState) { + return moveManager.acknowledgeMove(m, timeState); + } + + public inline function getQueuedMoves() { + return @:privateAccess moveManager.queuedMoves; + } + + public inline function getQueuedMovesLength() { + return moveManager.getQueueSize(); + } + + public function recordMove(marble:src.Marble, motionDir:h3d.Vector, timeState:TimeState, serverTicks:Int) { + return moveManager.recordMove(marble, motionDir, timeState, serverTicks); + } + + public function getNextMove() { + return moveManager.getNextMove(); + } + + public function sendBytes(b:haxe.io.Bytes) {} + + public function sendBytesUnreliable(b:haxe.io.Bytes) {} + + public inline function getName() { + return name; + } + + public inline function setName(value:String) { + name = value; + } + + public inline function setMarbleId(value:Int) { + marbleId = value; + } + + public inline function getMarbleId() { + return marbleId; + } +} diff --git a/src/net/GemPredictionStore.hx b/src/net/GemPredictionStore.hx new file mode 100644 index 00000000..476796ba --- /dev/null +++ b/src/net/GemPredictionStore.hx @@ -0,0 +1,29 @@ +package net; + +import net.NetPacket.GemSpawnPacket; +import net.NetPacket.GemPickupPacket; + +class GemPredictionStore { + var predictions:Array; + + public inline function new() { + predictions = []; + } + + public inline function alloc() { + predictions.push(true); + } + + public inline function getState(netIndex:Int) { + return predictions[netIndex]; + } + + public inline function acknowledgeGemPickup(packet:GemPickupPacket) { + predictions[packet.gemId] = true; + } + + public inline function acknowledgeGemSpawn(packet:GemSpawnPacket) { + for (gemId in packet.gemIds) + predictions[gemId] = false; + } +} diff --git a/src/net/MarblePredictionStore.hx b/src/net/MarblePredictionStore.hx new file mode 100644 index 00000000..7f7da41a --- /dev/null +++ b/src/net/MarblePredictionStore.hx @@ -0,0 +1,86 @@ +package net; + +import net.NetPacket.MarbleUpdatePacket; +import net.NetPacket.MarbleMovePacket; +import src.TimeState; +import src.Marble; +import h3d.Vector; + +@:publicFields +class MarblePrediction { + var tick:Int; + var position:Vector; + var velocity:Vector; + var omega:Vector; + var isControl:Bool; + var blastAmount:Int; + + public function new(marble:Marble, tick:Int) { + this.tick = tick; + position = @:privateAccess marble.newPos.clone(); + velocity = @:privateAccess marble.velocity.clone(); + omega = @:privateAccess marble.omega.clone(); + blastAmount = @:privateAccess marble.blastTicks; + isControl = @:privateAccess marble.controllable; + } + + public inline function getError(p:MarbleUpdatePacket) { + // Just doing position errors is enough to make it work + var subs = position.sub(p.position).lengthSq(); // + velocity.sub(p.velocity).lengthSq() + omega.sub(p.omega).lengthSq(); + if (p.netFlags != 0) + subs += 1; + // if (p.powerUpId != powerupItemId) + // if (tick % 10 == 0) + // subs += 1; // temp + // if (isControl) + // subs += Math.abs(blastAmount - p.blastAmount); + return subs; + } +} + +class MarblePredictionStore { + var predictions:Map>; + + public function new() { + predictions = []; + } + + public function storeState(marble:Marble, tick:Int) { + var state = new MarblePrediction(marble, tick); + if (predictions.exists(marble)) { + var arr = predictions[marble]; + while (arr.length != 0 && arr[0].tick >= tick) + arr.shift(); + arr.push(state); + } else { + predictions.set(marble, [state]); + } + } + + public function retrieveState(marble:Marble, tick:Int) { + if (predictions.exists(marble)) { + var arr = predictions[marble]; + while (arr.length != 0 && arr[0].tick < tick) + arr.shift(); + if (arr.length == 0) + return null; + var p = arr[0]; + if (p.tick == tick) + return p; + return null; + } + return null; + } + + public function clearStatesAfterTick(marble:Marble, tick:Int) { + if (predictions.exists(marble)) { + var arr = predictions[marble]; + while (arr.length != 0 && arr[arr.length - 1].tick >= tick) + arr.pop(); + } + } + + public function removeMarbleFromPrediction(marble:Marble) { + this.predictions.remove(marble); + } +} diff --git a/src/net/MarbleUpdateQueue.hx b/src/net/MarbleUpdateQueue.hx new file mode 100644 index 00000000..44f8e35a --- /dev/null +++ b/src/net/MarbleUpdateQueue.hx @@ -0,0 +1,94 @@ +package net; + +import h3d.Vector; +import net.NetPacket.MarbleNetFlags; +import net.NetPacket.MarbleUpdatePacket; +import net.Net; + +@:publicFields +class OtherMarbleUpdate { + var packets:Array = []; + var lastBlastTick:Int; + var lastHeliTick:Int; + var lastMegaTick:Int; + var lastPowerUpId:Int; + var lastGravityUp:Vector; + + public function new() {} +} + +@:publicFields +class MarbleUpdateQueue { + var otherMarbleUpdates:Map = []; + var myMarbleUpdate:MarbleUpdatePacket; + var ourMoveApplied:Bool = false; + + public function new() {} + + public function enqueue(update:MarbleUpdatePacket) { + var cc = update.clientId; + if (cc != Net.clientId) { + // if (myMarbleUpdate != null && update.serverTicks > myMarbleUpdate.serverTicks) + // ourMoveApplied = true; + if (otherMarbleUpdates.exists(cc)) { + var otherUpdate = otherMarbleUpdates[cc]; + var ourList = otherUpdate.packets; + // Copy the netflagg'd fields + if (update.netFlags & MarbleNetFlags.DoBlast == 0) + update.blastTick = otherUpdate.lastBlastTick; + else + otherUpdate.lastBlastTick = update.blastTick; + if (update.netFlags & MarbleNetFlags.DoHelicopter == 0) + update.heliTick = otherUpdate.lastHeliTick; + else + otherUpdate.lastHeliTick = update.heliTick; + if (update.netFlags & MarbleNetFlags.DoMega == 0) + update.megaTick = otherUpdate.lastMegaTick; + else + otherUpdate.lastMegaTick = update.megaTick; + if (update.netFlags & MarbleNetFlags.PickupPowerup == 0) + update.powerUpId = otherUpdate.lastPowerUpId; + else + otherUpdate.lastPowerUpId = update.powerUpId; + if (update.netFlags & MarbleNetFlags.GravityChange == 0) + update.gravityDirection = otherUpdate.lastGravityUp; + else + otherUpdate.lastGravityUp = update.gravityDirection; + ourList.push(update); + } else { + var otherUpdate = new OtherMarbleUpdate(); + otherUpdate.packets.push(update); + // Copy the netflagg'd fields + if (update.netFlags & MarbleNetFlags.DoBlast != 0) + otherUpdate.lastBlastTick = update.blastTick; + if (update.netFlags & MarbleNetFlags.DoHelicopter != 0) + otherUpdate.lastHeliTick = update.heliTick; + if (update.netFlags & MarbleNetFlags.DoMega != 0) + otherUpdate.lastMegaTick = update.megaTick; + if (update.netFlags & MarbleNetFlags.PickupPowerup != 0) + otherUpdate.lastPowerUpId = update.powerUpId; + if (update.netFlags & MarbleNetFlags.GravityChange != 0) + otherUpdate.lastGravityUp = update.gravityDirection; + otherMarbleUpdates[cc] = otherUpdate; + } + } else { + if (myMarbleUpdate == null || update.serverTicks > myMarbleUpdate.serverTicks) { + if (myMarbleUpdate != null) { + // Copy the netflagg'd fields + if (update.netFlags & MarbleNetFlags.DoBlast == 0) + update.blastTick = myMarbleUpdate.blastTick; + if (update.netFlags & MarbleNetFlags.DoHelicopter == 0) + update.heliTick = myMarbleUpdate.heliTick; + if (update.netFlags & MarbleNetFlags.DoMega == 0) + update.megaTick = myMarbleUpdate.megaTick; + if (update.netFlags & MarbleNetFlags.PickupPowerup == 0) + update.powerUpId = myMarbleUpdate.powerUpId; + if (update.netFlags & MarbleNetFlags.GravityChange == 0) + update.gravityDirection = myMarbleUpdate.gravityDirection; + } + myMarbleUpdate = update; + ourMoveApplied = false; + } + } + } +} diff --git a/src/net/MasterServerClient.hx b/src/net/MasterServerClient.hx new file mode 100644 index 00000000..045b5f3b --- /dev/null +++ b/src/net/MasterServerClient.hx @@ -0,0 +1,323 @@ +package net; + +import gui.MessageBoxOkDlg; +import src.MarbleGame; +import haxe.Json; +import net.Net.ServerInfo; +import haxe.net.WebSocket; +import src.Console; + +typedef RemoteServerInfo = { + name:String, + players:Int, + maxPlayers:Int, + platform:Int, + version:String +} + +class MasterServerClient { + #if js + static var serverIp = "wss://mbomaster.randomityguy.me:8443"; + #else + static var serverIp = "ws://89.58.58.191:8080"; + #end + public static var instance:MasterServerClient; + + var ws:WebSocket; + var serverListCb:Array->Void; + + var open = false; + + static var wsToken:Int = 0; + + #if hl + var wsThread:sys.thread.Thread; + + static var responses:sys.thread.Deque<() -> Void> = new sys.thread.Deque<() -> Void>(); + + var toSend:sys.thread.Deque = new sys.thread.Deque(); + var stopping:Bool = false; + var stopMutex:sys.thread.Mutex = new sys.thread.Mutex(); + #end + + public function new(onOpenFunc:() -> Void, onErrorFunc:() -> Void) { + #if hl + wsThread = sys.thread.Thread.create(() -> { + hl.Gc.enable(false); + hl.Gc.blocking(true); // Wtf is this shit + #end + wsToken++; + + var myToken = wsToken; + + ws = WebSocket.create(serverIp); + #if hl + hl.Gc.enable(true); + hl.Gc.blocking(false); + #end + ws.onopen = () -> { + open = true; + #if hl + responses.add(() -> onOpenFunc()); + #end + #if js + onOpenFunc(); + #end + } + ws.onmessageString = (m) -> { + #if hl + responses.add(() -> handleMessage(m)); + #end + #if js + handleMessage(m); + #end + } + ws.onerror = (m) -> { + #if hl + responses.add(() -> { + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to connect to master server: " + m)); + }); + if (onErrorFunc != null) + responses.add(() -> { + onErrorFunc(); + }); + #end + #if js + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to connect to master server: " + m)); + if (onErrorFunc != null) + onErrorFunc(); + #end + #if hl + stopMutex.acquire(); + #end + if (myToken == wsToken) { + open = false; + ws = null; + instance = null; + } + #if hl + stopMutex.acquire(); + stopping = true; + stopMutex.release(); + if (myToken == wsToken) { + wsThread = null; + } + #end + } + ws.onclose = (?e) -> { + #if hl + stopMutex.acquire(); + #end + if (myToken == wsToken) { + open = false; + ws = null; + instance = null; + } + #if hl + stopping = true; + stopMutex.release(); + if (myToken == wsToken) { + wsThread = null; + } + #end + } + #if hl + while (true) { + stopMutex.acquire(); + if (stopping) + break; + while (true) { + var s = toSend.pop(false); + if (s == null) + break; + #if hl + hl.Gc.blocking(true); + #end + ws.sendString(s); + #if hl + hl.Gc.blocking(false); + #end + } + + #if hl + hl.Gc.blocking(true); + #end + ws.process(); + #if hl + hl.Gc.blocking(false); + #end + stopMutex.release(); + Sys.sleep(0.1); + } + #end + #if hl + }); + #end + } + + public static function process() { + #if sys + var resp = responses.pop(false); + if (resp != null) { + resp(); + } + #end + } + + public static function connectToMasterServer(onConnect:() -> Void, onError:() -> Void = null) { + if (instance == null) + instance = new MasterServerClient(onConnect, onError); + else { + if (instance.open) + onConnect(); + else { + if (instance != null && instance.ws != null) + instance.ws.close(); + instance = new MasterServerClient(onConnect, onError); + } + } + } + + public static function disconnectFromMasterServer() { + if (instance != null && instance.ws != null) { + instance.ws.close(); + if (instance != null) { + instance.open = false; + instance.ws = null; + instance = null; + } + } + } + + function queueMessage(m:String) { + #if hl + toSend.add(m); + #end + #if js + ws.sendString(m); + #end + } + + public function heartBeat() { + queueMessage(Json.stringify({ + type: "heartbeat" + })); + } + + public function sendServerInfo(serverInfo:ServerInfo) { + queueMessage(Json.stringify({ + type: "serverInfo", + name: serverInfo.name, + players: serverInfo.players, + maxPlayers: serverInfo.maxPlayers, + privateSlots: serverInfo.privateSlots, + privateServer: serverInfo.privateServer, + inviteCode: serverInfo.inviteCode, + state: serverInfo.state, + platform: serverInfo.platform, + version: "MBP" // MarbleGame.currentVersion + })); + } + + public function sendConnectToServer(serverName:String, sdp:String, isInvite:Bool = false) { + if (!isInvite) { + queueMessage(Json.stringify({ + type: "connect", + serverName: serverName, + sdp: sdp + })); + } else { + queueMessage(Json.stringify({ + type: "connectInvite", + sdp: sdp, + inviteCode: serverName + })); + } + } + + public function getServerList(serverListCb:Array->Void) { + this.serverListCb = serverListCb; + queueMessage(Json.stringify({ + type: "serverList" + })); + } + + function handleMessage(message:String) { + var conts = Json.parse(message); + Console.log('Received ${conts.type}'); + if (conts.type == "serverList") { + if (serverListCb != null) { + serverListCb(conts.servers); + } + } + if (conts.type == "connect") { + if (!Net.isHost) { + queueMessage(Json.stringify({ + type: "connectFailed", + success: false, + reason: "The server has shut down" + })); + return; + } + var joiningPrivate = conts.isPrivate; + + if (Net.serverInfo.players >= Net.serverInfo.maxPlayers) { + queueMessage(Json.stringify({ + type: "connectFailed", + success: false, + reason: "The server is full" + })); + return; + } + var pubSlotsAvail = Net.serverInfo.maxPlayers - Net.serverInfo.privateSlots; + var privSlotsAvail = Net.serverInfo.privateSlots; + + var pubCount = 1; // Self + var privCount = 0; + for (cid => cc in Net.clientIdMap) { + if (cc.isPrivate) { + privCount++; + } else { + pubCount++; + } + } + + if (!joiningPrivate && pubCount >= pubSlotsAvail) { + queueMessage(Json.stringify({ + type: "connectFailed", + success: false, + reason: "The server is full" + })); + return; + } + + if (joiningPrivate && privCount >= privSlotsAvail) { + joiningPrivate = false; // Join publicly + } + + Net.addClientFromSdp(conts.sdp, joiningPrivate, (sdpReply) -> { + queueMessage(Json.stringify({ + success: true, + type: "connectResponse", + sdp: sdpReply, + clientId: conts.clientId + })); + }); + } + if (conts.type == "connectResponse") { + Console.log("Remote Description Received!"); + var sdpObj = Json.parse(conts.sdp); + if (@:privateAccess Net.client != null) + @:privateAccess Net.client.setRemoteDescription(sdpObj.sdp, sdpObj.type); + } + if (conts.type == "connectFailed") { + // var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content; + // if (loadGui != null) { + // loadGui.setErrorStatus(conts.reason); + // } + } + if (conts.type == "turnserver") { + Net.turnServer = conts.server; // Turn server! + } + } +} diff --git a/src/net/Move.hx b/src/net/Move.hx new file mode 100644 index 00000000..22f38775 --- /dev/null +++ b/src/net/Move.hx @@ -0,0 +1,12 @@ +package net; + +import h3d.Vector; + +class Move { + public var d:Vector; + public var jump:Bool; + public var powerup:Bool; + public var blast:Bool; + + public function new() {} +} diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx new file mode 100644 index 00000000..c70ee194 --- /dev/null +++ b/src/net/MoveManager.hx @@ -0,0 +1,294 @@ +package net; + +import net.BitStream.OutputBitStream; +import net.BitStream.InputBitStream; +import net.NetPacket.MarbleUpdatePacket; +import shapes.PowerUp; +import net.NetPacket.MarbleMovePacket; +import src.TimeState; +import src.Console; +import net.ClientConnection; +import net.Net.NetPacketType; +import src.MarbleWorld; +import net.Move; +import h3d.Vector; +import src.Gamepad; +import src.Settings; +import hxd.Key; +import src.MarbleGame; +import src.Util; +import src.Marble; + +@:publicFields +class NetMove { + var motionDir:Vector; + var move:Move; + var id:Int; + var timeState:TimeState; + var serverTicks:Int; + + public function new(move:Move, motionDir:Vector, timeState:TimeState, serverTicks:Int, id:Int) { + this.move = move; + this.motionDir = motionDir; + this.id = id; + this.serverTicks = serverTicks; + this.timeState = timeState; + } +} + +class MoveManager { + var connection:GameConnection; + var queuedMoves:Array; + var nextMoveId:Int; + var lastMove:NetMove; + var lastAckMoveId:Int = -1; + var ackRTT:Int = -1; + + var maxMoves = 45; + var maxSendMoveListSize = 30; + + var serverTargetMoveListSize = 3; + var serverMaxMoveListSize = 8; + var serverAvgMoveListSize = 3.0; + var serverSmoothMoveAvg = 0.15; + var serverMoveListSizeSlack = 1.5; + var serverDefaultMinTargetMoveListSize = 3; + var serverAbnormalMoveCount = 0; + var serverLastRecvMove = 0; + var serverLastAckMove = 0; + + public var stall = false; + + public function new(connection:GameConnection) { + queuedMoves = []; + nextMoveId = 0; + this.connection = connection; + var mv = new Move(); + mv.d = new Vector(0, 0); + } + + public function recordMove(marble:Marble, motionDir:Vector, timeState:TimeState, serverTicks:Int) { + if (queuedMoves.length >= maxMoves || stall) { + return queuedMoves[queuedMoves.length - 1]; + } + var move = new Move(); + move.d = new Vector(); + if (!MarbleGame.instance.paused) { + move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis); + move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis); + // if (@:privateAccess !MarbleGame.instance.world.playGui.isChatFocused()) { + 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 (Key.isDown(Settings.controlsSettings.blast) + || (MarbleGame.instance.touchInput.blastbutton.pressed) + || Gamepad.isDown(Settings.gamepadSettings.blast)) + move.blast = 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; + } + // } + + // quantize moves for client + var qx = Std.int((move.d.x * 16) + 16); + var qy = Std.int((move.d.y * 16) + 16); + move.d.x = (qx - 16) / 16.0; + move.d.y = (qy - 16) / 16.0; + } + + var netMove = new NetMove(move, motionDir, timeState.clone(), serverTicks, nextMoveId++); + queuedMoves.push(netMove); + + if (nextMoveId >= 65535) // 65535 is reserved for null move + nextMoveId = 0; + + var moveStartIdx = queuedMoves.length - maxSendMoveListSize; + if (moveStartIdx < 0) + moveStartIdx = 0; + + var b = new OutputBitStream(); + var movePacket = new MarbleMovePacket(); + movePacket.clientId = Net.clientId; + movePacket.moves = queuedMoves.slice(moveStartIdx); + movePacket.clientTicks = timeState.ticks; + b.writeByte(NetPacketType.MarbleMove); + movePacket.serialize(b); + + Net.sendPacketToHostUnreliable(b); + + return netMove; + } + + function copyMove(to:Int, from:Int) { + queuedMoves[to].move = queuedMoves[from].move; + queuedMoves[to].motionDir.load(queuedMoves[from].motionDir); + } + + public inline function duplicateLastMove() { + if (queuedMoves.length == 0) + return; + queuedMoves.insert(0, queuedMoves[0]); + } + + public static inline function packMove(m:NetMove, b:OutputBitStream) { + b.writeUInt16(m.id); + b.writeByte(Std.int((m.move.d.x * 16) + 16)); + b.writeByte(Std.int((m.move.d.y * 16) + 16)); + b.writeFlag(m.move.jump); + b.writeFlag(m.move.powerup); + b.writeFlag(m.move.blast); + b.writeFloat(m.motionDir.x); + b.writeFloat(m.motionDir.y); + b.writeFloat(m.motionDir.z); + return b; + } + + public static inline function unpackMove(b:InputBitStream) { + var moveId = b.readUInt16(); + var move = new Move(); + move.d = new Vector(); + move.d.x = (b.readByte() - 16) / 16.0; + move.d.y = (b.readByte() - 16) / 16.0; + move.jump = b.readFlag(); + move.powerup = b.readFlag(); + move.blast = b.readFlag(); + 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(), 0, moveId); + return netMove; + } + + public inline function queueMove(m:NetMove) { + if (serverLastRecvMove < m.id && serverLastAckMove < m.id) { + queuedMoves.push(m); + serverLastRecvMove = m.id; + } + // if (queuedMoves.length != 0) { + // var lastQueuedMove = queuedMoves[queuedMoves.length - 1]; + // if (lastQueuedMove.id < m.id) + // queuedMoves.push(m); + // } else if (lastMove == null || lastMove.id < m.id) { + // queuedMoves.push(m); + // } + } + + public function getNextMove() { + if (Net.isHost) { + serverAvgMoveListSize *= (1 - serverSmoothMoveAvg); + serverAvgMoveListSize += serverSmoothMoveAvg * queuedMoves.length; + if (serverAvgMoveListSize < serverTargetMoveListSize - serverMoveListSizeSlack + && queuedMoves.length < serverTargetMoveListSize + && queuedMoves.length != 0) { + serverAvgMoveListSize = Math.max(Std.int(serverAvgMoveListSize + serverMoveListSizeSlack + 0.5), queuedMoves.length); + // serverAbnormalMoveCount++; + // if (serverAbnormalMoveCount > 3) { + // serverTargetMoveListSize += 1; + // if (serverTargetMoveListSize > serverMaxMoveListSize) + // serverTargetMoveListSize = serverMaxMoveListSize; + // } + // Send null move + return null; + } + if (queuedMoves.length > serverMaxMoveListSize + || (serverAvgMoveListSize > serverTargetMoveListSize + serverMoveListSizeSlack + && queuedMoves.length > serverTargetMoveListSize)) { + // if (queuedMoves.length > serverMaxMoveListSize) { + var dropAmt = queuedMoves.length - serverTargetMoveListSize; + while (dropAmt-- > 0) { + queuedMoves.pop(); + } + // } + serverAvgMoveListSize = serverTargetMoveListSize; + // serverAbnormalMoveCount++; + // if (serverAbnormalMoveCount > 3) { + // serverTargetMoveListSize -= 1; + // if (serverTargetMoveListSize < serverDefaultMinTargetMoveListSize) + // serverTargetMoveListSize = serverDefaultMinTargetMoveListSize; + // } else { + // serverAbnormalMoveCount = 0; + // } + } + } + if (queuedMoves.length == 0) { + // if (lastMove != null) { + // lastMove.id++; // So that we force client's move to be overriden by this one + // } + return lastMove; + } else { + lastMove = queuedMoves[0]; + queuedMoves.shift(); + lastAckMoveId = lastMove.id; + return lastMove; + } + } + + public inline function getQueueSize() { + return queuedMoves.length; + } + + public function acknowledgeMove(m:NetMove, timeState:TimeState) { + if (m.id == 65535 || m.id == -1) { + return null; + } + if (m.id <= lastAckMoveId) + return null; // Already acked + if (queuedMoves.length == 0) + return null; + if (m.id >= nextMoveId) { + return queuedMoves[0]; // Input lag + } + while (m.id != queuedMoves[0].id) { + queuedMoves.shift(); + } + var delta = -1; + var mv = null; + if (m.id == queuedMoves[0].id) { + delta = queuedMoves[0].id - lastAckMoveId; + mv = queuedMoves.shift(); + ackRTT = timeState.ticks - mv.timeState.ticks; + // maxMoves = ackRTT + 2; + } + lastAckMoveId = m.id; + return mv; + } + + public function getMoveForTick(m:Int) { + if (m <= lastAckMoveId) + return null; + if (m == 65535 || m == -1) + return null; + if (queuedMoves.length == 0) + return null; + for (i in 0...queuedMoves.length) { + if (queuedMoves[i].id == m) + return queuedMoves[i]; + if (queuedMoves[i].id > m) + return null; + } + return null; + } +} diff --git a/src/net/Net.hx b/src/net/Net.hx new file mode 100644 index 00000000..7093448e --- /dev/null +++ b/src/net/Net.hx @@ -0,0 +1,836 @@ +package net; + +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 PlayerInfo; + var ScoreBoardInfo; +} + +@:publicFields +class ServerInfo { + var name:String; + var players:Int; + var maxPlayers:Int; + var privateSlots:Int; + var privateServer:Bool; + var inviteCode:Int; + var state:String; + var platform:NetPlatform; + + public function new(name:String, players:Int, maxPlayers:Int, privateSlots:Int, privateServer:Bool, inviteCode:Int, state:String, platform:NetPlatform) { + this.name = name; + this.players = players; + this.maxPlayers = maxPlayers; + this.privateSlots = privateSlots; + this.privateServer = privateServer; + this.inviteCode = inviteCode; + this.state = state; + this.platform = platform; + } +} + +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; + + 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; + + static var stunServers = ["stun:stun.l.google.com:19302"]; + + public static var turnServer:String = ""; + + 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()); + MasterServerClient.connectToMasterServer(() -> { + isHost = true; + isClient = false; + clientId = 0; + isMP = true; + MasterServerClient.instance.sendServerInfo(serverInfo); + onHosted(); + }); + } + + public static function addClientFromSdp(sdpString:String, privateJoin:Bool, onFinishSdp:String->Void) { + var peer = new RTCPeerConnection(stunServers, "0.0.0.0"); + var sdpObj = Json.parse(sdpString); + peer.setRemoteDescription(sdpObj.sdp, sdpObj.type); + addClient(peer, privateJoin, onFinishSdp); + } + + static function addClient(peer:RTCPeerConnection, privateJoin:Bool, 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, privateJoin); + } + } + + static function addGhost(id:Int) { + var ghost = new DummyConnection(id); + clientIdMap[id] = ghost; + } + + public static function joinServer(serverName:String, isInvite:Bool, connectedCb:() -> Void) { + MasterServerClient.connectToMasterServer(() -> { + client = new RTCPeerConnection(stunServers, "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 MultiplayerLoadingGui)) { + // var loadGui = new MultiplayerLoadingGui(msg); + // MarbleGame.canvas.setContent(loadGui); + // loadGui.setErrorStatus(msg); + // } else { + // var loadGui = cast(MarbleGame.canvas.content, MultiplayerLoadingGui); + // loadGui.setErrorStatus(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", true); + 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" + }), isInvite); + } + + 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 MultiplayerLoadingGui) { + // var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content; + // if (loadGui != null) { + // loadGui.setLoadingStatus("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", true); + } + + var onDatachannelError = (msg:String) -> { + Console.log('Errored out due to ${msg}'); + closeFunc("Connection error", true); + } + + 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.lobbyHostReady = false; + Net.lobbyClientReady = false; + Net.hostReady = false; + // 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.lobbyHostReady = false; + Net.lobbyClientReady = false; + Net.hostReady = false; + // 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 (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 MultiplayerLoadingGui)) { + // var loadGui = new MultiplayerLoadingGui("Timed out"); + // MarbleGame.canvas.setContent(loadGui); + // loadGui.setErrorStatus("Timed out"); + // } + } + } + } + } + } + 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, joiningPrivate:Bool) { + 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); + cc.isPrivate = joiningPrivate; + 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++; + } + + 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(); + } + } + + 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(); + } + } + + static function onClientHandshakeComplete(conn:ClientConnection) { + // Send our current mission to connecting client + // if (MultiplayerLevelSelectGui.custSelected) { + // NetCommands.setLobbyCustLevelNameClient(conn, MultiplayerLevelSelectGui.custPath); + // } else { + 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); + 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); + 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); // 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; + 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) + for (move in movePacket.moves) + cc.queueMove(move); + } + + 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); + @: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(); + 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); + clientIdMap[id].lobbyReady = cready; + clientIdMap[id].platform = platform; + } + if (Net.clientId == id) { + Net.lobbyClientReady = cready; + } + } + if (newP) { + // AudioManager.playSound(ResourceLoader.getAudio("sounds/spawn_alternate.wav").resource); + } + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + + 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 _: + 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 + } +} diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx new file mode 100644 index 00000000..fbd9a8bd --- /dev/null +++ b/src/net/NetCommands.hx @@ -0,0 +1,345 @@ +package net; + +import gui.MPPlayMissionGui; +import net.ClientConnection.NetPlatform; +import gui.EndGameGui; +import modes.HuntMode; +import net.ClientConnection.GameplayState; +import net.Net.NetPacketType; +import src.MarbleGame; +import src.MissionList; +import src.Console; + +@:build(net.RPCMacro.build()) +class NetCommands { + @:rpc(server) public static function setLobbyLevelIndex(category:String, i:Int) { + if (MPPlayMissionGui.setLevelFn == null) { + MPPlayMissionGui.currentCategoryStatic = category; + MPPlayMissionGui.currentSelectionStatic = i; + } else { + MPPlayMissionGui.setLevelFn(category, i); + } + } + + // @:rpc(server) public static function setLobbyCustLevelName(str:String) { + // if (MPPlayMissionGui.setLevelFn != null) { + // MPPlayMissionGui.setLevelStr(str); + // } else { + // MultiplayerLevelSelectGui.custSelected = true; + // MultiplayerLevelSelectGui.custPath = str; + // } + // } + + @:rpc(server) public static function playLevel(category:String, levelIndex:Int) { + MPPlayMissionGui.playSelectedLevel(category, 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 playCustomLevel(levelPath:String) { + // var levelEntry = MPCustoms.missionList.filter(x -> x.path == levelPath)[0]; + // MarbleGame.canvas.setContent(new MultiplayerLoadingGui("Downloading", false)); + // MPCustoms.play(levelEntry, () -> {}, () -> { + // MarbleGame.canvas.setContent(new MultiplayerGui()); + // Net.disconnect(); // disconnect from the server + // }); + // 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 playCustomLevelMidJoin(path:String) { + // if (Net.isClient) { + // playCustomLevel(path); + // } + // } + + @:rpc(server) public static function enterLobby() { + if (Net.isClient) { + MarbleGame.canvas.setContent(new MPPlayMissionGui(false)); + } + } + + @:rpc(server) public static function setNetworkRNG(rng:Float) { + Net.networkRNG = rng; + if (MarbleGame.instance.world != null) { + var gameMode = MarbleGame.instance.world.gameMode; + if (gameMode is modes.HuntMode) { + var hunt:modes.HuntMode = cast gameMode; + @:privateAccess hunt.rng.setSeed(cast rng); + @:privateAccess hunt.rng2.setSeed(cast rng); + } + } + } + + @:rpc(client) public static function toggleReadiness(clientId:Int) { + if (Net.isHost) { + if (clientId == 0) + Net.lobbyHostReady = !Net.lobbyHostReady; + else + Net.clientIdMap[clientId].toggleLobbyReady(); + var allReady = true; + for (id => client in Net.clientIdMap) { + if (!client.lobbyReady) { + allReady = false; + break; + } + } + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + var b = Net.sendPlayerInfosBytes(); + for (cc in Net.clients) { + cc.sendBytes(b); + } + + if (allReady && Net.lobbyHostReady) { + // if (MultiplayerLevelSelectGui.custSelected) { + // NetCommands.playCustomLevel(MultiplayerLevelSelectGui.custPath); + // } else + NetCommands.playLevel(MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic); + } + } + } + + @:rpc(client) public static function clientIsReady(clientId:Int) { + if (Net.isHost) { + if (Net.serverInfo.state == "WAITING") { + Console.log('Client ${clientId} is ready!'); + if (clientId != -1) + Net.clientIdMap[clientId].ready(); + else + Net.hostReady = true; + var allReady = true; + for (id => client in Net.clientIdMap) { + if (client.state != GameplayState.GAME) { + allReady = false; + break; + } + } + if (allReady && Net.hostReady) { + if (MarbleGame.instance.world != null) { + Console.log('All are ready, starting'); + MarbleGame.instance.world.allClientsReady(); + } + 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 + if (MarbleGame.instance.world != null) { + var packets = MarbleGame.instance.world.getWorldStateForClientJoin(); + var c = Net.clientIdMap[clientId]; + for (packet in packets) { + c.sendBytes(packet); + } + Net.clientIdMap[clientId].ready(); + + if (MarbleGame.instance.world.serverStartTicks == 0) { + 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(); + } + } + } else { + // Send the start ticks + NetCommands.setStartTicksMidJoinClient(c, MarbleGame.instance.world.serverStartTicks, MarbleGame.instance.world.timeState.ticks); + } + } + } + } + } + + @:rpc(server) public static function addMidGameJoinMarble(cc:Int) { + if (Net.isClient) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.addJoiningClientGhost(Net.clientIdMap[cc], () -> {}); + } + } + } + + @:rpc(server) public static function setStartTicks(ticks:Int) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.serverStartTicks = ticks + 1; // Extra tick so we don't get 0 + if (Net.isClient) { + @:privateAccess MarbleGame.instance.world.marble.serverTicks = ticks; + } + MarbleGame.instance.world.startTime = MarbleGame.instance.world.timeState.timeSinceLoad + 3.5 + 0.032; // 1 extra tick + } + } + + @: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) { + if (MarbleGame.instance.paused) { + MarbleGame.instance.handlePauseGame(); // Unpause + } + 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) { + var conn = Net.clientIdMap.get(clientId); + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.removePlayer(conn); + + var allReady = true; + for (id => client in Net.clientIdMap) { + if (client.state != GameplayState.GAME && client != conn) { + allReady = false; + break; + } + } + if (allReady && MarbleGame.instance.world.serverStartTicks == 0) { + MarbleGame.instance.world.allClientsReady(); + } + } + Net.clientIdMap.remove(clientId); + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + } + + @:rpc(server) public static function clientJoin(clientId:Int) {} + + @:rpc(client) public static function clientLeave(clientId:Int) { + if (Net.isHost) { + @:privateAccess Net.onClientLeave(cast Net.clientIdMap[clientId]); + } + } + + @:rpc(server) public static function serverClosed() { + if (Net.isClient) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.quitMission(); + } + // var loadGui = new MultiplayerLoadingGui("Server closed"); + // MarbleGame.canvas.setContent(loadGui); + // loadGui.setErrorStatus("Server closed"); + } + } + + @:rpc(client) public static function setPlayerData(clientId:Int, name:String, marble:Int) { + if (Net.isHost) { + Net.clientIdMap[clientId].setName(name); + Net.clientIdMap[clientId].setMarbleId(marble); + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + } + } + + @:rpc(client) public static function transmitPlatform(clientId:Int, platform:Int) { + if (Net.isHost) { + Net.clientIdMap[clientId].platform = platform; + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + } + } + + @:rpc(server) public static function endGame() { + for (c => v in Net.clientIdMap) { + v.state = LOBBY; + v.lobbyReady = false; + } + if (Net.isClient) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.quitMission(); + } + } + if (Net.isHost) { + Net.lobbyHostReady = false; + Net.hostReady = false; + + Net.serverInfo.state = "LOBBY"; + MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state + var b = Net.sendPlayerInfosBytes(); + for (cc in Net.clients) { + cc.sendBytes(b); + } + } + } + + @:rpc(server) public static function restartGame() { + var world = MarbleGame.instance.world; + if (Net.isHost) { + world.startTime = 1e8; + haxe.Timer.delay(() -> world.allClientsReady(), 1500); + } + if (Net.isClient) { + var gui = MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1]; + if (gui is EndGameGui) { + var egg = cast(gui, EndGameGui); + // egg.retryFunc(null); + world.restartMultiplayerState(); + } + } + world.multiplayerStarted = false; + } + + @:rpc(server) public static function ping(sendTime:Float) { + if (Net.isClient) { + pingBack(Console.time() - sendTime); + } + } + + @:rpc(client) public static function pingBack(ping:Float) { + // Do nothing??? + } + + @:rpc(client) public static function requestPing() { + if (Net.isHost) { + ping(Console.time()); + } + } + + // @:rpc(client) public static function sendChatMessage(msg:String) { + // if (Net.isHost) { + // sendServerChatMessage(msg); + // } + // } + // @:rpc(server) public static function sendServerChatMessage(msg:String) { + // if (MarbleGame.instance.world != null) { + // if (MarbleGame.instance.world._ready) { + // @:privateAccess MarbleGame.instance.world.playGui.addChatMessage(msg); + // } + // } else { + // if (MarbleGame.canvas.content is MultiplayerLevelSelectGui) { + // cast(MarbleGame.canvas.content, MultiplayerLevelSelectGui).addChatMessage(msg); + // } + // } + // } +} diff --git a/src/net/NetPacket.hx b/src/net/NetPacket.hx new file mode 100644 index 00000000..6ee74c82 --- /dev/null +++ b/src/net/NetPacket.hx @@ -0,0 +1,258 @@ +package net; + +import net.BitStream.InputBitStream; +import net.BitStream.OutputBitStream; +import h3d.Vector; +import net.MoveManager.NetMove; + +interface NetPacket { + public function serialize(b:OutputBitStream):Void; + public function deserialize(b:InputBitStream):Void; +} + +@:publicFields +class MarbleMovePacket implements NetPacket { + var clientId:Int; + var clientTicks:Int; + var moves:Array; + + public function new() { + moves = []; + } + + public inline function deserialize(b:InputBitStream) { + clientId = b.readByte(); + clientTicks = b.readUInt16(); + var count = b.readInt(5); + moves = []; + for (i in 0...count) { + moves.push(MoveManager.unpackMove(b)); + } + } + + public inline function serialize(b:OutputBitStream) { + b.writeByte(clientId); + b.writeUInt16(clientTicks); + b.writeInt(moves.length, 5); + for (move in moves) + MoveManager.packMove(move, b); + } +} + +enum abstract MarbleNetFlags(Int) from Int to Int { + var NullFlag = 0; + var DoBlast = 1 << 0; + var DoHelicopter = 1 << 1; + var DoMega = 1 << 2; + var PickupPowerup = 1 << 3; + var GravityChange = 1 << 4; + var UsePowerup = 1 << 5; +} + +@:publicFields +class MarbleUpdatePacket implements NetPacket { + var clientId:Int; + var move:NetMove; + var serverTicks:Int; + var calculationTicks:Int = -1; + var position:Vector; + var velocity:Vector; + var omega:Vector; + var blastAmount:Int; + var blastTick:Int; + var megaTick:Int; + var heliTick:Int; + var gravityDirection:Vector; + var oob:Bool; + var powerUpId:Int; + var moveQueueSize:Int; + var netFlags:Int; + + public function new() {} + + public inline function serialize(b:OutputBitStream) { + b.writeByte(clientId); + MoveManager.packMove(move, b); + b.writeUInt16(serverTicks); + b.writeByte(moveQueueSize); + b.writeFloat(position.x); + b.writeFloat(position.y); + b.writeFloat(position.z); + b.writeFloat(velocity.x); + b.writeFloat(velocity.y); + b.writeFloat(velocity.z); + b.writeFloat(omega.x); + b.writeFloat(omega.y); + b.writeFloat(omega.z); + b.writeInt(blastAmount, 11); + if (netFlags & MarbleNetFlags.DoBlast > 0) { + b.writeFlag(true); + b.writeUInt16(blastTick); + } else { + b.writeFlag(false); + } + if (netFlags & MarbleNetFlags.DoHelicopter > 0) { + b.writeFlag(true); + b.writeUInt16(heliTick); + } else { + b.writeFlag(false); + } + if (netFlags & MarbleNetFlags.DoMega > 0) { + b.writeFlag(true); + b.writeUInt16(megaTick); + } else { + b.writeFlag(false); + } + b.writeFlag(oob); + if (netFlags & MarbleNetFlags.UsePowerup > 0) { + b.writeFlag(true); + } else { + b.writeFlag(false); + } + + if (netFlags & MarbleNetFlags.PickupPowerup > 0) { + b.writeFlag(true); + b.writeInt(powerUpId, 9); + } else { + b.writeFlag(false); + } + if (netFlags & MarbleNetFlags.GravityChange > 0) { + b.writeFlag(true); + b.writeFloat(gravityDirection.x); + b.writeFloat(gravityDirection.y); + b.writeFloat(gravityDirection.z); + } else { + b.writeFlag(false); + } + } + + public inline function deserialize(b:InputBitStream) { + clientId = b.readByte(); + move = MoveManager.unpackMove(b); + serverTicks = b.readUInt16(); + moveQueueSize = b.readByte(); + position = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + velocity = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + omega = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + blastAmount = b.readInt(11); + this.netFlags = 0; + if (b.readFlag()) { + blastTick = b.readUInt16(); + this.netFlags |= MarbleNetFlags.DoBlast; + } + if (b.readFlag()) { + heliTick = b.readUInt16(); + this.netFlags |= MarbleNetFlags.DoHelicopter; + } + if (b.readFlag()) { + megaTick = b.readUInt16(); + this.netFlags |= MarbleNetFlags.DoMega; + } + oob = b.readFlag(); + if (b.readFlag()) + this.netFlags |= MarbleNetFlags.UsePowerup; + if (b.readFlag()) { + powerUpId = b.readInt(9); + this.netFlags |= MarbleNetFlags.PickupPowerup; + } + if (b.readFlag()) { + gravityDirection = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); + this.netFlags |= MarbleNetFlags.GravityChange; + } + } +} + +@:publicFields +class PowerupPickupPacket implements NetPacket { + var clientId:Int; + var serverTicks:Int; + var powerupItemId:Int; + + public function new() {} + + public inline function deserialize(b:InputBitStream) { + clientId = b.readByte(); + serverTicks = b.readUInt16(); + powerupItemId = b.readInt(10); + } + + public inline function serialize(b:OutputBitStream) { + b.writeByte(clientId); + b.writeUInt16(serverTicks); + b.writeInt(powerupItemId, 10); + } +} + +@:publicFields +class GemSpawnPacket implements NetPacket { + var gemIds:Array; + + public function new() { + gemIds = []; + } + + public function serialize(b:OutputBitStream) { + b.writeInt(gemIds.length, 5); + for (gemId in gemIds) { + b.writeInt(gemId, 11); + } + } + + public function deserialize(b:InputBitStream) { + var count = b.readInt(5); + for (i in 0...count) { + gemIds.push(b.readInt(11)); + } + } +} + +@:publicFields +class GemPickupPacket implements NetPacket { + var clientId:Int; + var serverTicks:Int; + var gemId:Int; + var scoreIncr:Int; + + public function new() {} + + public inline function deserialize(b:InputBitStream) { + clientId = b.readByte(); + serverTicks = b.readUInt16(); + gemId = b.readInt(11); + scoreIncr = b.readInt(4); + } + + public inline function serialize(b:OutputBitStream) { + b.writeByte(clientId); + b.writeUInt16(serverTicks); + b.writeInt(gemId, 11); + b.writeInt(scoreIncr, 4); + } +} + +@:publicFields +class ScoreboardPacket implements NetPacket { + var scoreBoard:Map; + + public function new() { + scoreBoard = new Map(); + } + + public inline function deserialize(b:InputBitStream) { + var count = b.readInt(4); + for (i in 0...count) { + scoreBoard[b.readInt(6)] = b.readInt(10); + } + } + + public inline function serialize(b:OutputBitStream) { + var keycount = 0; + for (k => v in scoreBoard) + keycount++; + b.writeInt(keycount, 4); + for (key => v in scoreBoard) { + b.writeInt(key, 6); + b.writeInt(v, 10); + } + } +} diff --git a/src/net/PowerupPredictionStore.hx b/src/net/PowerupPredictionStore.hx new file mode 100644 index 00000000..2bcb4923 --- /dev/null +++ b/src/net/PowerupPredictionStore.hx @@ -0,0 +1,24 @@ +package net; + +import src.TimeState; +import net.NetPacket.PowerupPickupPacket; + +class PowerupPredictionStore { + var predictions:Array; + + public inline function new() { + predictions = []; + } + + public inline function alloc() { + predictions.push(Math.NEGATIVE_INFINITY); + } + + public inline function getState(netIndex:Int) { + return predictions[netIndex]; + } + + public inline function acknowledgePowerupPickup(packet:PowerupPickupPacket, timeState:TimeState, futureTicks:Int) { + predictions[packet.powerupItemId] = timeState.currentAttemptTime - futureTicks * 0.032; // Approximate + } +} diff --git a/src/net/RPCMacro.hx b/src/net/RPCMacro.hx new file mode 100644 index 00000000..55a57670 --- /dev/null +++ b/src/net/RPCMacro.hx @@ -0,0 +1,175 @@ +package net; + +import haxe.macro.Context; +import haxe.macro.Expr; + +class RPCMacro { + macro static public function build():Array { + var fields = Context.getBuildFields(); + + var rpcFnId = 1; + + var idtoFn:Map, + deserialize:Array + }> = new Map(); + + var fieldsToAdd = []; + + for (field in fields) { + if (field.meta.length > 0 && field.meta[0].name == ':rpc') { + switch (field.kind) { + case FFun(f): + { + var serializeFns = []; + var deserializeFns = []; + var callExprs = []; + for (arg in f.args) { + var argName = arg.name; + switch (arg.type) { + case TPath({ + name: 'Int' + }): { + deserializeFns.push(macro var $argName = stream.readInt32()); + callExprs.push(macro $i{argName}); + serializeFns.push(macro stream.writeInt32($i{argName})); + } + + case TPath({ + name: 'Float' + }): { + deserializeFns.push(macro var $argName = stream.readFloat()); + callExprs.push(macro $i{argName}); + serializeFns.push(macro stream.writeFloat($i{argName})); + } + + case TPath({ + name: 'String' + }): { + deserializeFns.push(macro var $argName = stream.readString()); + callExprs.push(macro $i{argName}); + serializeFns.push(macro stream.writeString($i{argName})); + } + + case _: {} + } + } + deserializeFns.push(macro { + $i{field.name}($a{callExprs}); + }); + idtoFn.set(rpcFnId, { + name: field.name, + serialize: serializeFns, + deserialize: deserializeFns + }); + + var directionParam = field.meta[0].params[0].expr; + switch (directionParam) { + case EConst(CIdent("server")): + var lastExpr = macro { + if (Net.isHost) { + var stream = new net.BitStream.OutputBitStream(); + stream.writeByte(NetPacketType.NetCommand); + stream.writeByte($v{rpcFnId}); + $b{serializeFns}; + Net.sendPacketToAll(stream); + } + }; + var origExpr = f.expr; + var lastExprSingle = macro { + if (Net.isHost) { + var stream = new net.BitStream.OutputBitStream(); + stream.writeByte(NetPacketType.NetCommand); + stream.writeByte($v{rpcFnId}); + $b{serializeFns}; + Net.sendPacketToClient(client, stream); + } + }; + + var singleClientfn:Field = { + name: field.name + "Client", + pos: Context.currentPos(), + access: [APublic, AStatic], + kind: FFun({ + args: [ + { + name: "client", + type: haxe.macro.TypeTools.toComplexType(Context.getType('net.ClientConnection.GameConnection')) + } + ].concat(f.args), + expr: macro $b{[origExpr, lastExprSingle]} + }) + }; + fieldsToAdd.push(singleClientfn); + + f.expr = macro $b{[f.expr, lastExpr]}; + + case EConst(CIdent("client")): + var lastExpr = macro { + if (!Net.isHost) { + var stream = new net.BitStream.OutputBitStream(); + stream.writeByte(NetPacketType.NetCommand); + stream.writeByte($v{rpcFnId}); + $b{serializeFns}; + Net.sendPacketToHost(stream); + } + }; + + f.expr = macro $b{[f.expr, lastExpr]}; + + case _: + {} + } + + rpcFnId++; + } + + case _: + {} + } + } + } + + var cases:Array = []; + for (k => v in idtoFn) { + cases.push({ + values: [macro $v{k}], + expr: macro { + $b{v.deserialize} + } + }); + } + + var deserializeField:Field = { + name: "readPacket", + pos: Context.currentPos(), + access: [APublic, AStatic], + kind: FFun({ + args: [ + { + name: "stream", + type: haxe.macro.TypeTools.toComplexType(Context.getType('net.BitStream.InputBitStream')) + } + ], + expr: macro { + var fnId = stream.readByte(); + + $e{ + { + expr: ESwitch(macro fnId, cases, null), + pos: Context.currentPos() + } + } + } + }) + }; + + fields.push(deserializeField); + for (fn in fieldsToAdd) { + fields.push(fn); + } + + return fields; + } +} diff --git a/src/rewind/RewindManager.hx b/src/rewind/RewindManager.hx index 0479274d..584aa593 100644 --- a/src/rewind/RewindManager.hx +++ b/src/rewind/RewindManager.hx @@ -38,7 +38,7 @@ class RewindManager { public function recordFrame() { var rf = new RewindFrame(); rf.timeState = level.timeState.clone(); - rf.marblePosition = level.marble.getAbsPos().getPosition().clone(); + rf.marblePosition = level.marble.collider.transform.getPosition().clone(); rf.marbleOrientation = level.marble.getRotationQuat().clone(); rf.marbleVelocity = level.marble.velocity.clone(); rf.marbleAngularVelocity = level.marble.omega.clone(); @@ -98,7 +98,7 @@ class RewindManager { rf.powerupStates.push(ab.lastContactTime); } } - rf.blastAmt = level.blastAmount; + rf.blastAmt = level.marble.blastAmount; rf.oobState = { oob: level.marble.outOfBounds, timeState: level.marble.outOfBoundsTime != null ? level.marble.outOfBoundsTime.clone() : null @@ -121,7 +121,7 @@ class RewindManager { public function applyFrame(rf:RewindFrame) { level.timeState = rf.timeState.clone(); - level.marble.setPosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z); + level.marble.setMarblePosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z); level.marble.setRotationQuat(rf.marbleOrientation.clone()); level.marble.velocity.set(rf.marbleVelocity.x, rf.marbleVelocity.y, rf.marbleVelocity.z); level.marble.omega.set(rf.marbleAngularVelocity.x, rf.marbleAngularVelocity.y, rf.marbleAngularVelocity.z); @@ -219,7 +219,7 @@ class RewindManager { level.marble.outOfBounds = rf.oobState.oob; level.marble.camera.oob = rf.oobState.oob; level.marble.outOfBoundsTime = rf.oobState.timeState != null ? rf.oobState.timeState.clone() : null; - level.blastAmount = rf.blastAmt; + level.marble.blastAmount = rf.blastAmt; @:privateAccess level.checkpointUp = rf.checkpointState.checkpointUp; @:privateAccess level.checkpointCollectedGems = rf.checkpointState.checkpointCollectedGems; @:privateAccess level.cheeckpointBlast = rf.checkpointState.checkpointBlast; diff --git a/src/shapes/AntiGravity.hx b/src/shapes/AntiGravity.hx index 7d359580..5844f17b 100644 --- a/src/shapes/AntiGravity.hx +++ b/src/shapes/AntiGravity.hx @@ -6,6 +6,7 @@ import src.TimeState; import h3d.Vector; import src.DtsObject; import src.MarbleWorld; +import net.NetPacket.MarbleNetFlags; class AntiGravity extends PowerUp { public function new(element:MissionElementItem, norespawn:Bool = false) { @@ -33,7 +34,7 @@ class AntiGravity extends PowerUp { if (marble == level.marble) this.level.setUp(marble, direction, timeState); else { - // @:privateAccess marble.netFlags |= MarbleNetFlags.GravityChange; + @:privateAccess marble.netFlags |= MarbleNetFlags.GravityChange; marble.currentUp.load(direction); } } diff --git a/src/shapes/Blast.hx b/src/shapes/Blast.hx index 3d168a47..1ee1fb50 100644 --- a/src/shapes/Blast.hx +++ b/src/shapes/Blast.hx @@ -31,6 +31,7 @@ class Blast extends PowerUp { } public function use(marble:src.Marble, timeState:TimeState) { - this.level.blastAmount = 1.03; + marble.blastAmount = 1.03; + marble.blastTicks = 36000 >> 5; // Fix me } } diff --git a/src/shapes/Nuke.hx b/src/shapes/Nuke.hx index 47e154ff..8af41d36 100644 --- a/src/shapes/Nuke.hx +++ b/src/shapes/Nuke.hx @@ -128,10 +128,12 @@ class Nuke extends DtsObject { this.setCollisionEnabled(false); // if (!this.level.rewinding) - AudioManager.playSound(ResourceLoader.getResource("data/sound/nukeexplode.wav", ResourceLoader.getAudio, this.soundResources)); - this.level.particleManager.createEmitter(nukeParticle, nukeParticleData, this.getAbsPos().getPosition()); - this.level.particleManager.createEmitter(nukeSmokeParticle, nukeSmokeParticleData, this.getAbsPos().getPosition()); - this.level.particleManager.createEmitter(nukeSparksParticle, nukeSparkParticleData, this.getAbsPos().getPosition()); + if (@:privateAccess !marble.isNetUpdate) { + AudioManager.playSound(ResourceLoader.getResource("data/sound/nukeexplode.wav", ResourceLoader.getAudio, this.soundResources)); + this.level.particleManager.createEmitter(nukeParticle, nukeParticleData, this.getAbsPos().getPosition()); + this.level.particleManager.createEmitter(nukeSmokeParticle, nukeSmokeParticleData, this.getAbsPos().getPosition()); + this.level.particleManager.createEmitter(nukeSparksParticle, nukeSparkParticleData, this.getAbsPos().getPosition()); + } var minePos = this.getAbsPos().getPosition(); var dtsCenter = this.dts.bounds.center(); diff --git a/src/shapes/PowerUp.hx b/src/shapes/PowerUp.hx index b1d39372..952ad527 100644 --- a/src/shapes/PowerUp.hx +++ b/src/shapes/PowerUp.hx @@ -8,6 +8,9 @@ import src.Util; import h3d.Vector; import src.DtsObject; import src.Marble; +import net.Net; +import net.BitStream.OutputBitStream; +import net.NetPacket.PowerupPickupPacket; abstract class PowerUp extends DtsObject { public var lastPickUpTime:Float = -1; @@ -40,6 +43,23 @@ abstract class PowerUp extends DtsObject { if (this.pickUp(marble)) { // this.level.replay.recordMarbleInside(this); + if (level.isMultiplayer && Net.isHost) { + var b = new OutputBitStream(); + b.writeByte(NetPacketType.PowerupPickup); + var pickupPacket = new PowerupPickupPacket(); + pickupPacket.clientId = @:privateAccess marble.connection != null ? @:privateAccess marble.connection.id : 0; + pickupPacket.serverTicks = timeState.ticks; + pickupPacket.powerupItemId = this.netIndex; + pickupPacket.serialize(b); + Net.sendPacketToIngame(b); + pickupClient = pickupPacket.clientId; + pickupTicks = pickupPacket.serverTicks; + } + + if (level.isMultiplayer && Net.isClient) { + pickupClient = @:privateAccess marble.connection != null ? @:privateAccess marble.connection.id : Net.clientId; + } + this.lastPickUpTime = timeState.currentAttemptTime; if (this.autoUse) this.use(marble, timeState);