diff --git a/compile.hxml b/compile.hxml index 00e9d2cd..496d7199 100644 --- a/compile.hxml +++ b/compile.hxml @@ -1,6 +1,8 @@ -cp src -lib heaps -lib hlsdl +-lib hxWebSockets +-lib datachannel -hl marblegame.hl -D windowSize=1280x720 -D keep-inline-positions diff --git a/server/Signalling.hx b/server/Signalling.hx new file mode 100644 index 00000000..c7376b08 --- /dev/null +++ b/server/Signalling.hx @@ -0,0 +1,38 @@ +import haxe.Json; +import hx.ws.SocketImpl; +import hx.ws.WebSocketHandler; +import hx.ws.WebSocketServer; + +using Lambda; + +class SignallingHandler extends WebSocketHandler { + static var clients:Array = []; + + public function new(s:SocketImpl) { + super(s); + onopen = () -> { + clients.push(this); + } + onclose = () -> { + clients.remove(this); + } + onmessage = (m) -> { + switch (m) { + case StrMessage(content): + var conts = Json.parse(content); + if (conts.type == "connect") { + var other = clients.find(x -> x != this); + other.send(Json.stringify(conts.sdpObj)); + } + case _: {} + } + } + } +} + +class Signalling { + static function main() { + var ws = new WebSocketServer("0.0.0.0", 8080, 2); + ws.start(); + } +} diff --git a/server/build_signalling.hxml b/server/build_signalling.hxml new file mode 100644 index 00000000..924cb71c --- /dev/null +++ b/server/build_signalling.hxml @@ -0,0 +1,5 @@ +--library hxWebSockets +--library datachannel +--main Signalling +-cp . +--hl bin/signalling.hl \ No newline at end of file diff --git a/src/Console.hx b/src/Console.hx index f554e969..946b1c3b 100644 --- a/src/Console.hx +++ b/src/Console.hx @@ -184,6 +184,9 @@ class Console { log('Allocation Count: ${gc.allocationCount}'); log('Memory usage: ${gc.currentMemory}'); #end + } else if (cmdType == 'rollback') { + var t = Std.parseFloat(cmdSplit[1]); + MarbleGame.instance.world.rollback(t); } else { error("Unknown command"); } diff --git a/src/DynamicPolygon.hx b/src/DynamicPolygon.hx index 4f4ba1af..488e747b 100644 --- a/src/DynamicPolygon.hx +++ b/src/DynamicPolygon.hx @@ -298,94 +298,4 @@ class DynamicPolygon extends MeshPrimitive { else engine.renderMultiBuffers(bufs, engine.mem.triIndexes, 0, triCount()); } - - #if hxbit - override function customSerialize(ctx:hxbit.Serializer) { - ctx.addInt(points.length); - for (p in points) { - ctx.addDouble(p.x); - ctx.addDouble(p.y); - ctx.addDouble(p.z); - } - if (normals == null) - ctx.addInt(0); - else { - ctx.addInt(normals.length); - for (p in normals) { - ctx.addDouble(p.x); - ctx.addDouble(p.y); - ctx.addDouble(p.z); - } - } - if (tangents == null) - ctx.addInt(0); - else { - ctx.addInt(tangents.length); - for (p in tangents) { - ctx.addDouble(p.x); - ctx.addDouble(p.y); - ctx.addDouble(p.z); - } - } - if (uvs == null) - ctx.addInt(0); - else { - ctx.addInt(uvs.length); - for (uv in uvs) { - ctx.addDouble(uv.u); - ctx.addDouble(uv.v); - } - } - if (idx == null) - ctx.addInt(0); - else { - ctx.addInt(idx.length); - for (i in idx) - ctx.addInt(i); - } - if (colors == null) - ctx.addInt(0); - else { - ctx.addInt(colors.length); - for (c in colors) { - ctx.addDouble(c.x); - ctx.addDouble(c.y); - ctx.addDouble(c.z); - } - } - } - - override function customUnserialize(ctx:hxbit.Serializer) { - points = [ - for (i in 0...ctx.getInt()) - new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble()) - ]; - normals = [ - for (i in 0...ctx.getInt()) - new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble()) - ]; - tangents = [ - for (i in 0...ctx.getInt()) - new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble()) - ]; - uvs = [for (i in 0...ctx.getInt()) new UV(ctx.getDouble(), ctx.getDouble())]; - if (normals.length == 0) - normals = null; - if (uvs.length == 0) - uvs = null; - var nindex = ctx.getInt(); - if (nindex > 0) { - idx = new hxd.IndexBuffer(); - idx.grow(nindex); - for (i in 0...nindex) - idx[i] = ctx.getInt(); - } - colors = [ - for (i in 0...ctx.getInt()) - new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble()) - ]; - if (colors.length == 0) - colors = null; - } - #end } diff --git a/src/Main.hx b/src/Main.hx index 5fee9e17..6ca98cfc 100644 --- a/src/Main.hx +++ b/src/Main.hx @@ -1,5 +1,6 @@ package; +import datachannel.RTC; import gui.VersionGui; import gui.PresentsGui; import src.Debug; @@ -88,6 +89,7 @@ class Main extends hxd.App { #end // try { + RTC.init(); Http.init(); haxe.MainLoop.add(() -> Http.loop()); Settings.init(); @@ -148,6 +150,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 ac5bbe45..17dd5768 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -66,6 +66,7 @@ import src.ResourceLoaderWorker; import src.InteriorObject; import src.Console; import src.Gamepad; +import net.Net; class Move { public var d:Vector; @@ -284,12 +285,15 @@ class Marble extends GameObject { public var cubemapRenderer:CubemapRenderer; + var connection:net.Net.ClientConnection; + public function new() { super(); 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"; @@ -318,8 +322,9 @@ class Marble extends GameObject { this.helicopterSound.pause = true; } - public function init(level:MarbleWorld, onFinish:Void->Void) { + public function init(level:MarbleWorld, connection:ClientConnection, onFinish:Void->Void) { this.level = level; + this.connection = connection; if (this.level != null) this.collisionWorld = this.level.collisionWorld; @@ -522,6 +527,8 @@ class Marble extends GameObject { public function getMarbleAxis() { var motiondir = new Vector(0, -1, 0); + if (level.isReplayingMovement) + return level.currentInputMoves[1].marbleAxes; if (this.controllable) { motiondir.transform(Matrix.R(0, 0, camera.CameraYaw)); motiondir.transform(level.newOrientationQuat.toMatrix()); @@ -561,7 +568,7 @@ class Marble extends GameObject { for (contact in contacts) { if (contact.force != 0 && !forceObjects.contains(contact.otherObject)) { if (contact.otherObject is RoundBumper) { - if (!playedSounds.contains("data/sound/bumperding1.wav")) { + if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav")) { AudioManager.playSound(ResourceLoader.getResource("data/sound/bumperding1.wav", ResourceLoader.getAudio, this.soundResources)); playedSounds.push("data/sound/bumperding1.wav"); } @@ -605,6 +612,8 @@ class Marble extends GameObject { var R = currentGravityDir.multiply(-this._radius); var rollVelocity = this.omega.cross(R); var axes = this.getMarbleAxis(); + if (!level.isReplayingMovement) + level.inputRecorder.recordAxis(axes); var sideDir = axes[0]; var motionDir = axes[1]; var upDir = axes[2]; @@ -799,7 +808,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 (!level.isReplayingMovement && !playedSounds.contains("data/sound/jump.wav")) { AudioManager.playSound(ResourceLoader.getResource("data/sound/jump.wav", ResourceLoader.getAudio, this.soundResources)); playedSounds.push("data/sound/jump.wav"); } @@ -879,7 +888,7 @@ class Marble extends GameObject { } function bounceEmitter(speed:Float, normal:Vector) { - if (!this.controllable) + if (!this.controllable || level.isReplayingMovement) return; if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) { this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData, @@ -914,6 +923,8 @@ class Marble extends GameObject { } function playBoundSound(time:Float, contactVel:Float) { + if (level.isReplayingMovement) + return; if (minVelocityBounceSoft <= contactVel) { var hardBounceSpeed = minVelocityBounceHard; var bounceSoundNum = Math.floor(Math.random() * 4); @@ -943,6 +954,8 @@ class Marble extends GameObject { } function updateRollSound(time:TimeState, contactPct:Float, slipAmount:Float) { + if (level.isReplayingMovement) + return; var rSpat = rollSound.getEffect(Spatialization); rSpat.position = this.collider.transform.getPosition(); @@ -1542,6 +1555,7 @@ class Marble extends GameObject { // 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.level.outOfBounds) { @@ -1592,41 +1606,139 @@ class Marble extends GameObject { this.updateRollSound(timeState, contactTime / timeState.dt, this._slipAmount); } - 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; + // MP Only Functions + public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { + var move:Move = null; + if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) { + move = recordMove(); + } + if (!this.controllable && this.connection != null) { + move = new Move(); + move.d = new Vector(0, 0); + } + + playedSounds = []; + advancePhysics(timeState, move, collisionWorld, pathedInteriors); + + physicsAccumulator = 0; + } + + public function updateClient(timeState:TimeState, pathedInteriors:Array) { + if (oldPos != null && newPos != null) { + var deltaT = physicsAccumulator / 0.032; + var renderPos = Util.lerpThreeVectors(this.oldPos, this.newPos, deltaT); + this.setPosition(renderPos.x, renderPos.y, renderPos.z); + + var rot = this.prevRot; + var quat = new Quat(); + quat.initRotation(omega.x * physicsAccumulator, omega.y * physicsAccumulator, omega.z * physicsAccumulator); + quat.multiply(quat, rot); + this.setRotationQuat(quat); + + var adt = timeState.clone(); + adt.dt = physicsAccumulator; + for (pi in pathedInteriors) { + pi.update(adt); } - 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; + } + physicsAccumulator += timeState.dt; + + if (this.controllable && this.level != null && !this.level.rewinding) { + // this.camera.startCenterCamera(); + this.camera.update(timeState.currentAttemptTime, timeState.dt); + } + + updatePowerupStates(timeState.currentAttemptTime, timeState.dt); + + 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.2349999994039536 * s) + s + 1.0 + 0.4799999892711639 * s * s); + this._renderScale *= s; + s = 1 - s; + this._renderScale += s * this._marbleScale; + var marbledts = cast(this.getChildAt(0), DtsObject); + marbledts.setScale(this._renderScale); + + if (this._radius != 0.675 && timeState.currentAttemptTime - this.megaMarbleEnableTime < 10) { + this._prevRadius = this._radius; + this._radius = 0.675; + this.collider.radius = 0.675; + this._marbleScale *= 2.25; + var boost = this.level.currentUp.multiply(5); + this.velocity = this.velocity.add(boost); + } else if (timeState.currentAttemptTime - this.megaMarbleEnableTime > 10) { + if (this._radius != this._prevRadius) { + this._radius = this._prevRadius; + this.collider.radius = this._radius; + this._marbleScale = this._defaultScale; + AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null, false); } } + this.updateFinishAnimation(timeState.dt); + if (this.mode == Finish) { + this.setPosition(this.finishAnimPosition.x, this.finishAnimPosition.y, this.finishAnimPosition.z); + updatePowerupStates(timeState.currentAttemptTime, timeState.dt); + } + + 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 (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; + } + return move; + } + + // SP only function + public function update(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { + var move:Move = null; + if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) { + move = recordMove(); + } + + if (level.isReplayingMovement) + move = level.currentInputMoves[1].move; + if (this.controllable && this.level.isWatching) { + move = new Move(); if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(Jumped)) move.jump = true; if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(UsedPowerup)) @@ -1638,6 +1750,10 @@ class Marble extends GameObject { this.level.replay.recordMarbleInput(move.d.x, move.d.y); } } + if (!this.controllable && this.connection != null) { + move = new Move(); + move.d = new Vector(0, 0); + } physicsAccumulator += timeState.dt; diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index a9a3fc55..1632aee3 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -323,14 +323,14 @@ class MarbleGame { Settings.save(); } - public function playMission(mission:Mission) { + public function playMission(mission:Mission, multiplayer:Bool = false) { canvas.clearContent(); destroyPreviewWorld(); 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 3ee07209..7e75f0d9 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,9 @@ package src; +import net.NetCommands; +import net.Net; +import net.Net.ClientConnection; +import rewind.InputRecorder; import gui.AchievementsGui; import src.Radar; import gui.LevelSelectGui; @@ -105,7 +109,6 @@ class MarbleWorld extends Scheduler { public var interiors:Array = []; public var pathedInteriors:Array = []; - public var marbles:Array = []; public var dtsObjects:Array = []; public var forceObjects:Array = []; public var triggers:Array = []; @@ -183,6 +186,18 @@ class MarbleWorld extends Scheduler { public var rewindManager:RewindManager; public var rewinding:Bool = false; + public var inputRecorder:InputRecorder; + public var isReplayingMovement:Bool = false; + public var currentInputMoves:Array; + + // Multiplayer + public var isMultiplayer:Bool; + + public var startRealTime:Float = 0; + public var multiplayerStarted:Bool = false; + + var clientMarbles:Map = []; + // Loading var resourceLoadFuncs:Array<(() -> Void)->Void> = []; @@ -207,7 +222,7 @@ class MarbleWorld extends Scheduler { 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 +231,17 @@ class MarbleWorld extends Scheduler { this.replay = new Replay(mission.path, mission.isClaMission ? mission.id : 0); this.isRecording = record; this.rewindManager = new RewindManager(this); + this.inputRecorder = new InputRecorder(this); + this.isMultiplayer = multiplayer; + + // Set the network RNG for hunt + if (isMultiplayer && gameMode is modes.HuntMode && Net.isHost) { + var hunt:modes.HuntMode = cast gameMode; + var rng = Math.random() * 10000; + NetCommands.setNetworkRNG(rng); + @:privateAccess hunt.rng.setSeed(cast rng); + @:privateAccess hunt.rng2.setSeed(cast rng); + } } public function init() { @@ -266,7 +292,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.clients) { + this.resourceLoadFuncs.push(fwd -> this.initMarble(client, fwd)); // Others + } + } + this.resourceLoadFuncs.push(fwd -> this.initMarble(null, fwd)); // Self this.resourceLoadFuncs.push(fwd -> { this.addSimGroup(this.mission.root); this._loadingLength = resourceLoadFuncs.length; @@ -371,7 +402,7 @@ class MarbleWorld extends Scheduler { worker.run(); } - public function initMarble(onFinish:Void->Void) { + public function initMarble(client:ClientConnection, onFinish:Void->Void) { Console.log("Initializing marble"); var worker = new ResourceLoaderWorker(onFinish); var marblefiles = [ @@ -422,8 +453,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(); } @@ -435,6 +467,8 @@ class MarbleWorld extends Scheduler { interior.onLevelStart(); for (shape in this.dtsObjects) shape.onLevelStart(); + if (this.isMultiplayer && Net.isClient) + NetCommands.clientIsReady(Net.clientId); } public function restart(full:Bool = false) { @@ -526,6 +560,15 @@ class MarbleWorld extends Scheduler { this.marble.setMode(Start); sky.follow = marble.camera; + if (isMultiplayer) { + for (client => marble in clientMarbles) { + var marbleStartQuat = this.gameMode.getSpawnTransform(); + marble.setMarblePosition(marbleStartQuat.position.x, marbleStartQuat.position.y, marbleStartQuat.position.z); + marble.reset(); + marble.setMode(Start); + } + } + var missionInfo:MissionElementScriptObject = cast this.mission.root.elements.filter((element) -> element._type == MissionElementType.ScriptObject && element._name == "MissionInfo")[0]; if (missionInfo.starthelptext != null) @@ -585,17 +628,32 @@ class MarbleWorld extends Scheduler { AudioManager.playSound(ResourceLoader.getResource('data/sound/spawn_alternate.wav', ResourceLoader.getAudio, this.soundResources)); } + public function allClientsReady() { + NetCommands.setStartTime(3); // Start after 3 seconds + } + public function updateGameState() { if (this.outOfBounds) return; // We will update state manually - if (this.timeState.currentAttemptTime < 0.5) { - this.marble.setMode(Start); - } - if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 3.5)) { - this.marble.setMode(Start); - } - if (this.timeState.currentAttemptTime + skipStartBugPauseTime >= 3.5 && this.finishTime == null) { - this.marble.setMode(Play); + if (!this.isMultiplayer) { + if (this.timeState.currentAttemptTime < 0.5) { + this.marble.setMode(Start); + } + if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 3.5)) { + this.marble.setMode(Start); + } + if (this.timeState.currentAttemptTime + skipStartBugPauseTime >= 3.5 && this.finishTime == null) { + this.marble.setMode(Play); + } + } else { + if (!this.multiplayerStarted) { + if (this.startRealTime != 0 && this.timeState.timeSinceLoad > this.startRealTime) { + this.multiplayerStarted = true; + this.marble.setMode(Play); + for (client => marble in this.clientMarbles) + marble.setMode(Play); + } + } } } @@ -896,26 +954,46 @@ class MarbleWorld extends Scheduler { }); } - public function addMarble(marble:Marble, onFinish:Void->Void) { - this.marbles.push(marble); + public function addMarble(marble:Marble, client:ClientConnection, onFinish:Void->Void) { marble.level = cast this; if (marble.controllable) { - marble.init(cast this, () -> { + marble.init(cast this, client, () -> { this.scene.addChild(marble.camera); this.marble = marble; // Ugly hack // 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, () -> { + 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(); + }); } } + public function addGhostMarble(onFinish:Marble->Void) { + var marb = new Marble(); + marb.controllable = false; + marb.init(null, null, () -> { + marb.collisionWorld = this.collisionWorld; + this.collisionWorld.addMovingEntity(marb.collider); + this.collisionWorld.addMarbleEntity(marb.collider); + this.scene.addChild(marb); + onFinish(marb); + }); + return marb; + } + public function performRestart() { this.respawnPressedTime = timeState.timeSinceLoad; this.restart(); @@ -934,11 +1012,53 @@ class MarbleWorld extends Scheduler { } } + public function rollback(t:Float) { + var newT = timeState.currentAttemptTime - t; + var rewindFrame = rewindManager.getNextRewindFrame(timeState.currentAttemptTime - t); + rewindManager.applyFrame(rewindFrame); + this.isReplayingMovement = true; + this.currentInputMoves = this.inputRecorder.getMovesFrom(timeState.currentAttemptTime); + } + + public function advanceWorld(dt:Float) { + ProfilerUI.measure("updateTimer"); + this.updateTimer(dt); + this.tickSchedule(timeState.currentAttemptTime); + + if (Key.isDown(Settings.controlsSettings.blast) + || (MarbleGame.instance.touchInput.blastbutton.pressed) + || Gamepad.isDown(Settings.gamepadSettings.blast) + && !this.isWatching + && this.game == "ultra") { + this.marble.useBlast(); + } + + this.updateGameState(); + this.updateBlast(timeState); + ProfilerUI.measure("updateDTS"); + for (obj in dtsObjects) { + obj.update(timeState); + } + for (obj in triggers) { + obj.update(timeState); + } + + ProfilerUI.measure("updateMarbles"); + marble.update(timeState, collisionWorld, this.pathedInteriors); + for (client => marble in clientMarbles) { + marble.update(timeState, collisionWorld, this.pathedInteriors); + } + } + public function update(dt:Float) { if (!_ready) { return; } + if (Key.isPressed(Key.T)) { + rollback(0.4); + } + var realDt = dt; if ((Key.isDown(Settings.controlsSettings.rewind) @@ -995,9 +1115,34 @@ class MarbleWorld extends Scheduler { rewindManager.applyFrame(rframe); } } + if (dt < 0) return; + if (this.isReplayingMovement) { + while (this.currentInputMoves.length > 1) { + while (this.currentInputMoves[1].time <= timeState.currentAttemptTime) { + this.currentInputMoves = this.currentInputMoves.slice(1); + if (this.currentInputMoves.length == 1) + break; + } + if (this.currentInputMoves.length > 1) { + dt = this.currentInputMoves[1].time - this.currentInputMoves[0].time; + } + + if (this.isReplayingMovement) { + if (this.timeState.currentAttemptTime != this.currentInputMoves[0].time) + trace("fucked"); + } + + if (this.currentInputMoves.length > 1) { + advanceWorld(dt); + // trace('Position: ${@:privateAccess marble.newPos.sub(currentInputMoves[1].pos).length()}. Vel: ${marble.velocity.sub(currentInputMoves[1].velocity).length()}'); + } + } + this.isReplayingMovement = false; + } + ProfilerUI.measure("updateTimer"); this.updateTimer(dt); @@ -1056,8 +1201,14 @@ class MarbleWorld extends Scheduler { for (obj in triggers) { obj.update(timeState); } + + if (!isReplayingMovement) { + inputRecorder.recordInput(timeState.currentAttemptTime); + } + ProfilerUI.measure("updateMarbles"); - for (marble in marbles) { + marble.update(timeState, collisionWorld, this.pathedInteriors); + for (client => marble in clientMarbles) { marble.update(timeState, collisionWorld, this.pathedInteriors); } _cubemapNeedsUpdate = true; @@ -1095,6 +1246,10 @@ class MarbleWorld extends Scheduler { if (!this.rewinding && Settings.optionsSettings.rewindEnabled) this.rewindManager.recordFrame(); + if (!this.isReplayingMovement) { + inputRecorder.recordMarble(); + } + this.updateTexts(); } @@ -1176,7 +1331,8 @@ class MarbleWorld extends Scheduler { if (this.timeState.gameplayClock < 0) this.gameMode.onTimeExpire(); } - this.timeState.currentAttemptTime += dt; + if (!this.isMultiplayer || this.multiplayerStarted) + this.timeState.currentAttemptTime += dt; } else { this.timeState.currentAttemptTime = this.replay.currentPlaybackFrame.time; this.timeState.gameplayClock = this.replay.currentPlaybackFrame.clockTime; @@ -1833,10 +1989,10 @@ class MarbleWorld extends Scheduler { pathedInteriors.dispose(); } pathedInteriors = null; - for (marble in this.marbles) { + for (client => marble in clientMarbles) { marble.dispose(); } - marbles = null; + clientMarbles = null; for (dtsObject in this.dtsObjects) { dtsObject.dispose(); } diff --git a/src/PreviewWorld.hx b/src/PreviewWorld.hx index d563d77d..9e37d326 100644 --- a/src/PreviewWorld.hx +++ b/src/PreviewWorld.hx @@ -582,7 +582,7 @@ class PreviewWorld extends Scheduler { public function spawnMarble(onFinish:Marble->Void) { var marb = new Marble(); marb.controllable = false; - marb.init(null, () -> { + marb.init(null, null, () -> { marb.collisionWorld = this.collisionWorld; this.collisionWorld.addMovingEntity(marb.collider); this.scene.addChild(marb); diff --git a/src/collision/CollisionWorld.hx b/src/collision/CollisionWorld.hx index 28988fc0..c4fb0992 100644 --- a/src/collision/CollisionWorld.hx +++ b/src/collision/CollisionWorld.hx @@ -13,6 +13,8 @@ class CollisionWorld { public var dynamicEntities:Array = []; public var dynamicOctree:Octree; + var marbleEntities:Array = []; + var dynamicEntitySet:Map = []; public function new() { @@ -55,6 +57,13 @@ class CollisionWorld { contacts = contacts.concat(obj.sphereIntersection(spherecollision, timeState)); } } + + for (marb in marbleEntities) { + if (marb != spherecollision) { + if (spherecollision.go.isCollideable) + contacts = contacts.concat(marb.sphereIntersection(spherecollision, timeState)); + } + } return {foundEntities: foundEntities, contacts: contacts}; } @@ -114,6 +123,14 @@ class CollisionWorld { // [entity.boundingBox.xSize, entity.boundingBox.ySize, entity.boundingBox.zSize], entity); } + public function addMarbleEntity(entity:SphereCollisionEntity) { + this.marbleEntities.push(entity); + } + + public function removeMarbleEntity(entity:SphereCollisionEntity) { + this.marbleEntities.remove(entity); + } + public function addMovingEntity(entity:CollisionEntity) { this.dynamicEntities.push(entity); this.dynamicOctree.insert(entity); diff --git a/src/collision/SphereCollisionEntity.hx b/src/collision/SphereCollisionEntity.hx index 5bb97230..4909a42f 100644 --- a/src/collision/SphereCollisionEntity.hx +++ b/src/collision/SphereCollisionEntity.hx @@ -98,17 +98,17 @@ class SphereCollisionEntity extends CollisionEntity { contact.penetration = radius - (position.sub(contact.point).dot(contact.normal)); contacts.push(contact); - // var othercontact = new CollisionInfo(); - // othercontact.collider = collisionEntity; - // othercontact.friction = 1; - // othercontact.restitution = 1; - // othercontact.velocity = this.velocity; - // othercontact.point = thispos.add(position).multiply(0.5); - // othercontact.normal = contact.point.sub(position).normalized(); - // othercontact.contactDistance = contact.point.distance(position); - // othercontact.force = 0; - // othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal)); - // this.marble.queueCollision(othercontact); + var othercontact = new CollisionInfo(); + othercontact.collider = collisionEntity; + othercontact.friction = 1; + othercontact.restitution = 1; + othercontact.velocity = this.velocity; + othercontact.point = thispos.add(position).multiply(0.5); + othercontact.normal = contact.point.sub(position).normalized(); + othercontact.contactDistance = contact.point.distance(position); + othercontact.force = 0; + othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal)); + this.marble.queueCollision(othercontact); } return contacts; } diff --git a/src/gui/MainMenuGui.hx b/src/gui/MainMenuGui.hx index 41e594aa..d6e804a8 100644 --- a/src/gui/MainMenuGui.hx +++ b/src/gui/MainMenuGui.hx @@ -71,6 +71,9 @@ class MainMenuGui extends GuiImage { btnList.addButton(0, "Single Player Game", (sender) -> { cast(this.parent, Canvas).setContent(new DifficultySelectGui()); }); + btnList.addButton(0, "Multiplayer Game", (sender) -> { + cast(this.parent, Canvas).setContent(new MultiplayerGui()); + }); // btnList.addButton(2, "Leaderboards", (e) -> {}, 20); btnList.addButton(2, "Achievements", (e) -> { cast(this.parent, Canvas).setContent(new AchievementsGui()); diff --git a/src/gui/MultiplayerGui.hx b/src/gui/MultiplayerGui.hx new file mode 100644 index 00000000..27b5db3a --- /dev/null +++ b/src/gui/MultiplayerGui.hx @@ -0,0 +1,106 @@ +package gui; + +import net.Net; +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; +import src.Util; + +class MultiplayerGui extends GuiImage { + var innerCtrl:GuiControl; + var btnList:GuiXboxList; + + public function new() { + var res = ResourceLoader.getImage("data/ui/xbox/BG_fadeOutSoftEdge.png").resource.toTile(); + super(res); + var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt"); + var domcasual32b = new BitmapFont(domcasual32fontdata.entry); + @:privateAccess domcasual32b.loader = ResourceLoader.loader; + var domcasual32 = domcasual32b.toSdfFont(cast 42 * Settings.uiScale, MultiChannel); + + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + #if hl + var scene2d = hxd.Window.getInstance(); + #end + #if js + var scene2d = MarbleGame.instance.scene2d; + #end + + var offsetX = (scene2d.width - 1280) / 2; + var offsetY = (scene2d.height - 720) / 2; + + var subX = 640 - (scene2d.width - offsetX) * 640 / scene2d.width; + var subY = 480 - (scene2d.height - offsetY) * 480 / scene2d.height; + + innerCtrl = new GuiControl(); + innerCtrl.position = new Vector(offsetX, offsetY); + innerCtrl.extent = new Vector(640 - subX, 480 - subY); + innerCtrl.horizSizing = Width; + innerCtrl.vertSizing = Height; + this.addChild(innerCtrl); + + var coliseumfontdata = ResourceLoader.getFileEntry("data/font/ColiseumRR.fnt"); + var coliseumb = new BitmapFont(coliseumfontdata.entry); + @:privateAccess coliseumb.loader = ResourceLoader.loader; + var coliseum = coliseumb.toSdfFont(cast 44 * Settings.uiScale, MultiChannel); + + var rootTitle = new GuiText(coliseum); + rootTitle.position = new Vector(100, 30); + rootTitle.extent = new Vector(1120, 80); + rootTitle.text.textColor = 0xFFFFFF; + rootTitle.text.text = "MULTIPLAYER"; + rootTitle.text.alpha = 0.5; + innerCtrl.addChild(rootTitle); + + var btnList = new GuiXboxList(); + btnList.position = new Vector(70 - offsetX, 165); + btnList.horizSizing = Left; + btnList.extent = new Vector(502, 500); + innerCtrl.addChild(btnList); + + btnList.addButton(3, 'Create Game', (e) -> { + MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(true)); + Net.hostServer(); + }); + + btnList.addButton(3, 'Join Game', (e) -> { + Net.joinServer(() -> { + MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(false)); + }); + }); + + var bottomBar = new GuiControl(); + bottomBar.position = new Vector(0, 590); + bottomBar.extent = new Vector(640, 200); + bottomBar.horizSizing = Width; + bottomBar.vertSizing = Bottom; + innerCtrl.addChild(bottomBar); + + var backButton = new GuiXboxButton("Back", 160); + backButton.position = new Vector(400, 0); + backButton.vertSizing = Bottom; + backButton.horizSizing = Right; + backButton.gamepadAccelerator = ["B"]; + backButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE]; + backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new MainMenuGui()); + bottomBar.addChild(backButton); + } + + override function onResize(width:Int, height:Int) { + var offsetX = (width - 1280) / 2; + var offsetY = (height - 720) / 2; + + var subX = 640 - (width - offsetX) * 640 / width; + var subY = 480 - (height - offsetY) * 480 / height; + innerCtrl.position = new Vector(offsetX, offsetY); + innerCtrl.extent = new Vector(640 - subX, 480 - subY); + + super.onResize(width, height); + } +} diff --git a/src/gui/MultiplayerLevelSelectGui.hx b/src/gui/MultiplayerLevelSelectGui.hx new file mode 100644 index 00000000..6dec7aef --- /dev/null +++ b/src/gui/MultiplayerLevelSelectGui.hx @@ -0,0 +1,298 @@ +package gui; + +import net.NetCommands; +import modes.GameMode.ScoreType; +import src.Util; +import haxe.io.Path; +import h2d.filter.DropShadow; +import src.MarbleGame; +import gui.GuiControl.MouseState; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; +import src.MissionList; + +class MultiplayerLevelSelectGui extends GuiImage { + static var currentSelectionStatic:Int = 0; + + static var setLevelFn:Int->Void; + static var playSelectedLevel:Void->Void; + + var innerCtrl:GuiControl; + + public function new(isHost:Bool) { + var res = ResourceLoader.getImage("data/ui/game/CloudBG.jpg").resource.toTile(); + super(res); + + var arial14fontdata = ResourceLoader.getFileEntry("data/font/Arial Bold.fnt"); + var arial14b = new BitmapFont(arial14fontdata.entry); + @:privateAccess arial14b.loader = ResourceLoader.loader; + var arial14 = arial14b.toSdfFont(cast 21 * Settings.uiScale, h2d.Font.SDFChannel.MultiChannel); + function mlFontLoader(text:String) { + return arial14; + } + + MarbleGame.instance.toRecord = false; + + var fadeEdge = new GuiImage(ResourceLoader.getResource("data/ui/xbox/BG_fadeOutSoftEdge.png", ResourceLoader.getImage, this.imageResources).toTile()); + fadeEdge.position = new Vector(0, 0); + fadeEdge.extent = new Vector(640, 480); + fadeEdge.vertSizing = Height; + fadeEdge.horizSizing = Width; + this.addChild(fadeEdge); + + var loadAnim = new GuiLoadAnim(); + loadAnim.position = new Vector(610, 253); + loadAnim.extent = new Vector(63, 63); + loadAnim.horizSizing = Center; + loadAnim.vertSizing = Bottom; + this.addChild(loadAnim); + + var loadTextBg = new GuiText(arial14); + loadTextBg.position = new Vector(608, 335); + loadTextBg.extent = new Vector(63, 40); + loadTextBg.horizSizing = Center; + loadTextBg.vertSizing = Bottom; + loadTextBg.justify = Center; + loadTextBg.text.text = "Loading"; + loadTextBg.text.textColor = 0; + this.addChild(loadTextBg); + + var loadText = new GuiText(arial14); + loadText.position = new Vector(610, 334); + loadText.extent = new Vector(63, 40); + loadText.horizSizing = Center; + loadText.vertSizing = Bottom; + loadText.justify = Center; + loadText.text.text = "Loading"; + this.addChild(loadText); + + var difficultyMissions = MissionList.missionList['ultra']["multiplayer"]; + if (currentSelectionStatic >= difficultyMissions.length) + currentSelectionStatic = 0; + var curMission = difficultyMissions[currentSelectionStatic]; + + var lock = true; + var currentToken = 0; + var requestToken = 0; + + // var misFile = Path.withoutExtension(Path.withoutDirectory(curMission.path)); + // MarbleGame.instance.setPreviewMission(misFile, () -> { + // lock = false; + // if (currentToken != requestToken) + // return; + // this.bmp.visible = false; + // loadAnim.anim.visible = false; + // loadText.text.visible = false; + // loadTextBg.text.visible = false; + // }); + + var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt"); + var domcasual32b = new BitmapFont(domcasual32fontdata.entry); + @:privateAccess domcasual32b.loader = ResourceLoader.loader; + var domcasual32 = domcasual32b.toSdfFont(cast 42 * Settings.uiScale, MultiChannel); + + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + #if hl + var scene2d = hxd.Window.getInstance(); + #end + #if js + var scene2d = MarbleGame.instance.scene2d; + #end + + var offsetX = (scene2d.width - 1280) / 2; + var offsetY = (scene2d.height - 720) / 2; + + var subX = 640 - (scene2d.width - offsetX) * 640 / scene2d.width; + var subY = 480 - (scene2d.height - offsetY) * 480 / scene2d.height; + + innerCtrl = new GuiControl(); + + innerCtrl.position = new Vector(offsetX, offsetY); + innerCtrl.extent = new Vector(640 - subX, 480 - subY); + innerCtrl.horizSizing = Width; + innerCtrl.vertSizing = Height; + this.addChild(innerCtrl); + var coliseumfontdata = ResourceLoader.getFileEntry("data/font/ColiseumRR.fnt"); + var coliseumb = new BitmapFont(coliseumfontdata.entry); + @:privateAccess coliseumb.loader = ResourceLoader.loader; + var coliseum = coliseumb.toSdfFont(cast 44 * Settings.uiScale, MultiChannel); + + var rootTitle = new GuiText(coliseum); + + rootTitle.position = new Vector(100, 30); + rootTitle.extent = new Vector(1120, 80); + rootTitle.text.textColor = 0xFFFFFF; + rootTitle.text.text = "SELECT LEVEL"; + rootTitle.text.alpha = 0.5; + innerCtrl.addChild(rootTitle); + var bottomBar = new GuiControl(); + + bottomBar.position = new Vector(0, 590); + bottomBar.extent = new Vector(640, 200); + bottomBar.horizSizing = Width; + bottomBar.vertSizing = Bottom; + innerCtrl.addChild(bottomBar); + + var backButton = new GuiXboxButton("Back", 160); + backButton.position = new Vector(400, 0); + backButton.vertSizing = Bottom; + backButton.horizSizing = Right; + backButton.gamepadAccelerator = ["B"]; + backButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE]; + backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new DifficultySelectGui()); + bottomBar.addChild(backButton); + + // var lbButton = new GuiXboxButton("Leaderboard", 220); + // lbButton.position = new Vector(750, 0); + // lbButton.vertSizing = Bottom; + // lbButton.horizSizing = Right; + // bottomBar.addChild(lbButton); + + if (isHost) { + var nextButton = new GuiXboxButton("Play", 160); + nextButton.position = new Vector(960, 0); + nextButton.vertSizing = Bottom; + nextButton.horizSizing = Right; + nextButton.gamepadAccelerator = ["A"]; + nextButton.accelerators = [hxd.Key.ENTER]; + nextButton.pressedAction = (e) -> { + NetCommands.playLevel(); + }; + bottomBar.addChild(nextButton); + } + playSelectedLevel = () -> { + MarbleGame.instance.playMission(curMission, true); + } + + var levelWnd = new GuiImage(ResourceLoader.getResource("data/ui/xbox/levelPreviewWindow.png", ResourceLoader.getImage, this.imageResources).toTile()); + levelWnd.position = new Vector(555, 469); + levelWnd.extent = new Vector(535, 137); + levelWnd.vertSizing = Bottom; + levelWnd.horizSizing = Right; + innerCtrl.addChild(levelWnd); + + var statIcon = new GuiImage(ResourceLoader.getResource("data/ui/xbox/statIcon.png", ResourceLoader.getImage, this.imageResources).toTile()); + statIcon.position = new Vector(29, 54); + statIcon.extent = new Vector(20, 20); + levelWnd.addChild(statIcon); + + var eggIcon = new GuiImage(ResourceLoader.getResource("data/ui/xbox/eggIcon.png", ResourceLoader.getImage, this.imageResources).toTile()); + eggIcon.position = new Vector(29, 79); + eggIcon.extent = new Vector(20, 20); + levelWnd.addChild(eggIcon); + + var c0 = 0xEBEBEB; + var c1 = 0x8DFF8D; + var c2 = 0x88BCEE; + var c3 = 0xFF7575; + + var levelInfoLeft = new GuiMLText(arial14, mlFontLoader); + levelInfoLeft.position = new Vector(69, 54); + levelInfoLeft.extent = new Vector(180, 100); + levelInfoLeft.text.text = '

My Best Time:
Par Time:

'; + levelInfoLeft.text.lineSpacing = 6; + levelWnd.addChild(levelInfoLeft); + + var levelInfoMid = new GuiMLText(arial14, mlFontLoader); + levelInfoMid.position = new Vector(269, 54); + levelInfoMid.extent = new Vector(180, 100); + levelInfoMid.text.text = '

None
99:59:99

'; + levelInfoMid.text.lineSpacing = 6; + levelWnd.addChild(levelInfoMid); + + var levelInfoRight = new GuiMLText(arial14, mlFontLoader); + levelInfoRight.position = new Vector(379, 54); + levelInfoRight.extent = new Vector(180, 100); + levelInfoRight.text.text = '

Level 1
Difficulty 1

'; + levelInfoRight.text.lineSpacing = 6; + levelWnd.addChild(levelInfoRight); + + var levelNames = difficultyMissions.map(x -> x.title); + var levelSelectOpts = new GuiXboxOptionsList(6, "Level", levelNames); + + function setLevel(idx:Int) { + // if (lock) + // return false; + levelSelectOpts.currentOption = idx; + this.bmp.visible = true; + loadAnim.anim.visible = true; + loadText.text.visible = true; + loadTextBg.text.visible = true; + lock = true; + curMission = difficultyMissions[idx]; + currentSelectionStatic = idx; + currentToken++; + var misFile = Path.withoutExtension(Path.withoutDirectory(curMission.path)); + var mis = difficultyMissions[idx]; + var requestToken = currentToken; + if (Settings.easterEggs.exists(mis.path)) + eggIcon.bmp.visible = true; + else + eggIcon.bmp.visible = false; + MarbleGame.instance.setPreviewMission(misFile, () -> { + lock = false; + if (requestToken != currentToken) + return; + this.bmp.visible = false; + loadAnim.anim.visible = false; + loadText.text.visible = false; + loadTextBg.text.visible = false; + }); + + var scoreType = mis.missionInfo.gamemode != null + && mis.missionInfo.gamemode.toLowerCase() == 'scrum' ? ScoreType.Score : ScoreType.Time; + + var myScore = Settings.getScores(mis.path); + var scoreDisp = "None"; + if (myScore.length != 0) + scoreDisp = scoreType == Time ? Util.formatTime(myScore[0].time) : Util.formatScore(myScore[0].time); + var isPar = myScore.length != 0 && myScore[0].time < mis.qualifyTime; + var scoreColor = "#EBEBEB"; + if (isPar) + scoreColor = "#8DFF8D"; + if (scoreType == Score && myScore.length == 0) + scoreColor = "#EBEBEB"; + if (scoreType == Time) { + levelInfoLeft.text.text = '

My Best Time:
Par Time:

'; + levelInfoMid.text.text = '

${scoreDisp}
${Util.formatTime(mis.qualifyTime)}

'; + } + if (scoreType == Score) { + levelInfoLeft.text.text = '

My Best Score:

'; + levelInfoMid.text.text = '

${scoreDisp}

'; + } + levelInfoRight.text.text = '

Level ${mis.missionInfo.level}
Difficulty ${mis.missionInfo.difficulty == null ? "" : mis.missionInfo.difficulty}

'; + return true; + } + setLevelFn = setLevel; + + levelSelectOpts.position = new Vector(380, 435); + levelSelectOpts.extent = new Vector(815, 94); + levelSelectOpts.vertSizing = Bottom; + levelSelectOpts.horizSizing = Right; + levelSelectOpts.alwaysActive = true; + levelSelectOpts.onChangeFunc = (i) -> { + NetCommands.setLobbyLevelIndex(i); + return true; + }; + levelSelectOpts.setCurrentOption(currentSelectionStatic); + setLevel(currentSelectionStatic); + innerCtrl.addChild(levelSelectOpts); + } + + override function onResize(width:Int, height:Int) { + var offsetX = (width - 1280) / 2; + var offsetY = (height - 720) / 2; + + var subX = 640 - (width - offsetX) * 640 / width; + var subY = 480 - (height - offsetY) * 480 / height; + innerCtrl.position = new Vector(offsetX, offsetY); + innerCtrl.extent = new Vector(640 - subX, 480 - subY); + + super.onResize(width, height); + } +} diff --git a/src/net/Net.hx b/src/net/Net.hx new file mode 100644 index 00000000..e4a52483 --- /dev/null +++ b/src/net/Net.hx @@ -0,0 +1,244 @@ +package net; + +import haxe.Json; +import datachannel.RTCPeerConnection; +import datachannel.RTCDataChannel; +import hx.ws.WebSocket; +import src.Console; +import net.NetCommands; + +enum abstract GameplayState(Int) from Int to Int { + var UNKNOWN; + var LOBBY; + var GAME; +} + +enum abstract NetPacketType(Int) from Int to Int { + var NullPacket; + var ClientIdAssign; + var NetCommand; + var Ping; + var PingBack; +} + +@:publicFields +class ClientConnection { + var id:Int; + var socket:RTCPeerConnection; + var datachannel:RTCDataChannel; + var state:GameplayState; + var rtt:Float; + var pingSendTime:Float; + var _rttRecords:Array = []; + + public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel) { + this.socket = socket; + this.datachannel = datachannel; + this.id = id; + this.state = GameplayState.LOBBY; + this.rtt = 0; + } + + public function ready() { + state = GameplayState.GAME; + } +} + +class Net { + static var client:RTCPeerConnection; + static var clientDatachannel:RTCDataChannel; + + static var masterWs:WebSocket; + + public static var isMP:Bool; + public static var isHost:Bool; + public static var isClient:Bool; + + public static var startMP:Bool; + + public static var clientId:Int; + public static var networkRNG:Float; + public static var clients:Map = []; + public static var clientIdMap:Map = []; + + public static function hostServer() { + // host = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); + // host.bind("127.0.0.1", 28000, (c) -> { + // onClientConnect(c); + // isMP = true; + // }); + isHost = true; + isClient = false; + clientId = 0; + masterWs = new WebSocket("ws://localhost:8080"); + + masterWs.onmessage = (m) -> { + switch (m) { + case StrMessage(content): + var conts = Json.parse(content); + var peer = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); + peer.setRemoteDescription(conts.sdp, conts.type); + + var candidates = []; + peer.onLocalCandidate = (c) -> { + if (c != "") + candidates.push('a=${c}'); + } + peer.onGatheringStateChange = (s) -> { + if (s == RTC_GATHERING_COMPLETE) { + var sdpObj = StringTools.trim(peer.localDescription); + sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); + masterWs.send(Json.stringify({ + type: "connect", + sdpObj: { + sdp: sdpObj, + type: "offer" + } + })); + } + } + peer.onDataChannel = (dc) -> { + onClientConnect(peer, dc); + }; + case _: {} + } + } + + isMP = true; + } + + public static function joinServer(connectedCb:() -> Void) { + masterWs = new WebSocket("ws://localhost:8080"); + + client = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0"); + var candidates = []; + + client.onLocalCandidate = (c) -> { + if (c != "") + candidates.push('a=${c}'); + } + client.onGatheringStateChange = (s) -> { + if (s == RTC_GATHERING_COMPLETE) { + var sdpObj = StringTools.trim(client.localDescription); + sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); + masterWs.send(Json.stringify({ + type: "connect", + sdpObj: { + sdp: sdpObj, + type: "offer" + } + })); + } + } + + masterWs.onmessage = (m) -> { + switch (m) { + case StrMessage(content): + var conts = Json.parse(content); + client.setRemoteDescription(conts.sdp, conts.type); + case _: {} + } + } + + clientDatachannel = client.createDatachannel("mp"); + clientDatachannel.onOpen = (n) -> { + clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0 + clientIdMap[0] = clients[client]; + onConnectedToServer(); + haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation + } + clientDatachannel.onMessage = (b) -> { + onPacketReceived(client, clientDatachannel, new haxe.io.BytesInput(b)); + } + + isMP = true; + isHost = false; + isClient = true; + } + + static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel) { + clientId += 1; + clients.set(c, new ClientConnection(clientId, c, dc)); + clientIdMap[clientId] = clients[c]; + dc.onMessage = (msgBytes) -> { + onPacketReceived(c, dc, new haxe.io.BytesInput(msgBytes)); + } + var b = haxe.io.Bytes.alloc(3); + b.set(0, ClientIdAssign); + b.setUInt16(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 + clients[c].pingSendTime = Sys.time(); + dc.sendBytes(b); + Console.log("Sending ping packet!"); + } + + 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 + clients[client].pingSendTime = Sys.time(); + clientDatachannel.sendBytes(b); + Console.log("Sending ping packet!"); + } + + static function onPacketReceived(c:RTCPeerConnection, dc:RTCDataChannel, input:haxe.io.BytesInput) { + var packetType = input.readByte(); + switch (packetType) { + case NetCommand: + NetCommands.readPacket(input); + + case ClientIdAssign: + clientId = input.readUInt16(); + Console.log('Client ID set to ${clientId}'); + + 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 conn = clients[c]; + var now = Sys.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}'); + } + + case _: + trace("unknown command: " + packetType); + } + } + + public static function sendPacketToAll(packetData:haxe.io.BytesOutput) { + var bytes = packetData.getBytes(); + for (c => v in clients) { + v.datachannel.sendBytes(packetData.getBytes()); + } + } + + public static function sendPacketToHost(packetData:haxe.io.BytesOutput) { + var bytes = packetData.getBytes(); + clientDatachannel.sendBytes(bytes); + } +} diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx new file mode 100644 index 00000000..fa679fc7 --- /dev/null +++ b/src/net/NetCommands.hx @@ -0,0 +1,54 @@ +package net; + +import net.Net.GameplayState; +import net.Net.NetPacketType; +import gui.MultiplayerLevelSelectGui; +import src.MarbleGame; + +@:build(net.RPCMacro.build()) +class NetCommands { + @:rpc(server) public static function setLobbyLevelIndex(i:Int) { + MultiplayerLevelSelectGui.setLevelFn(i); + } + + @:rpc(server) public static function playLevel() { + MultiplayerLevelSelectGui.playSelectedLevel(); + } + + @: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 clientIsReady(clientId:Int) { + if (Net.isHost) { + Net.clientIdMap[clientId].ready(); + var allReady = true; + for (id => client in Net.clientIdMap) { + if (client.state != GameplayState.GAME) + allReady = false; + } + if (allReady) { + if (MarbleGame.instance.world != null) { + MarbleGame.instance.world.allClientsReady(); + } + } + } + } + + @:rpc(server) public static function setStartTime(t:Float) { + if (MarbleGame.instance.world != null) { + if (Net.isClient) { + t -= Net.clientIdMap[0].rtt / 2; // Subtract receving time + } + MarbleGame.instance.world.startRealTime = MarbleGame.instance.world.timeState.timeSinceLoad + t; + } + } +} diff --git a/src/net/RPCMacro.hx b/src/net/RPCMacro.hx new file mode 100644 index 00000000..8bc19a42 --- /dev/null +++ b/src/net/RPCMacro.hx @@ -0,0 +1,136 @@ +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(); + + 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 _: {} + } + } + 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 haxe.io.BytesOutput(); + stream.writeByte(NetPacketType.NetCommand); + stream.writeByte($v{rpcFnId}); + $b{serializeFns}; + Net.sendPacketToAll(stream); + } + }; + + f.expr = macro $b{[f.expr, lastExpr]}; + + case EConst(CIdent("client")): + var lastExpr = macro { + if (!Net.isHost) { + var stream = new haxe.io.BytesOutput(); + 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('haxe.io.Input')) + } + ], + expr: macro { + var fnId = stream.readByte(); + + $e{ + { + expr: ESwitch(macro fnId, cases, null), + pos: Context.currentPos() + } + } + } + }) + }; + + fields.push(deserializeField); + + return fields; + } +} diff --git a/src/rewind/InputRecorder.hx b/src/rewind/InputRecorder.hx new file mode 100644 index 00000000..3078cc1c --- /dev/null +++ b/src/rewind/InputRecorder.hx @@ -0,0 +1,62 @@ +package rewind; + +import src.MarbleWorld; +import h3d.Vector; +import src.Marble.Move; + +@:publicFields +class InputRecorderFrame { + var time:Float; + var move:Move; + var marbleAxes:Array; + var pos:Vector; + var velocity:Vector; + + public function new() {} +} + +class InputRecorder { + var frames:Array; + var level:MarbleWorld; + + public function new(level:MarbleWorld) { + frames = []; + this.level = level; + } + + public function recordInput(t:Float) { + var frame = new InputRecorderFrame(); + frame.time = t; + frame.move = level.marble.recordMove(); + frames.push(frame); + } + + public function recordMarble() { + frames[frames.length - 1].pos = @:privateAccess level.marble.newPos?.clone(); + frames[frames.length - 1].velocity = level.marble.velocity.clone(); + } + + public function recordAxis(axis:Array) { + frames[frames.length - 1].marbleAxes = axis.copy(); + } + + public function getMovesFrom(t:Float) { + if (frames.length == 0) + return []; + var start = 0; + var end = frames.length - 1; + var mid = Std.int(frames.length / 2); + while (end - start > 1) { + mid = Std.int((start / 2) + (end / 2)); + if (frames[mid].time < t) { + start = mid + 1; + } else if (frames[mid].time > t) { + end = mid - 1; + } else { + start = end = mid; + } + } + + return frames.slice(start - 1); + } +}