From f820a3e41b56ca584566896b890d8bb6d22a4002 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:42:58 +0530 Subject: [PATCH] replay support --- src/Marble.hx | 14 ++- src/MarbleWorld.hx | 175 +++++++++++++++++++++----------- src/Mission.hx | 4 + src/Replay.hx | 92 ++++++++++++++++- src/Util.hx | 3 +- src/gui/MainMenuGui.hx | 2 + src/gui/PlayMissionGui.hx | 57 ++++++----- src/triggers/TeleportTrigger.hx | 3 + 8 files changed, 257 insertions(+), 93 deletions(-) diff --git a/src/Marble.hx b/src/Marble.hx index 71045761..85392639 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1265,6 +1265,9 @@ class Marble extends GameObject { } } + var stoppedPaths = false; + var tempState = timeState.clone(); + var intersectData = testMove(velocity, this.getAbsPos().getPosition(), timeStep, _radius, true); // this.getIntersectionTime(timeStep, velocity); var intersectT = intersectData.t; @@ -1279,7 +1282,6 @@ class Marble extends GameObject { // this.setPosition(intersectData.position.x, intersectData.position.y, intersectData.position.z); } - var tempState = timeState.clone(); tempState.dt = timeStep; it++; @@ -1289,7 +1291,7 @@ class Marble extends GameObject { var isCentered:Bool = cmf.result; var aControl = cmf.aControl; var desiredOmega = cmf.desiredOmega; - var stoppedPaths = false; + stoppedPaths = this.velocityCancel(timeState.currentAttemptTime, timeStep, isCentered, false, stoppedPaths, pathedInteriors); var A = this.getExternalForces(timeState.currentAttemptTime, m, timeStep); var retf = this.applyContactForces(timeStep, m, isCentered, aControl, desiredOmega, A); @@ -1362,6 +1364,9 @@ class Marble extends GameObject { pTime.currentAttemptTime = piTime; this.heldPowerup.use(pTime); this.heldPowerup = null; + if (this.level.isRecording) { + this.level.replay.recordPowerupPickup(null); + } } if (this.controllable && this.prevPos != null) { @@ -1434,7 +1439,10 @@ class Marble extends GameObject { var expectedVel = this.level.replay.currentPlaybackFrame.marbleVelocity.clone(); var expectedOmega = this.level.replay.currentPlaybackFrame.marbleAngularVelocity.clone(); - this.getAbsPos().setPosition(expectedPos); + this.setPosition(expectedPos.x, expectedPos.y, expectedPos.z); + var tform = this.collider.transform; + tform.setPosition(new Vector(expectedPos.x, expectedPos.y, expectedPos.z)); + this.collider.setTransform(tform); this.velocity = expectedVel; this.setRotationQuat(this.level.replay.currentPlaybackFrame.marbleOrientation.clone()); this.omega = expectedOmega; diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 65e88485..4044e642 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -353,16 +353,16 @@ class MarbleWorld extends Scheduler { } public function restart(full:Bool = false) { - if (!this.isWatching) { - this.replay.clear(); - } else - this.replay.rewind(); - if (!full && this.currentCheckpoint != null) { this.loadCheckpointState(); return 0; // Load checkpoint } + if (!this.isWatching) { + this.replay.clear(); + } else + this.replay.rewind(); + this.timeState.currentAttemptTime = 0; this.timeState.gameplayClock = 0; this.bonusTime = 0; @@ -390,29 +390,32 @@ class MarbleWorld extends Scheduler { } // Record/Playback trapdoor and landmine states - var tidx = 0; - var lidx = 0; - for (dtss in this.dtsObjects) { - if (dtss is Trapdoor) { - var trapdoor:Trapdoor = cast dtss; - if (!this.isWatching) { - this.replay.recordTrapdoorState(trapdoor.lastContactTime - this.timeState.timeSinceLoad, trapdoor.lastDirection, trapdoor.lastCompletion); - } else { - var state = this.replay.getTrapdoorState(tidx); - trapdoor.lastContactTime = state.lastContactTime + this.timeState.timeSinceLoad; - trapdoor.lastDirection = state.lastDirection; - trapdoor.lastCompletion = state.lastCompletion; + if (full) { + var tidx = 0; + var lidx = 0; + for (dtss in this.dtsObjects) { + if (dtss is Trapdoor) { + var trapdoor:Trapdoor = cast dtss; + if (!this.isWatching) { + this.replay.recordTrapdoorState(trapdoor.lastContactTime - this.timeState.timeSinceLoad, trapdoor.lastDirection, + trapdoor.lastCompletion); + } else { + var state = this.replay.getTrapdoorState(tidx); + trapdoor.lastContactTime = state.lastContactTime + this.timeState.timeSinceLoad; + trapdoor.lastDirection = state.lastDirection; + trapdoor.lastCompletion = state.lastCompletion; + } + tidx++; } - tidx++; - } - if (dtss is LandMine) { - var landmine:LandMine = cast dtss; - if (!this.isWatching) { - this.replay.recordLandMineState(landmine.disappearTime - this.timeState.timeSinceLoad); - } else { - landmine.disappearTime = this.replay.getLandMineState(lidx) + this.timeState.timeSinceLoad; + if (dtss is LandMine) { + var landmine:LandMine = cast dtss; + if (!this.isWatching) { + this.replay.recordLandMineState(landmine.disappearTime - this.timeState.timeSinceLoad); + } else { + landmine.disappearTime = this.replay.getLandMineState(lidx) + this.timeState.timeSinceLoad; + } + lidx++; } - lidx++; } } @@ -988,20 +991,27 @@ class MarbleWorld extends Scheduler { if (Key.isPressed(Settings.controlsSettings.respawn)) { this.respawnPressedTime = timeState.timeSinceLoad; this.restart(); - Settings.playStatistics.respawns++; - if (!Settings.levelStatistics.exists(mission.path)) { - Settings.levelStatistics.set(mission.path, { - oobs: 0, - respawns: 1, - totalTime: 0, - }); - } else { - Settings.levelStatistics[mission.path].respawns++; + if (!this.isWatching) { + Settings.playStatistics.respawns++; + + if (!Settings.levelStatistics.exists(mission.path)) { + Settings.levelStatistics.set(mission.path, { + oobs: 0, + respawns: 1, + totalTime: 0, + }); + } else { + Settings.levelStatistics[mission.path].respawns++; + } + + if (this.isRecording) { + this.replay.endFrame(); + } } return; } - if (Key.isDown(Settings.controlsSettings.respawn)) { + if (Key.isDown(Settings.controlsSettings.respawn) && !this.isWatching) { if (timeState.timeSinceLoad - this.respawnPressedTime > 1.5) { this.restart(true); this.respawnPressedTime = Math.POSITIVE_INFINITY; @@ -1010,6 +1020,17 @@ class MarbleWorld extends Scheduler { } this.tickSchedule(timeState.currentAttemptTime); + + // Replay gravity + if (this.isWatching) { + if (this.replay.currentPlaybackFrame.gravityChange) { + this.setUp(this.replay.currentPlaybackFrame.gravity, timeState, this.replay.currentPlaybackFrame.gravityInstant); + } + if (this.replay.currentPlaybackFrame.powerupPickup != null) { + this.pickUpPowerUpReplay(this.replay.currentPlaybackFrame.powerupPickup); + } + } + this.updateGameState(); ProfilerUI.measure("updateDTS"); for (obj in dtsObjects) { @@ -1031,17 +1052,17 @@ class MarbleWorld extends Scheduler { ProfilerUI.measure("updateAudio"); AudioManager.update(this.scene); + if (this.outOfBounds && this.finishTime == null && Key.isDown(Settings.controlsSettings.powerup) && !this.isWatching) { + this.restart(); + return; + } + if (!this.isWatching) { if (this.isRecording) { this.replay.endFrame(); } } - if (this.outOfBounds && this.finishTime == null && Key.isDown(Settings.controlsSettings.powerup)) { - this.restart(); - return; - } - this.updateTexts(); } @@ -1412,6 +1433,18 @@ class MarbleWorld extends Scheduler { return 0; } + public function pickUpPowerUpReplay(powerupIdent:String) { + if (powerupIdent == null) + return false; + if (this.marble.heldPowerup != null) + if (this.marble.heldPowerup.identifier == powerupIdent) + return false; + + this.playGui.setPowerupImage(powerupIdent); + + return true; + } + public function pickUpPowerUp(powerUp:PowerUp) { if (powerUp == null) return false; @@ -1421,6 +1454,9 @@ class MarbleWorld extends Scheduler { this.marble.heldPowerup = powerUp; this.playGui.setPowerupImage(powerUp.identifier); MarbleGame.instance.touchInput.powerupButton.setEnabled(true); + if (this.isRecording) { + this.replay.recordPowerupPickup(powerUp); + } return true; } @@ -1479,6 +1515,10 @@ class MarbleWorld extends Scheduler { // quatChange.initMoveTo(oldUp, vec); quatChange.multiply(quatChange, currentQuat); + if (this.isRecording) { + this.replay.recordGravity(vec, instant); + } + this.newOrientationQuat = quatChange; this.oldOrientationQuat = currentQuat; this.orientationChangeTime = instant ? -1e8 : timeState.currentAttemptTime; @@ -1491,18 +1531,20 @@ class MarbleWorld extends Scheduler { this.outOfBounds = true; this.outOfBoundsTime = this.timeState.clone(); this.marble.camera.oob = true; - Settings.playStatistics.oobs++; - if (!Settings.levelStatistics.exists(mission.path)) { - Settings.levelStatistics.set(mission.path, { - oobs: 1, - respawns: 0, - totalTime: 0, - }); - } else { - Settings.levelStatistics[mission.path].oobs++; + if (!this.isWatching) { + Settings.playStatistics.oobs++; + if (!Settings.levelStatistics.exists(mission.path)) { + Settings.levelStatistics.set(mission.path, { + oobs: 1, + respawns: 0, + totalTime: 0, + }); + } else { + Settings.levelStatistics[mission.path].oobs++; + } + if (Settings.optionsSettings.oobInsults) + OOBInsultGui.OOBCheck(); } - if (Settings.optionsSettings.oobInsults) - OOBInsultGui.OOBCheck(); // sky.follow = null; // this.oobCameraPosition = camera.position.clone(); playGui.setCenterText('outofbounds'); @@ -1571,13 +1613,19 @@ class MarbleWorld extends Scheduler { this.marble.setPosition(mpos.x, mpos.y, mpos.z); marble.velocity.load(new Vector(0, 0, 0)); marble.omega.load(new Vector(0, 0, 0)); - // Set camera orienation + // Set camera orientation var euler = this.currentCheckpoint.obj.getRotationQuat().toEuler(); this.marble.camera.CameraYaw = euler.z + Math.PI / 2; this.marble.camera.CameraPitch = 0.45; this.marble.camera.nextCameraYaw = this.marble.camera.CameraYaw; this.marble.camera.nextCameraPitch = this.marble.camera.CameraPitch; this.marble.camera.oob = false; + if (this.isRecording) { + this.replay.recordCameraState(this.marble.camera.CameraYaw, this.marble.camera.CameraPitch); + this.replay.recordMarbleInput(0, 0); + this.replay.recordMarbleState(mpos, marble.velocity, marble.getRotationQuat(), marble.omega); + this.replay.recordMarbleStateFlags(false, false, true); + } var gravityField = ""; // (this.currentCheckpoint.srcElement as any) ?.gravity || this.currentCheckpointTrigger?.element.gravity; if (this.currentCheckpoint.elem.fields.exists('gravity')) { gravityField = this.currentCheckpoint.elem.fields.get('gravity')[0]; @@ -1661,15 +1709,18 @@ class MarbleWorld extends Scheduler { public function dispose() { // Gotta add the timesinceload to our stats - Settings.playStatistics.totalTime += this.timeState.timeSinceLoad; - if (!Settings.levelStatistics.exists(mission.path)) { - Settings.levelStatistics.set(mission.path, { - oobs: 0, - respawns: 0, - totalTime: this.timeState.timeSinceLoad, - }); - } else { - Settings.levelStatistics[mission.path].totalTime += this.timeState.timeSinceLoad; + if (!this.isWatching) { + Settings.playStatistics.totalTime += this.timeState.timeSinceLoad; + + if (!Settings.levelStatistics.exists(mission.path)) { + Settings.levelStatistics.set(mission.path, { + oobs: 0, + respawns: 0, + totalTime: this.timeState.timeSinceLoad, + }); + } else { + Settings.levelStatistics[mission.path].totalTime += this.timeState.timeSinceLoad; + } } this.playGui.dispose(); diff --git a/src/Mission.hx b/src/Mission.hx index 0c553581..e1003ccf 100644 --- a/src/Mission.hx +++ b/src/Mission.hx @@ -136,6 +136,7 @@ class Mission { var ret = ResourceLoader.getResource(basename + ".png", ResourceLoader.getImage, this.imageResources).toTile(); onLoaded(ret); }); + return imgFileEntry.path; } if (ResourceLoader.fileSystem.exists(basename + ".jpg")) { imgFileEntry = ResourceLoader.fileSystem.get(basename + ".jpg"); @@ -143,14 +144,17 @@ class Mission { var ret = ResourceLoader.getResource(basename + ".jpg", ResourceLoader.getImage, this.imageResources).toTile(); onLoaded(ret); }); + return imgFileEntry.path; } var img = new BitmapData(1, 1); img.setPixel(0, 0, 0); onLoaded(Tile.fromBitmap(img)); + return null; } else { var img = new BitmapData(1, 1); img.setPixel(0, 0, 0); onLoaded(Tile.fromBitmap(img)); + return null; } } diff --git a/src/Replay.hx b/src/Replay.hx index 97b89fa1..706aa8e6 100644 --- a/src/Replay.hx +++ b/src/Replay.hx @@ -1,5 +1,6 @@ package src; +import shapes.PowerUp; import haxe.io.BytesInput; import haxe.zip.Huffman; import haxe.io.Bytes; @@ -29,12 +30,17 @@ class ReplayFrame { var marbleOrientation:Quat; var marbleAngularVelocity:Vector; var marbleStateFlags:EnumFlags; + var powerupPickup:String; // Camera var cameraPitch:Float; var cameraYaw:Float; // Input var marbleX:Float; var marbleY:Float; + // Gravity + var gravity:Vector; + var gravityInstant:Bool; + var gravityChange:Bool; public function new() {} @@ -98,6 +104,22 @@ class ReplayFrame { interpFrame.marbleX = this.marbleX; interpFrame.marbleY = this.marbleY; + // Gravity + if (this.gravityChange) { + interpFrame.gravity = this.gravity.clone(); + interpFrame.gravityInstant = this.gravityInstant; + interpFrame.gravityChange = true; + } + if (next.gravityChange) { + interpFrame.gravity = next.gravity.clone(); + interpFrame.gravityInstant = next.gravityInstant; + interpFrame.gravityChange = true; + } + + if (this.powerupPickup != null) { + interpFrame.powerupPickup = this.powerupPickup; + } + return interpFrame; } @@ -123,6 +145,21 @@ class ReplayFrame { bw.writeFloat(this.cameraYaw); bw.writeFloat(this.marbleX); bw.writeFloat(this.marbleY); + if (this.gravityChange) { + bw.writeByte(1); + bw.writeFloat(this.gravity.x); + bw.writeFloat(this.gravity.y); + bw.writeFloat(this.gravity.z); + bw.writeByte(this.gravityInstant ? 1 : 0); + } else { + bw.writeByte(0); + } + if (this.powerupPickup != null) { + bw.writeByte(1); + bw.writeStr(this.powerupPickup); + } else { + bw.writeByte(0); + } } public function read(br:BytesReader) { @@ -138,6 +175,18 @@ class ReplayFrame { this.cameraYaw = br.readFloat(); this.marbleX = br.readFloat(); this.marbleY = br.readFloat(); + if (br.readByte() == 1) { + this.gravity = new Vector(br.readFloat(), br.readFloat(), br.readFloat()); + this.gravityInstant = br.readByte() == 1; + this.gravityChange = true; + } else { + this.gravityChange = false; + } + if (br.readByte() == 1) { + this.powerupPickup = br.readStr(); + } else { + this.powerupPickup = null; + } } } @@ -197,7 +246,7 @@ class Replay { var currentPlaybackFrameIdx:Int; var currentPlaybackTime:Float; - var version:Int = 1; + var version:Int = 3; public function new(mission:String) { this.mission = mission; @@ -235,6 +284,13 @@ class Replay { currentRecordFrame.marbleStateFlags.set(InstantTeleport); } + public function recordPowerupPickup(powerup:PowerUp) { + if (powerup == null) + currentRecordFrame.powerupPickup = ""; // Use powerup + else + currentRecordFrame.powerupPickup = powerup.identifier; + } + public function recordMarbleInput(x:Float, y:Float) { currentRecordFrame.marbleX = x; currentRecordFrame.marbleY = y; @@ -245,6 +301,13 @@ class Replay { currentRecordFrame.cameraYaw = yaw; } + public function recordGravity(gravity:Vector, instant:Bool) { + currentRecordFrame.gravityChange = true; + currentRecordFrame.gravity = gravity.clone(); + if (instant) + currentRecordFrame.gravityInstant = instant; + } + public function recordTrapdoorState(lastContactTime:Float, lastDirection:Int, lastCompletion:Float) { initialState.trapdoorLastContactTimes.push(lastContactTime); initialState.trapdoorLastDirections.push(lastDirection); @@ -283,15 +346,42 @@ class Replay { return false; } var nextFrame = this.frames[this.currentPlaybackFrameIdx + 1]; + var stateFlags = 0; + var nextGravityChange:Bool = false; + var nextGravityState:{ + instant:Bool, + gravity:Vector + } = null; + var powerup:String = null; while (nextFrame.time <= nextT) { this.currentPlaybackFrameIdx++; if (this.currentPlaybackFrameIdx + 1 >= this.frames.length) { return false; } var testNextFrame = this.frames[this.currentPlaybackFrameIdx + 1]; + stateFlags |= testNextFrame.marbleStateFlags.toInt(); + if (testNextFrame.gravityChange) { + nextGravityChange = true; + nextGravityState = { + instant: testNextFrame.gravityInstant, + gravity: testNextFrame.gravity.clone() + }; + } + if (testNextFrame.powerupPickup != null) { + powerup = testNextFrame.powerupPickup; + } startFrame = nextFrame; nextFrame = testNextFrame; } + nextFrame.marbleStateFlags = EnumFlags.ofInt(stateFlags); + if (nextGravityChange) { + nextFrame.gravityChange = true; + nextFrame.gravityInstant = nextGravityState.instant; + nextFrame.gravity = nextGravityState.gravity.clone(); + } + if (powerup != null) { + nextFrame.powerupPickup = powerup; + } this.currentPlaybackFrame = startFrame.interpolate(nextFrame, nextT); this.currentPlaybackTime += dt; return true; diff --git a/src/Util.hx b/src/Util.hx index 4fe9a970..821bb054 100644 --- a/src/Util.hx +++ b/src/Util.hx @@ -205,7 +205,8 @@ class Util { '\\f' => '\x0C', '\\n' => '\n', '\\r' => '\r', - "\\'" => "'" + "\\'" => "'", + "\\\"" => "\"", ]; for (obj => esc in specialCases) { diff --git a/src/gui/MainMenuGui.hx b/src/gui/MainMenuGui.hx index 296ed054..047dbbdc 100644 --- a/src/gui/MainMenuGui.hx +++ b/src/gui/MainMenuGui.hx @@ -99,6 +99,8 @@ class MainMenuGui extends GuiImage { #if js repmis = StringTools.replace(repmis, "data/", ""); #end + if (MissionList.missions == null) + MissionList.buildMissionList(); var playMis = MissionList.missions.get(repmis); if (playMis != null) { cast(this.parent, Canvas).marbleGame.watchMissionReplay(playMis, replay); diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index 13d99ca8..f942f48e 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -782,8 +782,6 @@ class PlayMissionGui extends GuiImage { index = 0; } - var selectionChanged = currentSelection != index; - currentSelection = index; currentSelectionStatic = currentSelection; @@ -899,31 +897,38 @@ class PlayMissionGui extends GuiImage { setScoreHover(scoreButtonHover); - if (selectionChanged) { - pmPreview.bmp.tile = tmpprevtile; - #if js - switch (previewTimeoutHandle) { - case None: - previewTimeoutHandle = Some(js.Browser.window.setTimeout(() -> { - currentMission.getPreviewImage(prevImg -> { - pmPreview.bmp.tile = prevImg; - }); - }, 75)); - case Some(previewTimeoutHandle_id): - js.Browser.window.clearTimeout(previewTimeoutHandle_id); - previewTimeoutHandle = Some(js.Browser.window.setTimeout(() -> { - currentMission.getPreviewImage(prevImg -> { - pmPreview.bmp.tile = prevImg; - }); - }, 75)); - } - #end - #if hl - currentMission.getPreviewImage(prevImg -> { - pmPreview.bmp.tile = prevImg; - }); // Shit be sync - #end + // pmPreview.bmp.tile = tmpprevtile; + #if js + switch (previewTimeoutHandle) { + case None: + previewTimeoutHandle = Some(js.Browser.window.setTimeout(() -> { + var prevpath = currentMission.getPreviewImage(prevImg -> { + pmPreview.bmp.tile = prevImg; + }); + if (prevpath != pmPreview.bmp.tile.getTexture().name) { + pmPreview.bmp.tile = tmpprevtile; + } + }, 75)); + case Some(previewTimeoutHandle_id): + js.Browser.window.clearTimeout(previewTimeoutHandle_id); + previewTimeoutHandle = Some(js.Browser.window.setTimeout(() -> { + var prevpath = currentMission.getPreviewImage(prevImg -> { + pmPreview.bmp.tile = prevImg; + }); + if (prevpath != pmPreview.bmp.tile.getTexture().name) { + pmPreview.bmp.tile = tmpprevtile; + } + }, 75)); } + #end + #if hl + var prevpath = currentMission.getPreviewImage(prevImg -> { + pmPreview.bmp.tile = prevImg; + }); // Shit be sync + if (prevpath != pmPreview.bmp.tile.getTexture().name) { + pmPreview.bmp.tile = tmpprevtile; + } + #end } setCategoryFunc(currentGame, currentCategoryStatic, false); diff --git a/src/triggers/TeleportTrigger.hx b/src/triggers/TeleportTrigger.hx index 47c88194..8b789d44 100644 --- a/src/triggers/TeleportTrigger.hx +++ b/src/triggers/TeleportTrigger.hx @@ -88,6 +88,9 @@ class TeleportTrigger extends Trigger { } this.level.marble.prevPos.load(position); this.level.marble.setPosition(position.x, position.y, position.z); + if (this.level.isRecording) { + this.level.replay.recordMarbleStateFlags(false, false, true); + } if (!MisParser.parseBoolean(chooseNonNull(this.element.keepvelocity, destination.element.keepvelocity))) this.level.marble.velocity.set(0, 0, 0);