replay support

This commit is contained in:
RandomityGuy 2022-11-27 23:42:58 +05:30
parent 0c1e121a8b
commit f820a3e41b
8 changed files with 257 additions and 93 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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<ReplayMarbleState>;
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;

View file

@ -205,7 +205,8 @@ class Util {
'\\f' => '\x0C',
'\\n' => '\n',
'\\r' => '\r',
"\\'" => "'"
"\\'" => "'",
"\\\"" => "\"",
];
for (obj => esc in specialCases) {

View file

@ -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);

View file

@ -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);

View file

@ -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);