├── .gitattributes ├── code └── pircbot.jar ├── LICENSE ├── Bot.pde ├── README.md └── TwitchPlaysEverything.pde /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /code/pircbot.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molleindustria/TwitchPlaysEverything/HEAD/code/pircbot.jar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 molleindustria 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 | -------------------------------------------------------------------------------- /Bot.pde: -------------------------------------------------------------------------------- 1 | 2 | public class Bot extends PircBot { 3 | 4 | //change these strings 5 | String channel = "#yourchannel"; 6 | String name = "botname"; 7 | 8 | //an oauth token looks like oauth:1l33afoi4tvlulkky1eile0ki59d52 9 | //This is NOT the streaming code 10 | //if you trust this 3rd party site you can get it here: 11 | //https://twitchapps.com/tmi/ 12 | String twitchOauth = "oauth:requestyourtoken"; 13 | 14 | public Bot() { 15 | 16 | this.setName(name); 17 | 18 | // Enable debugging output. 19 | setVerbose(true); 20 | 21 | try { 22 | // Connect to the IRC server. 23 | connect("irc.twitch.tv", 6667, twitchOauth); 24 | } 25 | catch (Exception e) { 26 | println(e.getMessage()); 27 | } 28 | 29 | // Join the #pircbot channel. 30 | joinChannel(channel); 31 | // Not sure if necessary 32 | this.sendRawLine("CAP REQ :twitch.tv/membership"); 33 | } 34 | 35 | 36 | public void onMessage(String channel, String sender, String login, String hostname, String message) { 37 | 38 | parseCommand(message); 39 | 40 | //send message to the whole channel 41 | //sendMessage(channel,"Welcome "+login+"!"); 42 | 43 | //send PM 44 | //sendRawLineViaQueue("PRIVMSG #jtv :/w "+sender+" psst, secret"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch Plays Everything 2 | 3 | A simple Processing/java template for Twitch-plays-pokemon type of projects. 4 | It creates a chatbot that connects to your Twitch channel and lets users interact with **your** computer through text commands. 5 | 6 | * Requires [Processing](https://processing.org/) a free Java-based environment for creative coding. 7 | 8 | * Uses [PircBot](http://www.jibble.org/pircbot.php) an IRC library for java (included in code/) 9 | 10 | * Uses Java's [Robots](https://docs.oracle.com/javase/7/docs/api/java/awt/Robot.html) class to emulate mouse and keyboard events. There is no gamepad support. 11 | 12 | 13 | # Setup 14 | 15 | *Traditionally* Twitch-plays games run on dedicated machines that are streaming the output of the game through [OBS](https://obsproject.com/) or other applications. This program listens to Twitch chat messages and interprets them as input on your computer. A Twitch chat mostly functions like an IRC channel, so your bot can both receive or send messages. 16 | 17 | 1. Add your Twitch credentials to Bot.pde. 18 | 2. Add the applicationPath and applicationName if you want to manage the game launch and shutdown via Processing (not necessary) 19 | 3. Add your own keypresses in the arraylist "keys" with the associated commands 20 | 4. Add/remove other kind of commands in the parseCommand function 21 | 5. If using mouse controls change the mouse area to your active area (MIN_X etc.) 22 | 6. Press play, make sure your game is *focused* (otherwise the commands will affect other programs) 23 | 7. Start streaming the output 24 | 8. ??? 25 | 9. Profit! 26 | 27 | You don't have to be live to test the chat functionalities, just open your offline channel and type in the chat. 28 | 29 | # Notes 30 | 31 | * Twitch-plays are meant to be miserable experiences but only 0.001% of the games can be successfully played in this format. Consider hybrid experiences with spectators interacting/trolling the streamer instead of playing 100% of the game. 32 | 33 | * There is a lag of a couple of seconds between video and chat commands so choose slow, forgiving games. 34 | 35 | * Keep in mind that the bot will never know the internal state of the game (unless it's a custom made game). 36 | 37 | * TwitchPlaysEverything has no built-in voting system but you can implement one with some programming. Check the [Twitch IRC specifications](https://dev.twitch.tv/docs/irc/guide). 38 | 39 | * Twitch hides the channel description at the bottom of the page so users don't generally see the instructions. Consider setting up a stream overlay with some information on how to interact. You can use Processing's graphical capabilities for that. Check the simple scrolling text example. -------------------------------------------------------------------------------- /TwitchPlaysEverything.pde: -------------------------------------------------------------------------------- 1 | import java.awt.*; 2 | import java.awt.event.*; 3 | import java.awt.event.KeyEvent; 4 | 5 | //See the Bot class for login settings 6 | 7 | //the robot automating input 8 | Robot robot; 9 | 10 | //the chat bot interfacing with Twitch, see Bot class 11 | Bot bot; 12 | 13 | //mouse area 14 | int MIN_X = 40; 15 | int MIN_Y = 80; 16 | int MAX_X = MIN_X+900; 17 | int MAX_Y = MIN_Y+710; 18 | 19 | ArrayList keys; 20 | 21 | //timing vars in millis 22 | int lastTime = 0; 23 | int deltaTime = 0; 24 | 25 | //if receiving a key press already down 26 | //should it add the time (true) or ignore the command (false) 27 | boolean KEY_CUMULATIVE = false; 28 | 29 | //generic timer for application restart 30 | //in this example if somebody calls "restart" and nobody says anything 31 | //for 3 seconds the application restarts 32 | //it's a simple "voting" system 33 | int restartTimer = -1; 34 | int RESTART_TIME = 5000; 35 | 36 | //typing shouldn't be zero or it will skipp characters 37 | int TYPE_DELAY = 60; 38 | 39 | //runtime to open or close external applications 40 | Runtime runtime; 41 | Process process; 42 | 43 | //if you need to restart an application through a command 44 | //you need the path of the application 45 | //on mac you drag and drop the app on a terminal window to find it 46 | //mind that .app files may not be understood as executables, you have to "show package contents" and find the actual launcher 47 | 48 | //on windows right click property, don't forget to add double slashes "C:\\Users\\paolo\\Desktop\\ 49 | 50 | String applicationPath = ""; // "/Applications/pokemon.app/Contents/MacOS/" 51 | String applicationName = ""; // eg pokemon.exe 52 | 53 | //twitch users rarely scroll down the channel page to read the instructions 54 | //it's a good idea to have an overlay summarizing the commands 55 | //just a simple scrolling text, leave it blank to disable it 56 | String ticker = "Your instructions here"; 57 | float tickerX = 0; 58 | float tickerW = 0; 59 | PFont tickerFont; 60 | 61 | void setup() { 62 | size(1024, 40); 63 | 64 | //create the robot automation 65 | try { 66 | robot = new Robot(); 67 | robot.setAutoDelay(0); 68 | } 69 | catch (Exception e) { 70 | e.printStackTrace(); 71 | } 72 | 73 | //start the chatbot that listens to commands 74 | bot = new Bot(); 75 | 76 | //initialize all the possible keypresses 77 | //KeyEvent are just int codes https://docs.oracle.com/javase/6/docs/api/java/awt/event/KeyEvent.html 78 | keys = new ArrayList(); 79 | 80 | // you can specify one command associated to a keypress eg: 81 | //keys.add(new KeyPress( "start", KeyEvent.VK_SPACE)); 82 | 83 | //...or multiple alias for the same event 84 | String[] ids = {"left", "l"}; 85 | keys.add(new KeyPress( ids, KeyEvent.VK_LEFT)); 86 | 87 | String[] ids1 = {"right", "r"}; 88 | keys.add(new KeyPress( ids1, KeyEvent.VK_RIGHT)); 89 | 90 | String[] ids2 = {"up", "u"}; 91 | keys.add(new KeyPress( ids2, KeyEvent.VK_UP)); 92 | 93 | String[] ids3 = {"down", "d"}; 94 | keys.add(new KeyPress(ids3, KeyEvent.VK_DOWN)); 95 | 96 | String[] ids4 = {"enter", "return", "start"}; 97 | keys.add(new KeyPress(ids4, KeyEvent.VK_ENTER, 0, 1)); 98 | 99 | //check parse commands for more commands 100 | runtime = Runtime.getRuntime(); 101 | 102 | /* 103 | This part is experimental and may not work in some cases 104 | allows java to open and close the application and let users restart it. 105 | It can be used when the game doesn't have a clean restart system 106 | */ 107 | if (applicationPath != "" && applicationName != "" && process == null) { 108 | startGame(); 109 | } 110 | 111 | //center mouse 112 | robot.mouseMove(int(MIN_X+(MAX_X-MIN_X)/2), int(MIN_Y+(MAX_Y-MIN_Y)/2)); 113 | 114 | //initialize scrolling text 115 | if (ticker!="") { 116 | textSize(28); 117 | tickerW = textWidth(ticker); 118 | tickerX = width; 119 | } 120 | } 121 | 122 | //main loop 123 | void draw() { 124 | 125 | deltaTime = millis() - lastTime; 126 | lastTime = millis(); 127 | 128 | //update all the keypresses 129 | for (int i = 0; i < keys.size(); i++) { 130 | KeyPress kp = keys.get(i); 131 | kp.update(); 132 | } 133 | 134 | //restart in progress 135 | if (restartTimer > 0) { 136 | restartTimer -= deltaTime; 137 | 138 | //nobody opposed 139 | if (restartTimer<=0) { 140 | bot.sendMessage(bot.channel, "Restart in progress..."); 141 | 142 | if (applicationPath != "" && applicationName != "") 143 | { 144 | if (process != null) 145 | process.destroy(); 146 | 147 | delay(5000); 148 | startGame(); 149 | } 150 | } 151 | } 152 | 153 | 154 | //scrolling text 155 | if (ticker !="") { 156 | background(0); 157 | textAlign(LEFT, TOP); 158 | fill(255); 159 | text(ticker, tickerX, 5); 160 | tickerX--; 161 | if (tickerX<-tickerW) 162 | tickerX = width; 163 | } 164 | } 165 | 166 | 167 | //every chat string passes through this 168 | void parseCommand(String command) { 169 | 170 | String[] c = split(command, ' '); 171 | boolean keyFound = false; 172 | 173 | //cancel restart if in progress 174 | restartTimer = -1; 175 | 176 | if (c.length>0) { 177 | 178 | //check all the keypresses first 179 | for (int i = 0; i < keys.size(); i++) { 180 | 181 | KeyPress kp = keys.get(i); 182 | boolean found = false; 183 | String id = c[0].toLowerCase(); 184 | 185 | //check the command against all the ids, there may be aliases 186 | for (int j = 0; j= 2) { 199 | duration = float(c[1]); 200 | } 201 | 202 | if (kp.timer > 0 && KEY_CUMULATIVE) { 203 | kp.timer += duration; 204 | } 205 | if (kp.timer > 0 && !KEY_CUMULATIVE) 206 | { 207 | //just ignore 208 | } else { 209 | //normal 210 | kp.press(duration); 211 | } 212 | 213 | keyFound = true; 214 | } 215 | }//key loop 216 | 217 | 218 | //check other keywords if keys are not found 219 | if (!keyFound) { 220 | switch(c[0].toLowerCase()) { 221 | case "click": 222 | robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); 223 | robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); 224 | break; 225 | 226 | //mouse x y - instant move 227 | case "mouse": 228 | if (c.length >=3) { 229 | int x = int(c[1]); 230 | int y = int(c[2]); 231 | x = constrain(x, MIN_X, MAX_X); 232 | y = constrain(y, MIN_Y, MAX_Y); 233 | robot.mouseMove(x, y); 234 | } 235 | break; 236 | 237 | //mouse% x y - instant move in percent of area 238 | case "mouse%": 239 | if (c.length >=3) { 240 | int x = int(c[1]); 241 | int y = int(c[2]); 242 | x = constrain(x, 0, 100); 243 | y = constrain(y, 0, 100); 244 | int xp = int(map(x, 0, 100, MIN_X, MAX_X)); 245 | int yp = int(map(y, 0, 100, MIN_Y, MAX_Y)); 246 | robot.mouseMove(xp, yp); 247 | } 248 | break; 249 | 250 | //mouse increment 251 | case "x": 252 | if (c.length >=2) { 253 | int dx = int(c[1]); 254 | 255 | PointerInfo pi = MouseInfo.getPointerInfo(); 256 | // get the location of mouse 257 | Point p = pi.getLocation(); 258 | 259 | int x = p.x + dx; 260 | x = constrain(x, MIN_X, MAX_X); 261 | robot.mouseMove(x, p.y); 262 | } 263 | break; 264 | 265 | //mouse increment 266 | case "y": 267 | if (c.length >=2) { 268 | int dy = int(c[1]); 269 | 270 | PointerInfo pi = MouseInfo.getPointerInfo(); 271 | // get the location of mouse 272 | Point p = pi.getLocation(); 273 | 274 | int y = p.y + dy; 275 | y = constrain(y, MIN_Y, MAX_Y); 276 | robot.mouseMove(p.x, y); 277 | } 278 | break; 279 | 280 | //types a whole string and presses enter 281 | case "type": 282 | 283 | //there must be another member 284 | if (c.length >= 2) { 285 | //slice from the first space 286 | 287 | String s = command.substring(command.indexOf(" ")); 288 | 289 | for (int i = 0; i < s.length(); i++) { 290 | 291 | char ch = s.charAt(i); 292 | 293 | //special chars, they don't have a key associated 294 | //this is keyboard-layout dependent 295 | if (ch == '!') { 296 | robot.keyPress(KeyEvent.VK_SHIFT); 297 | robot.keyPress(KeyEvent.VK_1); 298 | robot.keyRelease(KeyEvent.VK_1); 299 | robot.keyRelease(KeyEvent.VK_SHIFT); 300 | } else if (ch == '?') { 301 | robot.keyPress(KeyEvent.VK_SHIFT); 302 | robot.keyPress(KeyEvent.VK_SLASH); 303 | robot.keyRelease(KeyEvent.VK_SLASH); 304 | robot.keyRelease(KeyEvent.VK_SHIFT); 305 | } else if (ch == '\'' || ch =='`') { 306 | robot.keyPress(KeyEvent.VK_QUOTE); 307 | } else if (ch == '"' || ch=='“' || ch=='”') { 308 | /* 309 | robot.keyPress(KeyEvent.VK_SHIFT); 310 | robot.keyPress(KeyEvent.VK_QUOTE); 311 | robot.keyRelease(KeyEvent.VK_QUOTE); 312 | robot.keyRelease(KeyEvent.VK_SHIFT); 313 | */ 314 | //doesn't seem to work with the first quote? 315 | } else { 316 | //normal letters 317 | if (Character.isUpperCase(ch)) { 318 | robot.keyPress(KeyEvent.VK_SHIFT); 319 | } 320 | robot.keyPress(Character.toUpperCase(ch)); 321 | 322 | robot.keyRelease(Character.toUpperCase(ch)); 323 | 324 | if (Character.isUpperCase(ch)) { 325 | robot.keyRelease(KeyEvent.VK_SHIFT); 326 | } 327 | } 328 | 329 | delay(TYPE_DELAY); 330 | } 331 | 332 | robot.keyPress(KeyEvent.VK_ENTER); 333 | robot.keyRelease(KeyEvent.VK_ENTER); 334 | } 335 | 336 | break; 337 | 338 | //calls a restart poll 339 | case "restart": 340 | if (applicationPath !="" && applicationName !="") { 341 | restartTimer = RESTART_TIME; 342 | bot.sendMessage(bot.channel, "RESTART CALLED! Effective in 5 seconds. If you don't want to restart speak now or forever hold your peace."); 343 | } 344 | break; 345 | }//end case 346 | }//end keyfound 347 | }//end cmd not empty 348 | } 349 | 350 | void startGame() { 351 | 352 | try 353 | { 354 | process = null; 355 | ProcessBuilder pb = new ProcessBuilder(applicationPath+applicationName); 356 | pb.directory(new File(applicationPath)); 357 | process = pb.start(); 358 | } 359 | catch (IOException e) 360 | { 361 | e.printStackTrace(); 362 | } 363 | } 364 | 365 | //simple class that holds the key down for x seconds 366 | //new times 367 | class KeyPress { 368 | String[] id; 369 | int k; 370 | float timer = 0; 371 | 372 | //Default min and max time allowed in SECONDS 373 | float MAX = 2; 374 | float MIN = 0; 375 | 376 | //initialize 377 | KeyPress(String _id, int _k) { 378 | this.id = new String[1]; 379 | this.id[0] = _id; 380 | this.k = _k; 381 | } 382 | 383 | //initialize 384 | KeyPress(String[] _id, int _k) { 385 | this.id = _id; 386 | this.k = _k; 387 | } 388 | 389 | //alt initialize with min and max values 390 | KeyPress(String _id, int _k, int _MIN, int _MAX) { 391 | this.id = new String[1]; 392 | this.id[0] = _id; 393 | this.k = _k; 394 | this.MIN = _MIN; 395 | this.MAX = _MAX; 396 | } 397 | 398 | KeyPress(String[] _id, int _k, int _MIN, int _MAX) { 399 | this.id = _id; 400 | this.k = _k; 401 | this.MIN = _MIN; 402 | this.MAX = _MAX; 403 | } 404 | 405 | //time is passed in seconds and converted to millis 406 | void press(float _time) { 407 | this.timer = _time; 408 | this.timer = constrain(this.timer, MIN, MAX) * 1000; //convert to millis 409 | robot.keyPress(this.k); 410 | } 411 | 412 | //countdown and release 413 | void update() { 414 | if (this.timer>0) { 415 | this.timer -= deltaTime; 416 | 417 | if (this.timer<=0) { 418 | robot.keyRelease(this.k); 419 | this.timer = 0; 420 | } 421 | } 422 | } 423 | }//end KeyPress class 424 | 425 | 426 | //utility delay, stops execution 427 | void delay(int time) { 428 | int current = millis(); 429 | while (millis () < current+time) Thread.yield(); 430 | } 431 | --------------------------------------------------------------------------------