├── doomgif ├── package.json ├── README.md └── doomgif.js ├── LICENSE └── README.md /doomgif/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doomgif", 3 | "description": "An HTTP server for streaming and controlling the game Doom", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/apsillers/living-gif.git" 11 | }, 12 | "keywords": [ 13 | "Doom" 14 | ], 15 | "author": "Andrew Sillers ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/apsillers/living-gif/issues" 19 | }, 20 | "homepage": "https://github.com/apsillers/living-gif", 21 | "dependencies": { 22 | "canvas": "^2.8.0", 23 | "express": "^4.17.1", 24 | "gifencoder": "^2.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrew Sillers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Night of the Living GIF! 2 | 3 | Looking for the [Doom-in-a-GIF](https://archiveofourown.org/works/31295183) code? Check out the [`doomgif`](https://github.com/apsillers/living-gif/tree/main/doomgif) directory. 4 | 5 | * Original !!Con presentation: https://youtu.be/g_eMNX4OvoY?t=1080 6 | 7 | * Slides: https://docs.google.com/presentation/d/1mzLjPF4lHa33l90O_VVwg6Cz-Wslnu1kz11U-2EjqWw/edit?usp=sharing 8 | 9 | * Hit counter: 10 | * [Demo](https://hitcounter.apsillers.repl.co/) 11 | * [Source](https://replit.com/@apsillers/hitcounter) 12 | 13 | * Game of Life: 14 | * [apsillers Stack Overflow profile](https://stackoverflow.com/users/710446/apsillers) 15 | * [Source](https://github.com/apsillers/stacklife) 16 | 17 | * Cave Adventure AO3: 18 | * [AO3 Demo](https://archiveofourown.org/works/31095317#game) 19 | * [Source](https://replit.com/@apsillers/iftest) 20 | 21 | * Multiplayer roguelike: 22 | * [apsillers Arqade profile](https://gaming.stackexchange.com/users/87199/apsillers?tab=profile) 23 | * [Source](https://replit.com/@apsillers/rogue-gif) 24 | 25 | * Doom on AO3: 26 | * [Doom running on AO3](https://archiveofourown.org/works/31295183) 27 | * [Source](https://github.com/apsillers/living-gif/tree/main/doomgif) 28 | -------------------------------------------------------------------------------- /doomgif/README.md: -------------------------------------------------------------------------------- 1 | # Doom GIF server 2 | 3 | ## What is this? 4 | 5 | This is an HTTP server that controls and streams an instance of Doom running concurrently on the same system. (Doom sold seperately.) 6 | 7 | ## How does it work? 8 | 9 | * The HTTP server listens for key commands like `Up`, `Down`, `Ctrl`, `space`, etc. and uses `xdotool` to pass them to the running Doom instance. 10 | 11 | * The HTTP server can also stream an incomplete GIF to anyone who asks at `/doom.gif`. 12 | 13 | * The server periodically tells Doom to save a screenshot to `~/DOOM00.png`, and then it adds that image to all open GIF streams. 14 | 15 | * You can embed the stream with `` and add link controls like `Toggle Forward` 16 | 17 | * Tada! You're remotely streaming and controlling Doom! 18 | 19 | ## Installation 20 | 21 | 0. You probably need an operating system that strongly integrates X11, because this program strongly depends on `xdotool`. This probably limits you to GNU/Linux distros, but possibly MacOS could do it, too. I'm not sure. 22 | 23 | 1. Install these packages (check with your local package manager for avilability): 24 | 25 | * [`chocolate-doom`](https://www.chocolate-doom.org/wiki/index.php/Chocolate_Doom) for a runnable clone of Doom 26 | * [`freedoom`](https://freedoom.github.io/download.html) for WAD data files. 27 | * [`xdotool`](https://github.com/jordansissel/xdotool) which can fake keyboard input 28 | 29 | 2. Configure Doom: 30 | 31 | * to use `P` as the print-screen key, and 32 | * to capture screenshots as PNGs. 33 | 34 | You can do this in `chocolate-setup` or by adding these lines to `~/.local/games/chocolate-doom/chocolate-doom.cfg`: 35 | 36 | key_menu_screenshot 25 37 | png_screenshots 1 38 | 39 | 4. If you are running this on a server without X11, you need to install X11 (I used `startxfce4`), and probably install a VNC server. I recommend `tigervnc`, because I found that [`tightvnc` did not work correctly with `xdotool`](https://github.com/jordansissel/xdotool/issues/126). 40 | 41 | 5. Optionally generate SSL keys (if using HTTPS) and point to them in the code. [`certbot`](https://certbot.eff.org/) is a free option for generating and signing keys. 42 | 43 | 6. Run `npm install` from within the doomgif directory in install the `gifencoder` and `canvas` NPM packages. 44 | 45 | 7. Run `node doomgif.js` and `chocolate-doom -iwad [path to freedoom.wad]`. If you're running this in a non-graphical console, you will need to make sure to set the `DISPLAY=:1` environment variable for each so they share the same X display: 46 | 47 | DISPLAY=:1 chocolate-doom -iwad whatever/freedoom.wad 48 | 49 | DISPLAY=:1 node display.js 50 | # or if you run with sudo, pass through env vars with -E 51 | DISPLAY=:1 sudo -E node server.js 52 | 53 | 8. By default, the server runs on port 8444 for HTTPS or 8080 for HTTP, so go to https://127.0.0.1:8444/doom.gif or http://127.0.0.1:8080/doom.gif. You can change the port, or uncomment the plain-HTTP `app.listen(8080)` to use HTTP. 54 | * If you plan to embed your GIF in an HTTPS page, your image must be served over HTTPS. 55 | -------------------------------------------------------------------------------- /doomgif/doomgif.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Andrew Sillers 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | */ 10 | 11 | console.log("Interacting with Doom on display ", process.env['DISPLAY']); 12 | 13 | const https = require("https"); 14 | const fs = require("fs"); 15 | const cp = require("child_process"); 16 | const express = require("express"); 17 | const GIFEncoder = require("gifencoder"); 18 | const { loadImage, createCanvas } = require('canvas'); 19 | const app = express(); 20 | const EventEmitter = require("events"); 21 | EventEmitter.defaultMaxListeners = 0; 22 | 23 | const HOME = "/home/doom"; 24 | 25 | //var dims = [282, 200]; 26 | var dims = [226, 160]; 27 | 28 | const redrawEmitter = new EventEmitter(); 29 | const canvas = createCanvas(dims[0], dims[1]); 30 | 31 | function makeEncoder() { 32 | const encoder = new GIFEncoder(dims[0], dims[1]); 33 | encoder.mystream = encoder.createReadStream(); 34 | encoder.start(); 35 | encoder.setRepeat(-1); 36 | encoder.setDelay(0); 37 | encoder.setQuality(80); 38 | return encoder; 39 | } 40 | var nextEncoder = makeEncoder(); 41 | 42 | // whenever the user asks for /doom.gif... 43 | app.get("/doom.gif", function(req, res) { 44 | // a little hack to make the next GIF encoder ahead of time before it's needed 45 | var encoder = nextEncoder; 46 | if(!encoder) { encoder = makeEncoder() } 47 | setTimeout(_=>nextEncoder = makeEncoder(), 0) 48 | 49 | // attach this GIF encoder to this HTTP response 50 | // so that whenever the encoder has new material it streams it out 51 | encoder.mystream.pipe(res); 52 | 53 | // whenever the image-reader signals it has a new image, add it to the GIF 54 | function doRedraw (){ 55 | encoder.addFrame(canvas.getContext("2d")); 56 | }; 57 | doRedraw(); 58 | redrawEmitter.on("redraw", doRedraw) 59 | 60 | // when the HTTP client disconnnects, clean up that GIF to reduce memory usage 61 | res.on("close", _=>{ 62 | redrawEmitter.removeListener("redraw", doRedraw); 63 | encoder.mystream.destroy(); 64 | encoder.finish(); 65 | }) 66 | }); 67 | 68 | // take screenshots by repeatedly pressing P 69 | // `P` must be configed as the print-screen button in Doom 70 | setInterval(function() { 71 | cp.exec("xdotool key p;"); 72 | }, 190); 73 | 74 | // try pushing latest screenshot at ~/DOOM00.png to all GIF streams 75 | setInterval(function() { 76 | loadImage(HOME + "/DOOM00.png").then(function(image) { 77 | //console.log("loaded", image); 78 | canvas.getContext("2d").drawImage(image, 0, 0, dims[0], dims[1]); 79 | redrawEmitter.emit("redraw"); 80 | cp.exec("rm " + HOME + "/DOOM*.png;"); 81 | }).catch(function(){}); 82 | }, 100) 83 | 84 | // what keys are authorized to be passed to xdotools 85 | var keys = ["Up", "Down", "Left", "Right", "Escape", "space", "Ctrl", "Return", "period", "comma"]; 86 | // what keys are currenly being held down (for toggling state) 87 | var downKeys = []; 88 | // whenever this button is pressed, release these other keys 89 | var releaseKeys = { 90 | "Escape": ["Ctrl", "Up", "Down", "Left", "Right", "period", "comma", "space"], 91 | "period": ["comma"], 92 | "comma": ["period"], 93 | "Left": ["Right"], 94 | "Right": ["Left"] 95 | } 96 | 97 | // listen for `/tap/{key}` (nonce is always ignored) 98 | app.get('/tap/:key/:nonce?', function(req, res) { 99 | var key = req.params.key; 100 | // only operate on approved keys 101 | if(keys.includes(key)) { 102 | // release any keys that should be released before pressing this key 103 | var keysToRelease = releaseKeys[key] || []; 104 | keysToRelease.forEach(keyUp); 105 | 106 | // tap the key 107 | cp.execSync("xdotool key " + key); 108 | console.log("pressed " + key); 109 | // key is no longer held down 110 | downKeys = downKeys.filter(k=> k != key); 111 | } 112 | res.redirect("https://archiveofourown.org/works/31295183#game"); 113 | }); 114 | 115 | // release named key in X11 and remove it from our list of pressed keys 116 | function keyUp(key) { 117 | //console.log("releasing " + key); 118 | cp.execSync("xdotool keyup " + key); 119 | downKeys = downKeys.filter(k=> k != key); 120 | } 121 | 122 | // listen for `/tap/{key}` (nonce is always ignored) 123 | app.get('/toggle/:key/:nonce?', function(req, res) { 124 | var key = req.params.key; 125 | // only operate on approved keys 126 | if(keys.includes(key)) { 127 | if(downKeys.includes(key)) { 128 | // if the key is down, release it 129 | keyUp(key); 130 | } else { 131 | // going to press the key... 132 | // first release any keys that should be released before pressing this key 133 | var keysToRelease = releaseKeys[key] || []; 134 | keysToRelease.forEach(keyUp); 135 | // push the key and add it to the list of held-down keys 136 | downKeys.push(key); 137 | cp.execSync("xdotool keydown " + key); 138 | } 139 | console.log("toggled " + key); 140 | } 141 | res.redirect("https://archiveofourown.org/works/31295183#game"); 142 | }); 143 | 144 | app.get('/', function(req, res) { 145 | res.send(""); 146 | }); 147 | 148 | 149 | //app.listen(8080); 150 | 151 | https 152 | .createServer( 153 | { 154 | key: fs.readFileSync('/etc/letsencrypt/live/doomgif.apsillers.com/privkey.pem'), 155 | cert: fs.readFileSync('/etc/letsencrypt/live/doomgif.apsillers.com/fullchain.pem'), 156 | }, 157 | app 158 | ) 159 | .listen(8444, () => { 160 | console.log('Listening...') 161 | }) 162 | 163 | --------------------------------------------------------------------------------