Skip to content

Instantly share code, notes, and snippets.

@Rio6
Last active April 10, 2024 01:14
Show Gist options
  • Select an option

  • Save Rio6/b65a924b01e5011b72af59f48e817c06 to your computer and use it in GitHub Desktop.

Select an option

Save Rio6/b65a924b01e5011b72af59f48e817c06 to your computer and use it in GitHub Desktop.
Replay script - records, save, and replay istrolid games
/*
* 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