Skip to content

Instantly share code, notes, and snippets.

@twilight-sparkle-irl
Last active July 1, 2022 10:43
Show Gist options
  • Save twilight-sparkle-irl/eca383c67a50604b0d1783041429229c to your computer and use it in GitHub Desktop.
Save twilight-sparkle-irl/eca383c67a50604b0d1783041429229c to your computer and use it in GitHub Desktop.
gb.gif
/*
Copyright 2021 Andrew Sillers <apsillers@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// all rights reversed 2022 twilight sparkle
// if you can fix it you get to keep it
// kitbash of https://gitlab.com/piglet-plays/serverboy.js/-/blob/master/examples/streaming/server/index.js and https://github.com/apsillers/living-gif/
// input is heavily bugged and the gif will sometimes fall behind
const https = require("https");
const fs = require("fs");
const express = require("express");
const GIFEncoder = require("gifencoder");
const { createImageData, createCanvas } = require('canvas');
const app = express();
const EventEmitter = require("events");
const Gameboy = require("serverboy");
EventEmitter.defaultMaxListeners = 0;
var dims = [160,144];
var rom = fs.readFileSync('gb.rom');
var gameboy_instance = new Gameboy();
gameboy_instance.loadRom(rom);
const redrawEmitter = new EventEmitter();
const canvas = createCanvas(dims[0], dims[1]);
function makeEncoder() {
const encoder = new GIFEncoder(dims[0], dims[1]);
encoder.mystream = encoder.createReadStream();
encoder.start();
encoder.setRepeat(-1);
// issue in original code: setting this lower eventually hardcaps it to a value that is higher than the minimum
// read more here https://wunkolo.github.io/post/2020/02/buttery-smooth-10fps/
encoder.setDelay(24);
encoder.setQuality(80);
return encoder;
}
var nextEncoder = makeEncoder();
// whenever the user asks for /gameboy.gif...
app.get("/gameboy.gif", function(req, res) {
// a little hack to make the next GIF encoder ahead of time before it's needed
var encoder = nextEncoder;
if(!encoder) { encoder = makeEncoder() }
setTimeout(_=>nextEncoder = makeEncoder(), 0)
// attach this GIF encoder to this HTTP response
// so that whenever the encoder has new material it streams it out
encoder.mystream.pipe(res);
// whenever the image-reader signals it has a new image, add it to the GIF
function doRedraw (){
encoder.addFrame(canvas.getContext("2d"));
};
doRedraw();
redrawEmitter.on("redraw", doRedraw)
// when the HTTP client disconnnects, clean up that GIF to reduce memory usage
res.on("close", _=>{
redrawEmitter.removeListener("redraw", doRedraw);
encoder.mystream.destroy();
encoder.finish();
})
});
var frames = 0;
var keysToPress = [];
var emulatorLoop = function() {
gameboy_instance.pressKeys(keysToPress);
// pressing keys is done in doFrame so we need to do it before!!
// https://gitlab.com/piglet-plays/serverboy.js/-/blob/master/src/interface.js#L114
var currentScreen = gameboy_instance.doFrame();
if(keysToPress.length) console.log(frames, keysToPress)
keysToPress = [];
frames++;
if (frames % 30 === 0) { //Output every 30th frame.
// console.log(frames)
//console.log(screen)
var ctx = canvas.getContext('2d');
var data = createImageData(new Uint8ClampedArray(currentScreen), 160, 144);
ctx.putImageData(data, 0, 0);
redrawEmitter.emit("redraw");
}
setTimeout(emulatorLoop,5); //Try and run at about 60fps.
};
// https://gitlab.com/piglet-plays/serverboy.js/-/blob/master/src/interface.js#L4
keys = ["right","left","up","down","a","b","select","start"]
// listen for `/tap/{key}` (nonce is always ignored)
app.get('/tap/:key/:nonce?', function(req, res) {
var key = req.params.key;
// only operate on approved keys
if(keys.includes(key)) {
var k = keys.indexOf(key)
keysToPress.push(k);
}
res.send("<script>window.close()</script>"); // CLOSES THE WINDOW WHEN WE OPEN IT FROM A TARGET=_BLANK \o/ NO MORE TAB SPAM
});
app.get('/', function(req, res) {
res.send("<img src='/gameboy.gif'>");
});
emulatorLoop();
https
.createServer(
{
key: fs.readFileSync('/etc/letsencrypt/live/synthetic.garden/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/synthetic.garden/fullchain.pem'),
},
app
)
.listen(8444, () => {
console.log('Listening...')
})
<div style="width:200px;position:relative;text-align:center;background-color:lightgrey;padding:10px;border-radius:10px;">
<img src="https://synthetic.garden:8444/gameboy.gif" style="margin:auto;">
<table style="width:100px;height:100px;text-align:center;">
<tr><td style="padding:0;"></td><td style="padding:0;background-color:grey;color:white;"><a href="https://synthetic.garden:8444/tap/up">^</a></td><td style="padding:0;"></td></tr>
<tr><td style="background-color:grey;color:white;padding:0;"><a href="https://synthetic.garden:8444/tap/left">&lt;</a></td><td style="padding:0;"></td><td style="background-color:grey;color:white;padding:0;">
<a href="https://synthetic.garden:8444/tap/right">&gt;</a></td></tr>
<tr><td style="padding:0;"></td><td style="background-color:grey;color:white;padding:0;"><a href="https://synthetic.garden:8444/tap/down">v</a></td><td style="padding:0;"></td></tr>
</table>
<div style="width:100px;text-align:right;height:50px;position:absolute;right:5px;bottom:75px;">
<a href="https://synthetic.garden:8444/tap/a" style="background-color:red;padding:10px;border-radius:100%;">a</a><span style="padding:10px;">&nbsp;</span>
<br />
<a href="https://synthetic.garden:8444/tap/b" style="background-color:red;padding:10px;border-radius:100%;">b</a></div>
<a href="https://synthetic.garden:8444/tap/start">start</a> - <a href="https://synthetic.garden:8444/tap/select" >select</a></div>
<b>Hold CTRL while clicking the buttons for a better experience!</b><br />inspired by @mog's <a href="https://cohost.org/mog/post/18663-cohost-doom">cohost doom</a><br />
NOTE: <s>i'll try to keep this up for as long as it will run but <i>saving does not work.</i> please adjust your expectations accordingly.</s> SORRY FOLKS. WORKING ON PUTTING IT BACK TOGETHER AGAIN ASAP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment