package rewind; import rewind.RewindFrame.RewindMPState; import haxe.io.BytesInput; import haxe.io.BytesBuffer; import mis.MissionElement.MissionElementBase; import src.GameObject; import shapes.AbstractBumper; import shapes.PowerUp; import shapes.LandMine; import src.MarbleWorld; import shapes.Trapdoor; import shapes.PushButton; import src.Util; import shapes.Nuke; import src.Settings; class RewindManager { var frameData:BytesBuffer; var frameElapsedTimes:Array = []; var frameDataOffsets:Array = []; var frameSizes:Array = []; var allocObjMap:Map = []; var allocObjs:Array = []; var allocMeMap:Map = []; var allocMes:Array = []; var level:MarbleWorld; var allocId = 0; var allocMeId = 0; public var timeScale:Float = 1; public function new(level:MarbleWorld) { this.level = level; this.timeScale = Settings.optionsSettings.rewindTimescale; this.frameData = new BytesBuffer(); } public function recordFrame() { var rf = new RewindFrame(); rf.timeState = level.timeState.clone(); rf.marblePosition = level.marble.collider.transform.getPosition().clone(); rf.marbleOrientation = level.marble.getRotationQuat().clone(); rf.marbleVelocity = level.marble.velocity.clone(); rf.marbleAngularVelocity = level.marble.omega.clone(); rf.marblePowerup = level.marble.heldPowerup; rf.bonusTime = level.bonusTime; rf.gemCount = level.gemCount; rf.gemStates = level.gems.map(x -> x.pickedUp); rf.activePowerupStates = [@:privateAccess level.marble.superBounceEnableTime, @:privateAccess level.marble.shockAbsorberEnableTime, @:privateAccess level.marble.helicopterEnableTime, @:privateAccess level.marble.megaMarbleEnableTime ]; rf.currentUp = level.marble.currentUp.clone(); rf.lastContactNormal = level.marble.lastContactNormal.clone(); rf.mpStates = level.pathedInteriors.map(x -> { var mpstate = new RewindMPState(); mpstate.currentTime = x.currentTime; mpstate.targetTime = x.targetTime; mpstate.velocity = x.velocity.clone(); mpstate.stoppedPosition = @:privateAccess x.stopped ? @:privateAccess x.stoppedPosition.clone() : null; mpstate.position = @:privateAccess x.position.clone(); mpstate.prevPosition = @:privateAccess x.prevPosition.clone(); return mpstate; }); rf.powerupStates = []; rf.landMineStates = []; rf.trapdoorStates = []; for (dts in level.dtsObjects) { if (dts is PowerUp) { var pow:PowerUp = cast dts; rf.powerupStates.push(pow.lastPickUpTime); } if (dts is PushButton) { var pow:PushButton = cast dts; rf.powerupStates.push(pow.lastContactTime); } if (dts is LandMine) { var lm:LandMine = cast dts; rf.landMineStates.push(lm.disappearTime); } if (dts is Nuke) { var lm:Nuke = cast dts; rf.landMineStates.push(lm.disappearTime); } if (dts is Trapdoor) { var td:Trapdoor = cast dts; rf.trapdoorStates.push({ lastCompletion: td.lastCompletion, lastDirection: td.lastDirection, lastContactTime: td.lastContactTime }); } if (dts is AbstractBumper) { var ab:AbstractBumper = cast dts; rf.powerupStates.push(ab.lastContactTime); } } rf.blastAmt = level.marble.blastAmount; rf.oobState = { oob: level.marble.outOfBounds, timeState: level.marble.outOfBoundsTime != null ? level.marble.outOfBoundsTime.clone() : null }; rf.checkpointState = { currentCheckpoint: @:privateAccess level.currentCheckpoint, currentCheckpointTrigger: @:privateAccess level.currentCheckpointTrigger, checkpointBlast: @:privateAccess level.cheeckpointBlast, checkpointCollectedGems: @:privateAccess level.checkpointCollectedGems.copy(), checkpointHeldPowerup: @:privateAccess level.checkpointHeldPowerup, checkpointUp: @:privateAccess level.checkpointUp != null ? @:privateAccess level.checkpointUp.clone() : null, }; frameElapsedTimes.push(level.timeState.currentAttemptTime); frameDataOffsets.push(frameData.length); var frameDataSerialized = rf.serialize(this); frameSizes.push(frameDataSerialized.length); frameData.addBytes(frameDataSerialized, 0, frameDataSerialized.length); // frames.push(rf); } public function applyFrame(rf:RewindFrame) { level.timeState = rf.timeState.clone(); level.marble.setMarblePosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z); level.marble.setRotationQuat(rf.marbleOrientation.clone()); level.marble.velocity.set(rf.marbleVelocity.x, rf.marbleVelocity.y, rf.marbleVelocity.z); level.marble.omega.set(rf.marbleAngularVelocity.x, rf.marbleAngularVelocity.y, rf.marbleAngularVelocity.z); if (level.marble.heldPowerup == null) { if (rf.marblePowerup != null) { level.pickUpPowerUp(level.marble, rf.marblePowerup); } } else { if (rf.marblePowerup == null) { level.deselectPowerUp(level.marble); } else { level.pickUpPowerUp(level.marble, rf.marblePowerup); } } level.bonusTime = rf.bonusTime; level.gemCount = rf.gemCount; @:privateAccess level.playGui.formatGemCounter(level.gemCount, level.totalGems); for (i in 0...rf.gemStates.length) { level.gems[i].setHide(rf.gemStates[i]); } @:privateAccess level.marble.superBounceEnableTime = rf.activePowerupStates[0]; @:privateAccess level.marble.shockAbsorberEnableTime = rf.activePowerupStates[1]; @:privateAccess level.marble.helicopterEnableTime = rf.activePowerupStates[2]; @:privateAccess level.marble.megaMarbleEnableTime = rf.activePowerupStates[3]; if (level.marble.currentUp.x != rf.currentUp.x || level.marble.currentUp.y != rf.currentUp.y || level.marble.currentUp.z != rf.currentUp.z) { level.setUp(level.marble, rf.currentUp, level.timeState); // Hacky things @:privateAccess level.orientationChangeTime = level.timeState.currentAttemptTime - 0.3; var oldorient = level.newOrientationQuat; level.newOrientationQuat = @:privateAccess level.oldOrientationQuat; @:privateAccess level.oldOrientationQuat = oldorient; } var gravitycompletion = Util.clamp((level.timeState.currentAttemptTime - @:privateAccess level.orientationChangeTime) / 0.3, 0, 1); if (gravitycompletion == 0) { level.newOrientationQuat = @:privateAccess level.oldOrientationQuat; @:privateAccess level.orientationChangeTime = -1e8; } level.marble.currentUp.set(rf.currentUp.x, rf.currentUp.y, rf.currentUp.z); level.marble.lastContactNormal.set(rf.lastContactNormal.x, rf.lastContactNormal.y, rf.lastContactNormal.z); for (i in 0...rf.mpStates.length) { level.pathedInteriors[i].currentTime = rf.mpStates[i].currentTime; level.pathedInteriors[i].targetTime = rf.mpStates[i].targetTime; level.pathedInteriors[i].velocity.load(rf.mpStates[i].velocity); @:privateAccess level.pathedInteriors[i].stopped = rf.mpStates[i].stoppedPosition != null; @:privateAccess level.pathedInteriors[i].position.load(rf.mpStates[i].position); @:privateAccess level.pathedInteriors[i].prevPosition.load(rf.mpStates[i].prevPosition); @:privateAccess level.pathedInteriors[i].stoppedPosition = rf.mpStates[i].stoppedPosition; if (level.pathedInteriors[i].isCollideable) { var tform = level.pathedInteriors[i].getAbsPos().clone(); tform.setPosition(rf.mpStates[i].position); @:privateAccess level.pathedInteriors[i].collider.setTransform(tform); level.collisionWorld.updateTransform(@:privateAccess level.pathedInteriors[i].collider); } // level.pathedInteriors[i].setTransform(level.pathedInteriors[i].getTransform()); } var pstates = rf.powerupStates.copy(); var lmstates = rf.landMineStates.copy(); var tstates = rf.trapdoorStates.copy(); for (dts in level.dtsObjects) { if (dts is PowerUp) { var pow:PowerUp = cast dts; pow.lastPickUpTime = pstates.shift(); } if (dts is PushButton) { var pow:PushButton = cast dts; pow.lastContactTime = pstates.shift(); } if (dts is LandMine) { var lm:LandMine = cast dts; lm.disappearTime = lmstates.shift(); } if (dts is Nuke) { var lm:Nuke = cast dts; lm.disappearTime = lmstates.shift(); } if (dts is Trapdoor) { var td:Trapdoor = cast dts; var tdState = tstates.shift(); td.lastCompletion = tdState.lastCompletion; td.lastDirection = tdState.lastDirection; td.lastContactTime = tdState.lastContactTime; } if (dts is AbstractBumper) { var ab:AbstractBumper = cast dts; ab.lastContactTime = pstates.shift(); } } if (!rf.oobState.oob) { @:privateAccess level.cancel(level.oobSchedule); @:privateAccess level.cancel(level.marble.oobSchedule); } else { level.goOutOfBounds(level.marble); } level.marble.outOfBounds = rf.oobState.oob; level.marble.camera.oob = rf.oobState.oob; level.marble.outOfBoundsTime = rf.oobState.timeState != null ? rf.oobState.timeState.clone() : null; level.marble.blastAmount = rf.blastAmt; @:privateAccess level.checkpointUp = rf.checkpointState.checkpointUp; @:privateAccess level.checkpointCollectedGems = rf.checkpointState.checkpointCollectedGems; @:privateAccess level.cheeckpointBlast = rf.checkpointState.checkpointBlast; @:privateAccess level.checkpointHeldPowerup = rf.checkpointState.checkpointHeldPowerup; @:privateAccess level.currentCheckpoint = rf.checkpointState.currentCheckpoint; @:privateAccess level.currentCheckpointTrigger = rf.checkpointState.currentCheckpointTrigger; } public function getNextRewindFrame(absTime:Float):RewindFrame { if (frameElapsedTimes.length == 0) return null; var topFrame = frameElapsedTimes[frameElapsedTimes.length - 1]; while (topFrame > absTime) { if (frameElapsedTimes.length == 1) { return getFrameAtIndex(0); } popFrame(); if (frameElapsedTimes.length == 0) return null; topFrame = frameElapsedTimes[frameElapsedTimes.length - 1]; } return getFrameAtIndex(frameElapsedTimes.length - 1); // return topFrame; } function getFrameAtIndex(index:Int) { var offset = frameDataOffsets[index]; var size = frameSizes[index]; #if sys var frameBytes = @:privateAccess frameData.b.sub(offset, size); var bi = new BytesInput(frameBytes.toBytes(size)); #end #if js var frameBytes = @:privateAccess frameData.buffer.slice(offset, offset + size); var bi = new BytesInput(haxe.io.Bytes.ofData(frameBytes)); #end var fr = new RewindFrame(); fr.deserialize(this, bi); return fr; return null; } function popFrame() { frameElapsedTimes.pop(); var offset = frameDataOffsets[frameDataOffsets.length - 1]; @:privateAccess frameData.pos = offset; frameDataOffsets.pop(); frameSizes.pop(); } public function allocGO(go:GameObject) { if (go == null) return -1; if (allocObjMap.exists(go)) return allocObjMap.get(go); var newId = allocId++; allocObjMap.set(go, newId); allocObjs.push(go); return newId; } public function getGO(id:Int):GameObject { if (id == -1) return null; return allocObjs[id]; } public function allocME(me:MissionElementBase) { if (me == null) return -1; if (allocMeMap.exists(me)) return allocMeMap.get(me); var newId = allocMeId++; allocMeMap.set(me, newId); allocMes.push(me); return newId; } public function getME(id:Int):MissionElementBase { if (id == -1) return null; return allocMes[id]; } public function clear() { frameData = new BytesBuffer(); // clear frameDataOffsets = []; frameElapsedTimes = []; frameSizes = []; allocObjs = []; allocObjMap = []; allocMes = []; allocMeMap = []; allocId = 0; allocMeId = 0; } }