diff --git a/compile-js.hxml b/compile-js.hxml index ea0b1daa..f188602e 100644 --- a/compile-js.hxml +++ b/compile-js.hxml @@ -1,6 +1,7 @@ -cp src -lib heaps -lib stb_ogg_sound +-lib zip --js marblegame.js -D windowSize=1280x720 -D js-es=6 diff --git a/data/ui/play/playback.png b/data/ui/play/playback.png new file mode 100644 index 00000000..2cd46f32 Binary files /dev/null and b/data/ui/play/playback.png differ diff --git a/data/ui/play/record.png b/data/ui/play/record.png new file mode 100644 index 00000000..25a69a44 Binary files /dev/null and b/data/ui/play/record.png differ diff --git a/src/Marble.hx b/src/Marble.hx index 3f1b07dc..466eba6a 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1335,11 +1335,7 @@ class Marble extends GameObject { 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 - && this.level.isRecording) { + if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching) { if (Key.isDown(Settings.controlsSettings.forward)) { move.d.x -= 1; } diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index c6d4740c..881ecd2a 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -1,5 +1,6 @@ package src; +import src.Replay; import touch.TouchInput; import src.ResourceLoader; import src.AudioManager; @@ -28,6 +29,7 @@ class MarbleGame { var scene:h3d.scene.Scene; var paused:Bool; + var toRecord:Bool = false; var exitGameDlg:ExitGameDlg; @@ -212,14 +214,26 @@ class MarbleGame { paused = false; var pmg = new PlayMissionGui(); PlayMissionGui.currentSelectionStatic = world.mission.index; + if (world.isRecording) { + world.saveReplay(); + } world.dispose(); world = null; canvas.setContent(pmg); } public function playMission(mission:Mission) { + canvas.clearContent(); + world = new MarbleWorld(scene, scene2d, mission, toRecord); + toRecord = false; + world.init(); + } + + public function watchMissionReplay(mission:Mission, replay:Replay) { canvas.clearContent(); world = new MarbleWorld(scene, scene2d, mission); + world.replay = replay; + world.isWatching = true; world.init(); } diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 5eb6f4bf..70c5a8c9 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -131,7 +131,7 @@ class MarbleWorld extends Scheduler { // Replay public var replay:Replay; public var isWatching:Bool = false; - public var isRecording:Bool = true; + public var isRecording:Bool = false; // Loading var resourceLoadFuncs:Array<(() -> Void)->Void> = []; @@ -149,11 +149,12 @@ class MarbleWorld extends Scheduler { var lock:Bool = false; - public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission) { + public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission, record:Bool = false) { this.scene = scene; this.scene2d = scene2d; this.mission = mission; this.replay = new Replay(mission.path); + this.isRecording = record; } public function init() { @@ -323,7 +324,6 @@ class MarbleWorld extends Scheduler { public function restart() { if (!this.isWatching) { this.replay.clear(); - this.isRecording = true; } else this.replay.rewind(); this.timeState.currentAttemptTime = 0; @@ -863,6 +863,7 @@ class MarbleWorld extends Scheduler { PlayMissionGui.currentSelectionStatic = mission.index + 1; MarbleGame.canvas.setContent(pmg); #if js + var pointercontainer = js.Browser.document.querySelector("#pointercontainer"); pointercontainer.hidden = false; #end return; @@ -1195,7 +1196,10 @@ class MarbleWorld extends Scheduler { var pointercontainer = js.Browser.document.querySelector("#pointercontainer"); pointercontainer.hidden = false; #end - this.isRecording = false; // Stop recording here + if (this.isRecording) { + this.isRecording = false; // Stop recording here + this.saveReplay(); + } if (Util.isTouchDevice()) { MarbleGame.instance.touchInput.setControlsEnabled(false); } @@ -1323,6 +1327,20 @@ class MarbleWorld extends Scheduler { } } + public function saveReplay() { + var replayBytes = this.replay.write(); + hxd.File.saveAs(replayBytes, { + title: 'Save Replay', + fileTypes: [ + { + name: "Replay (*.mbr)", + extensions: ["mbr"] + } + ], + defaultPath: '${this.mission.title}${this.timeState.gameplayClock}.mbr' + }); + } + public function dispose() { this.playGui.dispose(); scene.removeChildren(); diff --git a/src/MissionList.hx b/src/MissionList.hx index 4cd07134..96044f55 100644 --- a/src/MissionList.hx +++ b/src/MissionList.hx @@ -10,6 +10,8 @@ class MissionList { static var advancedMissions:Array; static var customMissions:Array; + static var missions:Map; + static var _build:Bool = false; public function new() {} @@ -17,6 +19,9 @@ class MissionList { public static function buildMissionList() { if (_build) return; + + missions = new Map(); + function parseDifficulty(difficulty:String) { #if (hl && !android) var difficultyFiles = ResourceLoader.fileSystem.dir("data/missions/" + difficulty); @@ -30,6 +35,7 @@ class MissionList { var misParser = new MisParser(file.getText()); var mInfo = misParser.parseMissionInfo(); var mission = Mission.fromMissionInfo(file.path, mInfo); + missions.set(file.path, mission); difficultyMissions.push(mission); } } diff --git a/src/Replay.hx b/src/Replay.hx index ab9d4464..97b89fa1 100644 --- a/src/Replay.hx +++ b/src/Replay.hx @@ -1,5 +1,7 @@ package src; +import haxe.io.BytesInput; +import haxe.zip.Huffman; import haxe.io.Bytes; import haxe.io.BytesBuffer; import dif.io.BytesReader; @@ -195,6 +197,8 @@ class Replay { var currentPlaybackFrameIdx:Int; var currentPlaybackTime:Float; + var version:Int = 1; + public function new(mission:String) { this.mission = mission; this.initialState = new ReplayInitialState(); @@ -311,20 +315,37 @@ class Replay { var buf = bw.getBuffer(); var bufsize = buf.length; - var compressed = haxe.zip.Compress.run(bw.getBuffer(), 7); + #if hl + var compressed = haxe.zip.Compress.run(bw.getBuffer(), 9); + #end + #if js + var stream = zip.DeflateStream.create(zip.DeflateStream.CompressionLevel.GOOD, false); + stream.write(new BytesInput(bw.getBuffer())); + var compressed = stream.finalize(); + #end var finalB = new BytesBuffer(); + finalB.addByte(version); finalB.addInt32(bufsize); - finalB.addBytes(compressed, 4, compressed.length); + finalB.addBytes(compressed, 0, compressed.length); return finalB.getBytes(); } public function read(data:Bytes) { - var uncompressedLength = data.getInt32(0); - var compressedData = data.sub(4, data.length - 4); + var replayVersion = data.get(0); + if (replayVersion > version) { + return false; + } + var uncompressedLength = data.getInt32(1); + var compressedData = data.sub(5, data.length - 5); + #if hl var uncompressed = haxe.zip.Uncompress.run(compressedData, uncompressedLength); + #end + #if js + var uncompressed = haxe.zip.InflateImpl.run(new BytesInput(compressedData), uncompressedLength); + #end var br = new BytesReader(uncompressed); this.mission = br.readStr(); this.initialState.read(br); @@ -335,5 +356,6 @@ class Replay { frame.read(br); this.frames.push(frame); } + return true; } } diff --git a/src/dif/io/BytesWriter.hx b/src/dif/io/BytesWriter.hx index f6dbd77a..ee41c80a 100644 --- a/src/dif/io/BytesWriter.hx +++ b/src/dif/io/BytesWriter.hx @@ -19,15 +19,15 @@ class BytesWriter { public function writeUInt16(int:Int) { var h = int >> 8; var l = int & 0xFF; - this.bytes.addByte(h); this.bytes.addByte(l); + this.bytes.addByte(h); } public function writeInt16(int:Int) { var h = int >> 8; var l = int & 0xFF; - this.bytes.addByte(h); this.bytes.addByte(l); + this.bytes.addByte(h); } public function writeByte(int:Int) { diff --git a/src/gui/MessageBoxOkDlg.hx b/src/gui/MessageBoxOkDlg.hx new file mode 100644 index 00000000..6689b838 --- /dev/null +++ b/src/gui/MessageBoxOkDlg.hx @@ -0,0 +1,60 @@ +package gui; + +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; + +class MessageBoxOkDlg extends GuiControl { + public function new(text:String) { + super(); + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + var domcasual24fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt"); + var domcasual24b = new BitmapFont(domcasual24fontdata.entry); + @:privateAccess domcasual24b.loader = ResourceLoader.loader; + var domcasual24 = domcasual24b.toSdfFont(cast 20 * Settings.uiScale, MultiChannel); + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + return [normal, hover, pressed]; + } + + var yesNoFrame = new GuiImage(ResourceLoader.getResource("data/ui/common/dialog.png", ResourceLoader.getImage, this.imageResources).toTile()); + yesNoFrame.horizSizing = Center; + yesNoFrame.vertSizing = Center; + yesNoFrame.position = new Vector(187, 156); + yesNoFrame.extent = new Vector(300, 161); + this.addChild(yesNoFrame); + + var yesNoText = new GuiMLText(domcasual24, null); + yesNoText.position = new Vector(33, 46); + yesNoText.horizSizing = Center; + yesNoText.extent = new Vector(198, 23); + yesNoText.text.text = text; + yesNoText.text.textColor = 0; + yesNoText.text.maxWidth = 198; + yesNoFrame.addChild(yesNoText); + + var okButton = new GuiButton(loadButtonImages("data/ui/common/ok")); + okButton.position = new Vector(117, 85); + okButton.extent = new Vector(78, 59); + okButton.vertSizing = Top; + okButton.pressedAction = (sender) -> { + MarbleGame.canvas.popDialog(this); + } + yesNoFrame.addChild(okButton); + + if (yesNoText.text.getBounds().yMax > yesNoText.extent.y) { + var diff = yesNoText.text.getBounds().yMax - yesNoText.extent.y; + yesNoFrame.extent.y += diff; + okButton.position.y += diff; + } + } +} diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index ae3e8bfb..89a47801 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -1,5 +1,6 @@ package gui; +import src.Replay; import haxe.ds.Option; import hxd.Key; import gui.GuiControl.MouseState; @@ -128,6 +129,50 @@ class PlayMissionGui extends GuiImage { var filt = new ColorMatrix(Matrix.I()); pmPreview.bmp.filter = filt; + var replayPlayButton = new GuiImage(ResourceLoader.getResource("data/ui/play/playback.png", ResourceLoader.getImage, this.imageResources).toTile()); + replayPlayButton.position = new Vector(38, 315); + replayPlayButton.extent = new Vector(18, 18); + replayPlayButton.pressedAction = (sender) -> { + hxd.File.browse((replayToLoad) -> { + replayToLoad.load((replayData) -> { + var replay = new Replay(""); + if (!replay.read(replayData)) { + cast(this.parent, Canvas).pushDialog(new MessageBoxOkDlg("Cannot load replay.")); + // Idk do something to notify the user here + } else { + var repmis = replay.mission; + #if js + repmis = StringTools.replace(repmis, "data/", ""); + #end + var playMis = MissionList.missions.get(repmis); + if (playMis != null) { + cast(this.parent, Canvas).marbleGame.watchMissionReplay(playMis, replay); + } else { + cast(this.parent, Canvas).pushDialog(new MessageBoxOkDlg("Cannot load replay.")); + } + } + }); + }, { + title: "Select replay file", + fileTypes: [ + { + name: "Replay (*.mbr)", + extensions: ["mbr"] + } + ], + }); + }; + pmBox.addChild(replayPlayButton); + + var replayRecordButton = new GuiImage(ResourceLoader.getResource("data/ui/play/record.png", ResourceLoader.getImage, this.imageResources).toTile()); + replayRecordButton.position = new Vector(56, 315); + replayRecordButton.extent = new Vector(18, 18); + replayRecordButton.pressedAction = (sender) -> { + cast(this.parent, Canvas).marbleGame.toRecord = true; + cast(this.parent, Canvas).pushDialog(new MessageBoxOkDlg("The next mission you play will be recorded.")); + }; + pmBox.addChild(replayRecordButton); + var levelWnd = new GuiImage(ResourceLoader.getResource("data/ui/play/level_window.png", ResourceLoader.getImage, this.imageResources).toTile()); levelWnd.position = new Vector(); levelWnd.extent = new Vector(258, 194);