Last active
April 10, 2024 01:14
-
-
Save Rio6/b65a924b01e5011b72af59f48e817c06 to your computer and use it in GitHub Desktop.
Replay script - records, save, and replay istrolid games
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Replay script - records, save, and replay istrolid games | |
| * | |
| * After loading the script, it's recommended to rejoin the server or press the | |
| * "Reset Game" button in case of local to reload the interpolator. | |
| * | |
| * Loading non-normal saves - saves from modded games, corrupted or modified | |
| * saves - might brake the game client. Just restart the game and not to load | |
| * that file. | |
| * | |
| * Replaying a game creates a view only game. To reset, press the "Reset Game" | |
| * button. | |
| * | |
| * Leaving a server while recording interrupts it. | |
| */ | |
| var replay = replay || { | |
| interpolator_process: Interpolator.prototype.process, | |
| window_body: window.body, | |
| chat_lines_push: chat.lines.push, | |
| }; | |
| replay.winX = 500; | |
| replay.winY = 80; | |
| replay.fileChooser = false; | |
| replay.frames = null; | |
| replay.chat = []; | |
| replay.playChat = true; | |
| replay.minimized = true; | |
| replay.autoStart = false; | |
| replay.autoStop = true; | |
| replay.zJson = true; | |
| replay.error = ""; | |
| replay.specName = "[spectator]" | |
| // save a blob with given name https://stackoverflow.com/a/48968694/6023997 | |
| function saveFile(blob, filename) { | |
| let url = URL.createObjectURL(blob); | |
| let a = document.createElement('a'); | |
| document.body.appendChild(a); | |
| a.href = url; | |
| a.download = filename; | |
| a.target = "_blank"; | |
| a.click(); | |
| setTimeout(() => { | |
| URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| }, 0) | |
| } | |
| Interpolator.prototype.process = function(data) { | |
| packet = data; | |
| return replay.interpolator_process.call(this, data); | |
| }; | |
| Interpolator.prototype.recordReplay = function() { | |
| this.replay = "recording"; | |
| this.replayFrames = []; | |
| this.chatReplay = []; | |
| if (intp.state !== "starting") { | |
| return network.sendPlayer(); | |
| } | |
| }; | |
| Interpolator.prototype.stopReplay = function() { | |
| control.cheatSimInterval = -12; | |
| commander.name = account.name; | |
| this.replay = "off"; | |
| }; | |
| // Called by the game and the end of a game | |
| Interpolator.prototype.uploadReplay = function() { | |
| } | |
| Interpolator.prototype.saveReplay = function() { | |
| if(this.replay === "recording") | |
| this.stopReplay(); | |
| if (this.replayFrames) { | |
| let data = []; | |
| if(replay.zJson) { | |
| let packet = { | |
| chat: this.chatReplay.slice(0, 12000), | |
| frames: [null], | |
| }; | |
| let dv = sim.zJson.dumpDv(packet); | |
| let len = this.replayFrames.length; | |
| if(len <= 4) { | |
| dv.setUint8(dv.byteLength-5, sim.zJson.ARRAY0_MARK + len); | |
| data.push(new DataView(dv.buffer, dv.byteOffset, dv.byteLength-4)); | |
| } else if(len <= 256) { | |
| dv.setUint8(dv.byteLength-5, sim.zJson.ARRAY_MARK8); | |
| dv.setUint8(dv.byteLength-4, len); | |
| data.push(new DataView(dv.buffer, dv.byteOffset, dv.byteLength-3)); | |
| } else if(len <= 256*256) { | |
| dv.setUint8(dv.byteLength-5, sim.zJson.ARRAY_MARK16); | |
| dv.setUint16(dv.byteLength-4, len); | |
| data.push(new DataView(dv.buffer, dv.byteOffset, dv.byteLength-2)); | |
| } else { | |
| replay.error = "too many frames to save as zJson"; | |
| return; | |
| } | |
| for(let i in this.replayFrames) { | |
| if(i === "last") continue; | |
| let dv = sim.zJson.dumpDv(this.replayFrames[i]); | |
| if(i == this.replayFrames.length-1) { | |
| data.push(dv); | |
| } else { | |
| data.push(new DataView(dv.buffer, dv.byteOffset, dv.byteLength-3)); | |
| } | |
| } | |
| } else { | |
| data.push(JSON.stringify({ | |
| chat: this.chatReplay, | |
| frames: this.replayFrames, | |
| })); | |
| } | |
| let blob = new Blob(data, { type: "application/octet-stream" }); | |
| let now = new Date(); | |
| let alpha = sim.players.filter(p => p.side === "alpha").map(p => p.name).join("-"); | |
| let beta = sim.players.filter(p => p.side === "beta").map(p => p.name).join("-"); | |
| let name = (alpha && beta) ? `${alpha}-vs-${beta}` : (alpha || beta || "battle"); | |
| let ext = replay.zJson ? "zjson" : "json"; | |
| saveFile(blob, `${name}-${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}.${ext}`); | |
| } | |
| }; | |
| Interpolator.prototype.playReplay = function() { | |
| if(!this.replayFrames || this.replayFrames.length === 0) return; | |
| if(!this.chatReplay) this.chatReplay = []; | |
| this.chatReplay.head = 0; | |
| if(network instanceof Connection) { | |
| network.close(); | |
| network = new Local(); | |
| if(replay.replayChat) | |
| battleMode.server = null; | |
| } | |
| this.players = []; | |
| this.things = {}; | |
| this.particles = {}; | |
| this.winningSide = null; | |
| this.step = 0; | |
| this.replay = "playing"; | |
| this.replayStep = 0; | |
| this.local = false; | |
| commander.name = replay.specName; | |
| }; | |
| // free function, lets use it | |
| Interpolator.prototype.think = function() { | |
| // sync frame data off-intp so it wont't get deleted with it | |
| if(this.replayFrames) | |
| replay.frames = this.replayFrames; | |
| else if(replay.frames) | |
| this.replayFrames = replay.frames; | |
| if(this.chatReplay) | |
| replay.chat = this.chatReplay; | |
| else if(replay.chat) | |
| this.chatReplay = replay.chat; | |
| // pause functionality during replay | |
| if(this.replay === "playing" && sim.paused) | |
| this.replay = "paused"; | |
| else if(this.replay === "paused" && !sim.paused) | |
| this.replay = "playing"; | |
| // autostart | |
| if(replay.autoStart && this.replay === "off" && intp.state === "running") { | |
| replay.autoStart = false; | |
| this.recordReplay(); | |
| } | |
| // autostop | |
| if(replay.autoStop && this.replay === "recording" && this.state !== "running") | |
| this.stopReplay(); | |
| if(this.replay === "playing") { | |
| // stop after replay ended | |
| if(intp.replayStep >= intp.replayFrames.length) { | |
| this.stopReplay(); | |
| } | |
| // Create a spectator if it got overwritten | |
| if(!this.players.find(({name}) => name === replay.specName)) { | |
| let spec = new Player(); | |
| spec.name = replay.specName; | |
| spec.side = "spectators"; | |
| spec.number = this.players.length; | |
| this.players.push(spec); | |
| } | |
| // Replay chat | |
| if(replay.replayChat) { | |
| for(let i = this.chatReplay.head; i < this.chatReplay.length; this.chatReplay.head = ++i) { | |
| let msg = this.chatReplay[i]; | |
| if(!msg || msg.step > this.step) break; | |
| chat.lines.push({ | |
| text: msg.text, | |
| name: msg.name, | |
| color: msg.color, | |
| channel: "local", | |
| time: Date.now() | |
| }); | |
| onecup.refresh(); | |
| } | |
| } | |
| } | |
| }; | |
| chat.lines.push = function(msg) { | |
| if(intp.replay === "recording" && msg.channel === chat.channel) { | |
| intp.chatReplay.push({ | |
| text: msg.text, | |
| name: msg.name, | |
| color: msg.color, | |
| step: intp.step | |
| }); | |
| }; | |
| return replay.chat_lines_push.call(this, msg); | |
| } | |
| replay.onmousemove = function(e) { | |
| if(e.buttons) { | |
| replay.winX += e.movementX; | |
| replay.winY += e.movementY; | |
| onecup.refresh(); | |
| } | |
| } | |
| window.body = function() { | |
| replay.window_body.apply(this, arguments); | |
| if(!ui.show) return; | |
| switch(ui.mode) { | |
| case "battle": | |
| case "quickscore": | |
| case "multiplayer": | |
| case "battleroom": | |
| case "design": | |
| break; | |
| default: | |
| return; | |
| } | |
| onecup.div(".hover-black", () => { | |
| onecup.position("absolute"); | |
| onecup.top(0); | |
| onecup.right(128); | |
| onecup.width(64); | |
| onecup.height(64); | |
| if(!replay.minimized) | |
| onecup.background_color("rgba(255, 255, 255, .6)"); | |
| onecup.img({src: "http://www.istrolid.com/img/ui/rank/rank13@2x.png"}, () => { // use url to bypass onecup's src replacing | |
| onecup.position("absolute"); | |
| onecup.top(3); | |
| onecup.left(12); | |
| onecup.width(42); | |
| }); | |
| onecup.div(() => { | |
| onecup.position("absolute"); | |
| onecup.line_height(12); | |
| onecup.font_size(12); | |
| onecup.text_align("center"); | |
| onecup.width(64); | |
| onecup.top(44); | |
| onecup.color("white"); | |
| onecup.text("record"); | |
| }); | |
| onecup.onclick(() => { | |
| replay.minimized = !replay.minimized; | |
| }); | |
| }); | |
| if(replay.minimized) return; | |
| onecup.div("#replay-dialog", () => { | |
| onecup.position("absolute"); | |
| onecup.top(replay.winY); | |
| onecup.left(replay.winX); | |
| onecup.z_index("2"); | |
| onecup.min_width(150); | |
| onecup.padding(15); | |
| onecup.font_size(14); | |
| onecup.color("white"); | |
| onecup.text_align("center"); | |
| onecup.background_color("rgba(30, 30, 30, 0.6)"); | |
| onecup.border_radius(10); | |
| onecup.onmousedown(e => { | |
| if(!e.target.className.includes("replay-button")) { | |
| document.addEventListener("mousemove", replay.onmousemove); | |
| e.preventDefault(); | |
| } | |
| e.stopPropagation(); | |
| }); | |
| onecup.onmouseup(e => { | |
| document.removeEventListener("mousemove", replay.onmousemove); | |
| }); | |
| onecup.div(".hover-black-dark", () => { | |
| onecup.position("absolute"); | |
| onecup.top(4); | |
| onecup.right(4); | |
| onecup.width(18); | |
| onecup.height(18); | |
| onecup.border("2px dashed #888888"); | |
| onecup.border_radius(2); | |
| onecup.color("#888888"); | |
| onecup.onclick(() => { | |
| replay.minimized = true; | |
| }); | |
| }); | |
| onecup.text("Status: " + intp.replay); | |
| onecup.br(); | |
| onecup.text("Frames: " + (intp.replayFrames ? intp.replayFrames.length : 0)); | |
| onecup.br(); | |
| let hasReplay = () => intp.replayFrames && intp.replayFrames.length > 0; | |
| if(intp.replay === "off" && intp.state !== "running") { | |
| onecup.div(() => { | |
| onecup.margin(5); | |
| onecup.color("white"); | |
| onecup.text("Record next game "); | |
| onecup.span(".hover-white", () => onecup.text(replay.autoStart ? "☑ " : "☐")); | |
| onecup.onclick(e => { | |
| replay.autoStart = !replay.autoStart; | |
| }); | |
| }); | |
| } | |
| if(intp.replay === "off" || intp.replay === "recording") { | |
| onecup.div(() => { | |
| onecup.margin(5); | |
| onecup.color("white"); | |
| onecup.text("Stop when ended "); | |
| onecup.span(".hover-white", () => onecup.text(replay.autoStop ? "☑ " : "☐")); | |
| onecup.onclick(e => { | |
| replay.autoStop = !replay.autoStop; | |
| }); | |
| }); | |
| } | |
| if(intp.replay === "off" && hasReplay()) { | |
| onecup.div(() => { | |
| onecup.margin(5); | |
| onecup.color("white"); | |
| onecup.text("Replay chat "); | |
| onecup.span(".hover-white", () => onecup.text(replay.replayChat ? "☑ " : "☐")); | |
| onecup.onclick(e => { | |
| replay.replayChat = !replay.replayChat; | |
| }); | |
| }); | |
| } | |
| onecup.div(() => { | |
| onecup.text_align("center"); | |
| onecup.margin(10); | |
| onecup.line_height(30); | |
| let button = (text, onclick) => { | |
| onecup.span(".hover-white replay-button", () => { | |
| onecup.margin(3); | |
| onecup.padding(3); | |
| onecup.border("1px solid white"); | |
| onecup.border_radius(4); | |
| onecup.text(text); | |
| if(onclick) { | |
| onecup.onclick(() => { | |
| replay.error = ""; | |
| onclick(); | |
| }); | |
| } | |
| }); | |
| }; | |
| switch(intp.replay) { | |
| case "off": | |
| if(hasReplay()) { | |
| onecup.div(() => { | |
| onecup.margin(5); | |
| onecup.color("white"); | |
| onecup.text("Save as zJson "); | |
| onecup.span(".hover-white", () => onecup.text(replay.zJson ? "☑ " : "☐")); | |
| onecup.onclick(e => { | |
| replay.zJson = !replay.zJson; | |
| }); | |
| }); | |
| button("Record (Overwrites)", () => intp.recordReplay()); | |
| onecup.br(); | |
| button("Play", () => intp.playReplay()); | |
| button("Save", () => intp.saveReplay()); | |
| button("Clear", () => intp.replayFrames = []); | |
| } else { | |
| button("Record", () => intp.recordReplay()); | |
| } | |
| onecup.br(); | |
| button("Reset Game", () => { | |
| intp.stopReplay(); | |
| sim.local = false; | |
| battleMode.joinLocal(); | |
| }); | |
| onecup.br(); | |
| button(hasReplay() ? "Load (Overwrites)" : "Load", () => { | |
| let fileChooser = onecup.lookup("#replay-file"); | |
| if(!fileChooser) { | |
| replay.fileChooser = true; | |
| return; | |
| } | |
| replay.fileChooser = false; | |
| let file = fileChooser.files[0]; | |
| if(!file) return; | |
| let reader = new FileReader(); | |
| reader.onerror = e => replay.error = e.message; | |
| reader.onload = () => { | |
| try { | |
| let data = null; | |
| if(file.type === "application/json") { | |
| data = JSON.parse(reader.result); | |
| } else { | |
| let dv = new DataView(reader.result); | |
| data = sim.zJson.loadDv(dv); | |
| } | |
| intp.replayFrames = data.frames; | |
| intp.chatReplay = data.chat; | |
| } catch (e) { | |
| replay.error = e.message; | |
| } | |
| }; | |
| if(file.type === "application/json") { | |
| reader.readAsText(file); | |
| } else { | |
| reader.readAsArrayBuffer(file); | |
| } | |
| }); | |
| onecup.br(); | |
| if(replay.fileChooser) { | |
| onecup.input("#replay-file", {type: "file"}); | |
| } | |
| break; | |
| case "recording": | |
| button("Stop", () => intp.stopReplay()); | |
| break; | |
| case "playing": | |
| case "paused": | |
| button(sim.paused ? "Play" : "Pause", () => sim.paused = !sim.paused); | |
| button("Stop", () => intp.stopReplay()); | |
| button("Restart", () => { | |
| sim.paused = false; | |
| intp.playReplay(); | |
| }); | |
| onecup.br(); | |
| let currentSpeed = 50.5 / (62.5 + control.cheatSimInterval); | |
| let updateSpeed = speed => { | |
| if(speed >= 0.03125 && speed <= 32) | |
| control.cheatSimInterval = 50.5 / speed - 62.5; | |
| }; | |
| button("Slower", () => updateSpeed(currentSpeed / 2)); | |
| button("Faster", () => updateSpeed(currentSpeed * 2)); | |
| onecup.br(); | |
| onecup.text("Speed: " + currentSpeed); | |
| onecup.nbsp(); | |
| button("Reset Speed", () => control.cheatSimInterval = -12); | |
| onecup.br(); | |
| onecup.text("Watch as:"); | |
| onecup.br(); | |
| let playerButton = player => { | |
| if(!player.ai) { | |
| onecup.span(".hover-white", () => { | |
| onecup.margin(4); | |
| onecup.u(() => onecup.text(player.name)); | |
| onecup.onclick(() => { | |
| commander.name = player.name; | |
| commander.side = player.side; | |
| }); | |
| }); | |
| } else { | |
| onecup.text(player.name); | |
| } | |
| onecup.br(); | |
| }; | |
| onecup.div(() => { | |
| onecup.float("left"); | |
| onecup.margin_top(-12); | |
| for(let p of intp.players.filter(p => p.side === "alpha")) { | |
| playerButton(p); | |
| } | |
| }); | |
| onecup.div(() => { | |
| onecup.float("right"); | |
| onecup.margin_top(-12); | |
| for(let p of intp.players.filter(p => p.side === "beta")) { | |
| playerButton(p); | |
| } | |
| }); | |
| // custom game mode support | |
| onecup.div(() => { | |
| onecup.clear("both"); | |
| onecup.line_height(20); | |
| for(let p of intp.players.filter(p => p.side && p.side !== "alpha" && p.side !== "beta" && p.side !== "spectators")) { | |
| playerButton(p); | |
| } | |
| }); | |
| button("Spectate", () => { | |
| commander.name = replay.specName; | |
| }); | |
| break; | |
| } | |
| }); | |
| if(replay.error) { | |
| onecup.span(() => { | |
| onecup.color("#ff8888"); | |
| onecup.text("Error: " + replay.error); | |
| }); | |
| } | |
| }); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment