├── .gitignore ├── LICENSE ├── README.md ├── config ├── default.sample.json ├── gba.json ├── go.json ├── gta.json ├── hat.json ├── magic.json ├── paint.json ├── pong.json ├── qwerty.json └── stardew.json ├── docs ├── SegaCollection.MD └── VBA.MD ├── img ├── logo.png ├── share.png ├── share_code.png └── vba.png ├── index.js ├── lib ├── Config.js ├── ControlsProcessor.js ├── auth.js ├── consensus │ ├── keyboard │ │ ├── anarchy.js │ │ └── democracy.js │ ├── mouse │ │ └── democracy.js │ └── screen │ │ └── democracy.js ├── constraints │ └── window.js ├── errors.js ├── handlers │ ├── kbm-robot.js │ ├── keyboardz.js │ ├── mouse │ │ ├── robot-js.js │ │ └── robotjs.js │ ├── robot-js.js │ ├── robotjs.js │ ├── screen │ │ └── robotjs.js │ └── window │ │ ├── robot-js.js │ │ └── windoze.js ├── reconnector.js ├── state │ ├── ControlState.js │ ├── JoyStick.js │ ├── Tactile.js │ └── enhancer.js ├── util.js └── widgets │ ├── KeyBar.js │ ├── QGram.js │ └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | /config/default.json 30 | /config/auth.json 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Richard Fox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive Keyboard (DEPRECATED) 2 | 3 | ## Deprecation Note 4 | 5 | This project was written for Interactive 1 which is slowely being replaced with Interactive 2. Due to this, the project is now under deprecation. You should look for an Interactive 2 compatible project. 6 | 7 | ## Introduction 8 | 9 | [Beam.pro](https://beam.pro) is a live streaming site that lets viewers interract through onscreen controls with the streamer's game. This project binds beam interactive controls to keyboard/mouse events on the system. This allows viewers to control aspects of/the whole game through beam. 10 | 11 | A few 24/7 automated streams make heavy use of this. 12 | * https://beam.pro/Youplay 13 | * https://beam.pro/merlin 14 | 15 | This project is currently in a *pre-release state*. There are undocumented features and [bugs](https://github.com/ProbablePrime/interactive-keyboard/issues). That need to be solved before it is in a stable condition. 16 | 17 | Until this project is in a stable condition I do not reccomend submitting any integrations that use this to the interactive store. 18 | 19 | ## Support 20 | Please open an issue directly on this project for support. 21 | 22 | ## Requirements 23 | * Node.js >= 6 24 | 25 | ## Setup 26 | 1. Choose a keyboard controlled game. 27 | 2. Make a Controls layout for that game in the Beam Controls Editor ensuring that both **holding and frequency** are checked under the analysis section for each Control and that each key has a valid key code in `Keyboard Trigger` 28 | 3. Clone this project down to your pc. 29 | 5. Open a terminal/cmd to the root folder of your cloned copy. 30 | 6. Run `npm install` to install dependancies 31 | 7. In the `config/` folder create a file called auth.json. It should contain your username and a password **OR** an OAuth token. Tthis is used to authenticate with Beam. 32 | 33 | ### Password Authentication 34 | ``` 35 | { 36 | "username":"ProbablePrime", 37 | "password":"password" 38 | } 39 | ``` 40 | ### OAuth Token Authentication 41 | ``` 42 | { 43 | "username":"ProbablePrime", 44 | "token":"OAuth Token" 45 | } 46 | ``` 47 | 48 | If you're using OAuth, the scopes required on the OAuth Token are `interactive:robot:self` and `channel:update:self`. 49 | 50 | Next: 51 | 52 | 10. Create a config file in config/ called .json for example `config/pokemon.json`. You can use [config/default.sample.json](config/default.sample.json) as a base/example to work from. 53 | 11. Start your chosen game, preferably in windowed mode. 54 | 12. Go back to a terminal/cmd that's in this project's folder. 55 | 12. Enter `node index.js ./config/yourgame.json` in the terminal replacing yourgame with the name of the config file you created. 56 | 13. If you see "Connected to beam" you should be good to go. 57 | 14. Test out your controls. 58 | 15. If they do not work, see the [troubleshooting section ](README.md#troubleshooting) 59 | 60 | # Sharing your game 61 | 62 | If your game is private or not published. You can use the version id and share code to enable other people (Including yourself to play it). To obtain these visit your controls and click the share button. 63 | 64 | ![share](https://raw.githubusercontent.com/ProbablePrime/beam-keyboard/master/img/share.png) 65 | 66 | Select the second radio button in the popup. Your version id is a number displayed at the **top of the popup**. The share code is in the text box in the middle of the popup: 67 | 68 | ![share_code](https://raw.githubusercontent.com/ProbablePrime/beam-keyboard/master/img/share_code.png) 69 | 70 | Place these in your config file ensuring that the file is still valid json: 71 | ``` 72 | "version":versionid, 73 | "code":"sharecode" 74 | ``` 75 | 76 | # Handlers 77 | 78 | Handlers are provided to do the actual keypressing when keys are recieved from Beam. We currently support: 79 | 80 | * robot-js - A new alternative to robotjs 81 | * robotjs - Robust, Linux/Windows/Mac (Robotjs is now easier to install go play, yay!) 82 | 83 | ## Deprecated Handlers 84 | These handlers have undocumented compatibility issues. 85 | * keyboardz - Easy to install, Windows only 86 | * kbm-robot - Easy to install, Flakey/Unpredictable. Supports DirectInput/XInput games 87 | 88 | 89 | To use a handler for your game install it in the same folder as this project with `npm install` so if you chose `kbm-robot` that would be `npm install kbm-robot`. Then in your config file change the `"handler":"robot-js",` to `"handler":"kbm-robot",`. 90 | 91 | # Consensus / Metric / Maths 92 | With potentially 100s of people pushing the buttons we need some way to decide if a button should be pushed. 93 | 94 | Beam currently provides in each report: 95 | * the number of people who've used the controls at various intervals(now, 10s,20s,30s..etc) 96 | * the number of people watching the stream 97 | * For each button: 98 | * The number of people holding a button down 99 | * the number of button pushes 100 | * the number of button releases 101 | 102 | We support multiple Consensus algorithms but for now the default is called "Democracy" it works as follows: 103 | 104 | * Calculate a percentage value for holding, releasing, pushing for this report 105 | * For Each Button: 106 | * If the percent of people holding the button down this report is greater or equal to the threshold value(defaults to 50%) 107 | * Push the button. 108 | * Else Release the button. 109 | 110 | If you can think of a better Metric. Please feel free to PR. 111 | 112 | # Advanced Configuration 113 | 114 | ## Blocks 115 | You can specify an optional configuration attribute called "blocks" which will block certain keys from being held down at the same time. 116 | 117 | For example: 118 | ``` 119 | "blocks": { 120 | "start":"select", 121 | "select":"start" 122 | } 123 | ``` 124 | Will prevent people from being able to push both select and start at the same time. This is helpful as it prevents soft resets. 125 | In older games. 126 | 127 | ## Tactile Threshold 128 | Set the required percentage of users pushing a button for it to be depressed. Defaults to 0.1. 129 | ``` 130 | tactileThreshhold: a Number greater than 0 131 | ``` 132 | 133 | # FAQ 134 | 135 | ## Controls not Working 136 | While every effort is made to support as many games as possible. There are issues with certain titles. Ultimately some games are incompatible with fake inputs from code. They want real keyboard presses. 137 | 138 | * Check your keybindings for the game they should match the keys you are pressing. Press the actual keys on your keyboard to check. 139 | * Try refreshing the beam page. 140 | * Are you focused on/in your game. You must have your mouse inside the game for the keys to register. 141 | * Try `kbm-robot` as your handler some require this to interface with DirectInput/XInput. 142 | * Try from another device. As this pushes your physical keys, its often impossible to test on the same machine as an infinite loop of key presses occurs. Summon a friend into your channel to help test :). 143 | * Try The key without a spark cost or cooldown. 144 | * Set your `tactileThreshold` to `0.1` in the config file 145 | * Try another game 146 | * Try notepad 147 | 148 | ## Cannot set channel to interactive with that game 149 | 150 | This usually means you have an incorrect share code, version code or just generally don't have access to that interactive game. Try a control layout you own. 151 | 152 | ## Exiting due to zero 153 | This one's hard, try checking your config files, it shouldn't happen. 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /config/default.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":000, 3 | "code":"shareCode" 4 | } 5 | -------------------------------------------------------------------------------- /config/gba.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":619, 3 | "code":"nwpzgv50", 4 | "threshold":0.5, 5 | "handler": "robotjs", 6 | "remap":false, 7 | "remapTable": { 8 | }, 9 | "blocks": { 10 | "start":"select", 11 | "select":"start", 12 | "W":"S", 13 | "S":"W", 14 | "A":"D", 15 | "D":"A", 16 | "L":"R", 17 | "R":"L" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/go.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":3138, 3 | "code":"40fi0ogq", 4 | "threshold":0.1, 5 | "handler": "robotjs", 6 | "windowTarget":"", 7 | "widgets":true, 8 | "mouseEnabled":true, 9 | "consensus": "democracy", 10 | "custom": { 11 | "joystick": { 12 | "0": "geofix" 13 | } 14 | }, 15 | "telnet": { 16 | "host": "192.168.0.110", 17 | "port": "5554", 18 | "shellPrompt":"", 19 | "echoLines":0 20 | }, 21 | "start": { 22 | "long": 47.656848, 23 | "lat": -122.316136 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/gta.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":678, 3 | "code":"742xctbe", 4 | "threshold":0.5, 5 | "handler": "kbm-robot", 6 | "remap":false, 7 | "remapTable": {}, 8 | "widgets":false 9 | } 10 | -------------------------------------------------------------------------------- /config/hat.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":541, 3 | "code":"cgvh91le", 4 | "threshold":0.5, 5 | "handler": "robotjs", 6 | "remap":false, 7 | "remapTable": { 8 | "J":"SPACE" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/magic.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":1144, 3 | "code":"ecqd28hi", 4 | "threshold":0.1, 5 | "handler": "kbm-robot" 6 | } 7 | -------------------------------------------------------------------------------- /config/paint.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":2320, 3 | "code":"ecqd28hi", 4 | "threshold":0.1, 5 | "handler": "robot-js", 6 | "windowTarget":"Untitled.*", 7 | "widgets":false, 8 | "mouseSource":"screen", 9 | "mouseEnabled":true, 10 | "consensus": "democracy" 11 | } 12 | -------------------------------------------------------------------------------- /config/pong.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":582, 3 | "code":"r8rdivrk", 4 | "threshold":0.5, 5 | "handler": "robotjs", 6 | "remap":false, 7 | "remapTable": { 8 | "J":"SPACE" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/qwerty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":1235, 3 | "code":"bpgsdklz", 4 | "threshold":0.1, 5 | "handler": "robotjs" 6 | } 7 | -------------------------------------------------------------------------------- /config/stardew.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":1369, 3 | "code":"ttrmmrc", 4 | "threshold":0.1, 5 | "handler": "robotjs", 6 | "windowTarget":"Stardew Valley - Version 1.04-Z_MODDED | SMAPI 0.37.2 Alpha", 7 | "widgets":true, 8 | "mouseEnabled":true, 9 | "consensus": "democracy" 10 | } 11 | -------------------------------------------------------------------------------- /docs/SegaCollection.MD: -------------------------------------------------------------------------------- 1 | 2 | # Beam Plays the Sega Collection 3 | 4 | Sega released a ton of Mega Drive/Genesis games into packs onto steam that are wrapped up into a nice official emulator. 5 | 6 | Owning at least one game from the collection is **REQUIRED**. I reccomend [Streets of Rage 2](http://store.steampowered.com/app/71165/) 7 | 8 | There's also packs that include multiple games you can find the [packs on Steam](http://store.steampowered.com/search/?term=SEGA%20MEGA%20Drive%20Classics%20Pack) + 9 | [Pack 5](http://store.steampowered.com/sub/14445/) (Not included in my search results for some reason). 10 | 11 | There's also a [bundle](http://store.steampowered.com/sub/7827/) for a load of them which is what I purchased awhile ago. 12 | 13 | ## Setup 14 | * Follow the regular set from the [readme](../README.md) 15 | * Write a config file in config/ called sega.json (`config/sega.json`) 16 | ``` 17 | { 18 | "beam": { 19 | "username": "Your username", 20 | "password": "", 21 | "channel": "Your channel" 22 | }, 23 | "handler": "kbm-robot", 24 | "remap":true, 25 | "remapTable": { 26 | 'W':'1', 27 | 'S':'2', 28 | 'A':'3', 29 | 'D':'4', 30 | 'J':'5', 31 | 'K':'6', 32 | 'L':'7', 33 | 'I':'8' 34 | } 35 | } 36 | ``` 37 | * Open up the Sega Collection via steam and click options. 38 | 39 | I've set this up to use the top row of the keyboard 1 - =. This keeps things like WSAD free for YOU to use if you don't have a controller. I'm also limited by the choice of keys that the underlying nodejs module that pushes keyboard keys for you can use. 40 | 41 | The end goal is to emulate a HID device and ouput button pushes so we don't have to use your keyboard but until then. Pause interactive mode if you need to type. 42 | 43 | ### For One player games (Beam Plays): 44 | Setup the 1st player to be custom keyboard 45 | 46 | ![Custom Keyboard](http://i.ahref.co.uk/u/r/QlSx.png) 47 | 48 | Click assign controller mapping and set it up like this: 49 | 50 | ![One player controls][controls] 51 | 52 | ### For Two Player Games (Play *With* Beam) 53 | 54 | Setup Player one however you like. 55 | 56 | Set player two to custom keyboard: 57 | 58 | ![Custom Keyboard](http://i.ahref.co.uk/u/r/QlSx.png) 59 | 60 | Click assign controller mapping and set it up like this: 61 | 62 | ![Two player controls][controls] 63 | 64 | ### Notes 65 | X,Y and Z don't really matter here only a small amount of Mega Drive games used them and to the best of my knowledge they aren't included in the packs on steam. 66 | 67 | Please remember what start is too as I neglected to give viewers that particular control to prevent "Pause Spam" whilst I workout kinks in how I handle data from Beam. You may have to for some games push start for the viewers. Every other control works fine though. 68 | 69 | ## Use 70 | 71 | 1. Launch the sega collection and pick a game, Wait till you get to the games main menu. 72 | 2. Set Beam to Interactive mode and select the Sega Collection as your game 73 | 3. Run the script with `node index.js ./config/sega.json` or your Operating system's equivelant. 74 | 4. If you see "Connected to beam" your all good and can start playing. 75 | 76 | ##Games Tested 77 | Ive tested these with low numbers of viewers: 78 | * Gunstar Heroes(The whole reason this project exists) 79 | * Streets of Rage 2. There's a duel mode here where you can fight each other too! 80 | * Streets of Rage. 81 | * Ecco The Dolphin 82 | 83 | ##Games 84 | A partial list of games in the collection: 85 | * Alex Kidd™ in the Enchanted Castle 86 | * Alien Soldier 87 | * Alien Storm 88 | * Altered Beast™ 89 | * Bio-Hazard Battle™ 90 | * Bonanza Bros.™ 91 | * Columns™ 92 | * Columns™ III 93 | * Comix Zone™ 94 | * Crack Down™ 95 | * Decap Attack™ 96 | * Ecco the Dolphin™ 97 | * Ecco™ Jr. 98 | * Ecco™: The Tides of Time 99 | * ESWAT™: City Under Siege 100 | * Eternal Champions™ 101 | * Fatal Labyrinth™ 102 | * Flicky™ 103 | * Gain Ground™ 104 | * Galaxy Force II™ 105 | * Golden Axe™ 106 | * Golden Axe™ II 107 | * Gunstar Heroes 108 | * Kid Chameleon™ 109 | * Landstalker: The Treasures of King Nole 110 | * Light Crusader 111 | * Ristar™ 112 | * Shadow Dancer™ 113 | * Shining Force 114 | * Shining Force II 115 | * Shining in the Darkness 116 | * Shinobi™ III: Return of the Ninja Master 117 | * Space Harrier™ II 118 | * Streets of Rage 119 | * Streets of Rage 2 120 | * Super Thunder Blade™ 121 | * Sword of Vermilion™ 122 | * Vectorman™ 123 | * Virtua Fighter™ 2 124 | 125 | [controls]: http://i.ahref.co.uk/u/r/LW5B.png 126 | -------------------------------------------------------------------------------- /docs/VBA.MD: -------------------------------------------------------------------------------- 1 | #Visual Boy Advance 2 | 3 | Visual Boy Advance is a a gameboy advanced and gameboy color emulator. 4 | 5 | #Setup 6 | * Get yourself Visual Boy Advance. 7 | * Get yourself the ROM of the game you wish to play 8 | * Set the controls up to match your controls in the control editor: 9 | 10 | ![logo](https://raw.githubusercontent.com/rfox90/beam-segacollection/master/img/vba.png) 11 | 12 | 13 | * Follow the usual setup instructions from the [readme](../README.md) 14 | * Install the robotjs handler `npm install robotjs` if you haven't already 15 | * Use the following as a sample config file for this script 16 | ``` 17 | { 18 | "beam": { 19 | "username": "Your channel", 20 | "password": "", 21 | "channel": "Your channel" 22 | }, 23 | "handler": "robotjs", 24 | "remap":false 25 | } 26 | ``` 27 | * Save the config in `config/vba.json` or `config/gamename.json` 28 | * Launch the script in the usual way, specifiyng your VBA config `node index.js ./config/vba.json` 29 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProbablePrime/interactive-keyboard/a7176b75c98033288801e658542186baddddf527/img/logo.png -------------------------------------------------------------------------------- /img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProbablePrime/interactive-keyboard/a7176b75c98033288801e658542186baddddf527/img/share.png -------------------------------------------------------------------------------- /img/share_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProbablePrime/interactive-keyboard/a7176b75c98033288801e658542186baddddf527/img/share_code.png -------------------------------------------------------------------------------- /img/vba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProbablePrime/interactive-keyboard/a7176b75c98033288801e658542186baddddf527/img/vba.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Beam = require('beam-client-node'); 3 | const Interactive = require('beam-interactive-node'); 4 | const Packets = require('beam-interactive-node/dist/robot/packets').default; 5 | const Promise = require('bluebird'); 6 | const cargo = require('async.cargo'); 7 | 8 | const auth = require('./lib/auth.js'); 9 | 10 | const State = require('./lib/state/ControlState'); 11 | const ControlsProcessor = require('./lib/ControlsProcessor'); 12 | const Config = require('./lib/Config'); 13 | const enhanceState = require('./lib/state/enhancer'); 14 | const reconnector = require('./lib/reconnector'); 15 | 16 | const args = process.argv.slice(2); 17 | const file = args[0]; 18 | const config = new Config(file); 19 | 20 | let state; 21 | let widgets; 22 | let channelId; 23 | const beam = new Beam(); 24 | let robot = null; 25 | 26 | if (config.widgets !== undefined && config.widgets) { 27 | widgets = require('./lib/widgets'); 28 | } else { 29 | widgets = function () {}; 30 | } 31 | const processor = new ControlsProcessor(config); 32 | 33 | processor.on('changed', report => { 34 | widgets(report); 35 | }); 36 | const limit = 10; 37 | const train = cargo((tasks, callback) => { 38 | if (train.length() > limit) { 39 | train.kill(); 40 | } 41 | tasks.forEach(task => { 42 | const progress = processor.process(task.current, task.previous); 43 | if (robot !== null && robot.connect.socket.readyState === robot.connect.socket.OPEN) { 44 | if (progress.tactile.length !== 0 || progress.joystick.length !== 0) { 45 | robot.send(new Packets.ProgressUpdate(progress)); 46 | } 47 | } 48 | }); 49 | callback(); 50 | }, limit); 51 | /** 52 | * Our report handler, entry point for data from beam 53 | * @param {Object} report Follows the format specified in the latest tetris.proto file 54 | */ 55 | function handleReport(report) { 56 | // Each report needs to be treated by itself and thus we can't carry any references through 57 | // If we do newer reports will update some state before we've processed it. 58 | // Due to this we're processing everything in an Immuteable style. 59 | const enhancedState = enhanceState(report, state); 60 | 61 | train.push({current: enhancedState, previous: state}); 62 | if (!report.tactile.length && report.users.active === 0) { 63 | processor.clearKeys(state.tactiles); 64 | } 65 | } 66 | 67 | process.on('exit', code => { 68 | console.log(`Caught an exit event, cleaning up.`); 69 | processor.clearKeys(state); 70 | }); 71 | process.on('SIGINT', () => { 72 | processor.clearKeys(state); 73 | process.exit(); 74 | }); 75 | 76 | function getchannelId(channelName) { 77 | return beam.request('GET', `channels/${channelName}`).then(res => { 78 | channelId = res.body.id; 79 | return res.body.id; 80 | }); 81 | } 82 | 83 | function goInteractive(versionCode, shareCode) { 84 | return beam.request('PUT', `channels/${channelId}`, {body: { 85 | interactive: true, 86 | interactiveGameId: versionCode, 87 | interactiveShareCode: shareCode 88 | }, json: true}).then(res => { 89 | if (res.statusCode !== 200 || !res.body.interactive) { 90 | throw new Error('Couldn\'t set channel to interactive with that game.'); 91 | } 92 | }); 93 | } 94 | 95 | function checkInteractive() { 96 | return beam.request('GET', `channels/${channelId}?fields=interactive`).then(res => { 97 | return res.body && res.body.interactive; 98 | }); 99 | } 100 | 101 | function hasControls(type, controls) { 102 | return controls[type] && controls[type].length; 103 | } 104 | 105 | function hasSomeControls(controls) { 106 | return hasControls('tactiles', controls) || hasControls('joysticks', controls) || hasControls('screens', controls); 107 | } 108 | 109 | function validateControls(controls) { 110 | if (!hasSomeControls(controls)) { 111 | throw new Error('No controls found'); 112 | } 113 | return controls; 114 | } 115 | 116 | function getControls(version, code) { 117 | return beam.request('GET', `interactive/versions/${version}?code=${code}`) 118 | .then(res => { 119 | if (!res.body.controls) { 120 | throw new Error('Incorrect version id or share code in your config or no control layout saved for that version.'); 121 | } 122 | return res.body.controls; 123 | }).catch(() => { 124 | throw new Error('Problem retrieving controls'); 125 | }); 126 | } 127 | 128 | function createState(controls) { 129 | return new State(controls); 130 | } 131 | 132 | function validateConfig() { 133 | if (!config) { 134 | throw new Error('Missing config file cannot proceed, Please create a config file. Check the readme for help!'); 135 | } 136 | if (!config.version || !config.code) { 137 | throw new Error('Missing version id and share code. These are required for now'); 138 | } 139 | } 140 | 141 | function setup() { 142 | validateConfig(); 143 | console.warn('This is a pre-release, open issues on the github: https://github.com/ProbablePrime/interactive-keyboard for help.'); 144 | console.log(`Using ${config.beam.username} with Version: ${config.version} & Code: ${config.code} & Handler: ${config.handler}`); 145 | go(); 146 | } 147 | 148 | function onInteractiveConnect(err) { 149 | if (err) { 150 | console.log('Theres a problem connecting to Interactive'); 151 | console.log(err); 152 | } else { 153 | console.log('Connected to Interactive'); 154 | } 155 | } 156 | 157 | function performRobotHandshake(robot) { 158 | return new Promise((resolve, reject) => { 159 | robot.handshake(err => { 160 | if (err) { 161 | reject(err); 162 | } 163 | onInteractiveConnect(err); 164 | resolve(); 165 | }); 166 | }); 167 | } 168 | 169 | function launchInteractive(beam, id) { 170 | return beam.game.join(id).then(details => { 171 | console.log('Authenticated, Spinning up Interactive Connection'); 172 | robot = new Interactive.Robot({ 173 | remote: details.body.address, 174 | key: details.body.key, 175 | channel: id 176 | }); 177 | robot.on('report', handleReport); 178 | robot.on('error', code => console.log(code)); 179 | reconnector(robot, launchInteractive.bind(this, beam, id), onInteractiveConnect); 180 | return performRobotHandshake(robot); 181 | }); 182 | } 183 | 184 | function go() { 185 | auth(config.beam, beam) 186 | .then(res => { 187 | channelId = res.channel.id; 188 | }) 189 | .then(() => goInteractive(config.version, config.code)) 190 | .then(() => getControls(config.version, config.code)) 191 | .then(controls => validateControls(controls)) 192 | .then(controls => { 193 | state = createState(controls); 194 | return state; 195 | }).then(() => launchInteractive(beam, channelId)) 196 | .catch(err => { 197 | if (err.message !== undefined && err.message.body !== undefined) { 198 | console.log(err); 199 | } else { 200 | throw err; 201 | } 202 | }); 203 | } 204 | 205 | setup(); 206 | -------------------------------------------------------------------------------- /lib/Config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const errors = require('./errors.js'); 3 | function Config(file) { 4 | this.beam = {}; 5 | this.blocks = {}; 6 | this.remap = false; 7 | this.remapTable = {}; 8 | this.tactileThreshold = 0.1; 9 | this.handler = 'robotjs'; 10 | this.version = ''; 11 | this.shareCode = ''; 12 | this.widgets = true; 13 | this.windowBorder = { 14 | x: { 15 | max: 0, 16 | min: 0 17 | }, 18 | y: { 19 | max: 0, 20 | min: 0 21 | } 22 | }; 23 | 24 | this.consensus = 'democracy'; 25 | this.joyStickConsensus = 'democracy'; 26 | this.mouseEnabled = false; 27 | this.windowTarget = ''; 28 | 29 | this.mouseSource = 'joystick'; 30 | 31 | if (file) { 32 | file = file.replace('\\', '/'); 33 | } else { 34 | console.warn('using default config file'); 35 | file = './config/default.json'; 36 | } 37 | 38 | let config; 39 | try { 40 | // Load the config values in from the json 41 | config = require(`.${file}`); 42 | if (typeof config.password === "string" || (config.beam && typeof config.beam.password === "string")) { 43 | throw new errors.AuthDataError(); 44 | } 45 | Object.assign(this, config); 46 | } catch (e) { 47 | config = {}; 48 | if (e.code === 'MODULE_NOT_FOUND') { 49 | throw new Error(`Cannot find ${file}`); 50 | } 51 | if (e instanceof errors.BeamKeyboardError) { 52 | throw e; 53 | } 54 | throw new errors.JSONValidationError(); 55 | } 56 | let authConfig; 57 | try { 58 | authConfig = require('../config/auth.json'); 59 | if (authConfig) { 60 | Object.assign(this.beam, authConfig); 61 | } 62 | } catch (e) { 63 | authConfig = null; 64 | if (e.code === 'MODULE_NOT_FOUND') { 65 | throw new Error('Missing auth.json cannot proceed'); 66 | } 67 | throw e; 68 | } 69 | } 70 | 71 | module.exports = Config; 72 | -------------------------------------------------------------------------------- /lib/ControlsProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | const nodeUtil = require('util'); 4 | const keycode = require('keycode'); 5 | 6 | const Packets = require('beam-interactive-node/dist/robot/packets').default; 7 | 8 | const util = require('./util'); 9 | 10 | function ControlsProcessor(config) { 11 | this.config = config; 12 | 13 | this.joyStickConsensus = require(`./consensus/mouse/${config.joyStickConsensus}`); 14 | this.screenConsensus = require(`./consensus/screen/democracy.js`); 15 | 16 | this.consensus = require(`./consensus/keyboard/${config.consensus}`); 17 | 18 | this.keyboardHandler = require(`./handlers/${config.handler}`); 19 | this.windowHandler = null; 20 | this.mouseHandler = null; 21 | 22 | if (this.config.mouseEnabled) { 23 | // Mouse requires robot-js atm 24 | this.mouseHandler = require('./handlers/mouse/robot-js'); 25 | 26 | if (this.config.windowTarget) { 27 | this.windowHandler = require('./handlers/window/robot-js'); 28 | 29 | this.constrainWindow(this.config.windowTarget, this.config.windowBorder); 30 | } 31 | } 32 | EventEmitter.call(this); 33 | } 34 | 35 | nodeUtil.inherits(ControlsProcessor, EventEmitter); 36 | 37 | ControlsProcessor.prototype.constrainWindow = function (title, border) { 38 | if (this.windowHandler !== null) { 39 | const details = this.windowHandler.getWindowInfo(title); 40 | if (details) { 41 | this.mouseHandler.constrainToWindow(details, border); 42 | } 43 | } 44 | }; 45 | 46 | /** 47 | * Given a Key name "W" or a keycode 87 transform that key using the 48 | * remapping table from the config. 49 | * @param {Number|String} code 50 | * @return {String} the keyname of the remapped key 51 | */ 52 | ControlsProcessor.prototype.remapKey = function (code) { 53 | let stringCode; 54 | if (typeof code === 'number') { 55 | stringCode = keycode(code).toUpperCase(); 56 | } else { 57 | stringCode = code; 58 | } 59 | 60 | if (this.config.remap[stringCode]) { 61 | return this.config.remap[stringCode].toLowerCase(); 62 | } 63 | return code; 64 | }; 65 | 66 | ControlsProcessor.prototype.clearAllKeys = function () { 67 | // this.setKeys(Object.keys(map), false, config.remap); 68 | }; 69 | 70 | ControlsProcessor.prototype.process = function (report, controlState) { 71 | const result = { 72 | state: 'default', 73 | tactile: [], 74 | joystick: [] 75 | }; 76 | if (report.screen && report.screen.length) { 77 | result.screen = report.screen.map(screen => this.processScreen(controlState, report.users, screen)) 78 | .filter(value => value !== undefined); 79 | } 80 | if (report.tactile && report.tactile.length) { 81 | result.tactile = report.tactile.map(tactile => this.processTactile(controlState, report.users, tactile)) 82 | .filter(value => value !== undefined); 83 | } 84 | if (report.joystick && report.joystick.length) { 85 | result.joystick = report.joystick.map(joystick => this.processJoyStick(controlState, report.users, joystick)) 86 | .filter(value => value !== undefined); 87 | } 88 | if (result.tactile.length || result.joystick.length) { 89 | this.emit('changed', report, controlState); 90 | } 91 | return result; 92 | }; 93 | ControlsProcessor.prototype.processTactile = function (controlState, users, tactileState) { 94 | const control = controlState.getTactileById(tactileState.id); 95 | let decision = this.consensus(tactileState, users, this.config); 96 | let changed = false; 97 | decision = this.checkBlocks(tactileState, decision, controlState); 98 | if (!decision || decision.action === null) { 99 | return; 100 | } 101 | if (control.action !== decision.action) { 102 | changed = true; 103 | if (control.isMouseClick() && this.config.mouseEnabled) { 104 | this.handleClick(control.label.toLowerCase(), decision.action); 105 | } else { 106 | this.setKey(control.name, decision.action); 107 | } 108 | control.action = decision.action; 109 | 110 | if (control.action) { 111 | // This is bad 112 | decision.cooldown = control.cooldown; 113 | } else { 114 | decision.cooldown = 0; 115 | } 116 | } 117 | if (decision.progress !== control.progress || changed) { 118 | changed = true; 119 | control.progress = decision.progress; 120 | } 121 | 122 | // Here we only send a progress update if something has changed. be it the progress 123 | if (changed) { 124 | return this.createProgressForKey(control, decision); 125 | } 126 | return; 127 | }; 128 | 129 | ControlsProcessor.prototype.processScreen = function (controlState, users, screenState) { 130 | if (!this.config.mouseEnabled || this.config.mouseSource !== 'screen') { 131 | return; 132 | } 133 | const control = screenState; 134 | const result = this.screenConsensus(screenState, users, this.config, controlState); 135 | if (result) { 136 | this.mouseHandler.relativeConstrainedMove(result.x, result.y); 137 | return this.createProgressForScreen(control, result); 138 | } 139 | }; 140 | 141 | ControlsProcessor.prototype.processJoyStick = function (controlState, users, joyStickState) { 142 | if (!this.config.mouseEnabled || this.config.mouseSource !== 'joystick') { 143 | return undefined; 144 | } 145 | const control = controlState.getJoyStickById(joyStickState.id); 146 | const result = this.joyStickConsensus(joyStickState, users, this.config, controlState); 147 | if (result) { 148 | // this.mouseHandler.moveTo(result.x, result.y); 149 | return this.createProgressForJoyStick(control, result); 150 | } 151 | }; 152 | 153 | /** 154 | * Given a tactile state, an in progress decision and a set of 2 paired keys 155 | * Block the current key from being pushed if its paired key is down. 156 | * @param {Object} keyState State to check 157 | * @param {Object} decision Decision in progress 158 | * @param {String} a The first key in the pair, the one to block 159 | * @param {String} b The second key in the pair, the one to check for 160 | * @return {Object} The updated decision 161 | */ 162 | ControlsProcessor.prototype.checkBlock = function (stateA, stateB, decision) { 163 | if (stateB && stateB.action) { 164 | decision.action = false; 165 | decision.progress = 0; 166 | } 167 | return decision; 168 | }; 169 | 170 | /** 171 | * Give a tactile state loop through the blocks as defined in the config, 172 | * working out if this current decision should be blocked 173 | * @param {Object} keyState State to check 174 | * @param {Object} decision Current decision in progress 175 | * @return {Object} 176 | */ 177 | ControlsProcessor.prototype.checkBlocks = function (keyState, decision, state) { 178 | Object.keys(this.config.blocks).forEach(blockA => { 179 | if (keyState.label.toLowerCase() !== blockA.toLowerCase()) { 180 | return; 181 | } 182 | decision = this.checkBlock(keyState, state.getTactileByLabel(this.config.blocks[blockA]), decision); 183 | }); 184 | return decision; 185 | }; 186 | /** 187 | * Given a key name set it to the apropriate status 188 | * @param {String} keyName The key name, "W" and not 87 189 | * @param {Boolean} status true to push the key, false to release 190 | */ 191 | ControlsProcessor.prototype.setKey = function (keyName, status) { 192 | // Beam reports back keycodes, convert them to keynames, which our handlers accept 193 | if (typeof keyName === 'number') { 194 | console.log('warning setting by number'); 195 | keyName = keycode(keyName); 196 | } 197 | 198 | // Something in remapping or handling sometimes makes this undefined 199 | // It causes an error to proceed so we'll stop here 200 | if (!keyName) { 201 | return; 202 | } 203 | 204 | if (status) { 205 | this.keyboardHandler.press(keyName.toUpperCase()); 206 | } else { 207 | this.keyboardHandler.release(keyName.toUpperCase()); 208 | } 209 | }; 210 | 211 | ControlsProcessor.prototype.handleClick = function (button, action) { 212 | if (button.search('left') !== -1 || button.search('draw') !== -1) { 213 | this.mouseHandler.leftHold(action); 214 | return; 215 | } 216 | if (button.search('right') !== -1) { 217 | this.mouseHandler.rightHold(action); 218 | return; 219 | } 220 | }; 221 | 222 | /** 223 | * Given a tactile from a Tetris report, generate a ProgressUpdate packet 224 | * to be sent back to Tetris 225 | * @param {Object} keyObj The tactile from the report 226 | * @param {Object} result The decision from the decision maker process 227 | * @return {Object} The tactile progress update to be sent back to tetris 228 | */ 229 | ControlsProcessor.prototype.createProgressForKey = function (keyObj, result) { 230 | return new Packets.ProgressUpdate.TactileUpdate({ 231 | id: keyObj.id, 232 | cooldown: keyObj.cooldown, 233 | fired: result.action, 234 | progress: result.progress 235 | }); 236 | }; 237 | 238 | ControlsProcessor.prototype.createProgressForJoyStick = function (state, result) { 239 | if (result) { 240 | return new Packets.ProgressUpdate.JoystickUpdate({ 241 | id: state.id, 242 | angle: result.angle, 243 | intensity: result.intensity 244 | }); 245 | } 246 | }; 247 | 248 | ControlsProcessor.prototype.createProgressForScreen = function (state, result) { 249 | 250 | }; 251 | 252 | ControlsProcessor.prototype.constrainMouse = function (mouseBounds) { 253 | this.mouseHandler.constrainMouse(mouseBounds); 254 | }; 255 | 256 | ControlsProcessor.prototype.clearKeys = function (keysToClear) { 257 | util.convertToArray(keysToClear) 258 | .forEach(tactile => { 259 | if (tactile.action) { 260 | tactile.clear(); 261 | this.setKey(tactile.name, false); 262 | } 263 | }); 264 | }; 265 | 266 | module.exports = ControlsProcessor; 267 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | const errors = require('./errors.js'); 3 | 4 | module.exports = function (config, beam) { 5 | if (config.password && config.token) { 6 | throw new errors.ConfusingAuthError(); 7 | } 8 | 9 | if (config.token) { 10 | return oAuth(config, beam); 11 | } 12 | if (config.username && config.password) { 13 | return password(config, beam); 14 | } 15 | throw new errors.AuthError('Missing Authentication'); 16 | }; 17 | 18 | function oAuth(config, beam) { 19 | beam.use('oauth', { 20 | tokens: { 21 | access: config.token, 22 | expires: Date.now() + (365 * 24 * 60 * 60 * 1000) 23 | } 24 | }); 25 | return beam.request('GET', `users/current`).then(res=> res.body); 26 | } 27 | 28 | function password(config, beam) { 29 | console.warn('Consider using an OAuth Token for security, interactive:robot:self and channel:update:self scopes are required'); 30 | return beam.use('password', config).attempt().then(res=> res.body); 31 | } 32 | -------------------------------------------------------------------------------- /lib/consensus/keyboard/anarchy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Given a report, workout what should happen to the key. 4 | * @param {Object} keyState the internal state 5 | * @return {Boolean} true to push AND HOLD the button, false to let go. null to do nothing. 6 | */ 7 | const utils = require('../../util.js'); 8 | module.exports = function (keyState, users) { 9 | const decision = { 10 | action: false, 11 | progress: 0 12 | }; 13 | if ((keyState.pressFrequency - keyState.releaseFrequency) > 0) { 14 | decision.action = true; 15 | } else { 16 | decision.action = false; 17 | } 18 | 19 | decision.progress = utils.calculateFocus(keyState.pressFrequency, users.quorum); 20 | 21 | return decision; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/consensus/keyboard/democracy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Given a report, workout what should happen to the key. 4 | * @param {Object} keyState the internal state 5 | * @return {Boolean} true to push AND HOLD the button, false to let go. null to do nothing. 6 | */ 7 | module.exports = function (keyState, users, config) { 8 | const decision = { 9 | action: false, 10 | progress: 0 11 | }; 12 | 13 | decision.progress = Math.min(keyState.percentHolding, 1); 14 | 15 | if (keyState.percentHolding >= config.tactileThreshold) { 16 | decision.action = true; 17 | } 18 | 19 | if (keyState.percentReleasing >= config.tactileThreshold) { 20 | decision.action = false; 21 | } 22 | return decision; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/consensus/mouse/democracy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Mostly borrowed from https://github.com/WatchBeam/agar-node-robot/blob/master/index.js 3 | const ANGLE_OFFSET = Math.PI / 2; 4 | const multiplier = 20; 5 | function clamp(val, min, max) { 6 | if (isNaN(val)) { 7 | return 0; 8 | } 9 | if (val < min) { 10 | return min; 11 | } 12 | if (val > max) { 13 | return max; 14 | } 15 | return val; 16 | } 17 | 18 | function formula(value, multiplier, intensity) { 19 | return value * (multiplier * intensity); 20 | } 21 | 22 | function intensity(x, y) { 23 | return clamp(Math.sqrt(x * x + y * y), 0, 1); 24 | } 25 | 26 | module.exports = function (joyStickState) { 27 | const result = {}; 28 | if (joyStickState.coordMean && joyStickState.coordMean.x) { 29 | result.x = joyStickState.coordMean.x; 30 | result.y = joyStickState.coordMean.y; 31 | result.intensity = intensity(result.x, result.y); 32 | result.x = formula(result.x, multiplier, result.intensity); 33 | result.y = formula(result.y, multiplier, result.intensity); 34 | const angle = Math.atan2(result.y, result.x) + ANGLE_OFFSET; 35 | result.angle = isNaN (angle) ? 0 : angle; 36 | 37 | return result; 38 | } 39 | return undefined; 40 | }; 41 | -------------------------------------------------------------------------------- /lib/consensus/screen/democracy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Screen consensuseses (consensusi) are hard. 4 | * 5 | * We get a new coordinate for this report and a numeric number of clicks 6 | * 7 | * @param {[type]} controlState [description] 8 | * @return {[type]} [description] 9 | */ 10 | module.exports = function (controlState) { 11 | const result = {}; 12 | 13 | if (controlState.coordMean && controlState.coordMean.x && !isNaN(controlState.coordMean.x) && !isNaN(controlState.coordMean.y)) { 14 | result.x = controlState.coordMean.x; 15 | result.y = controlState.coordMean.y; 16 | return result; 17 | } 18 | return undefined; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/constraints/window.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | createConstraints(windowDetails, border) { 4 | if (!windowDetails.title) { 5 | return; 6 | } 7 | return { 8 | x: { 9 | min: windowDetails.x + border.x.min, 10 | max: (windowDetails.x + border.x.min) + windowDetails.width - border.x.max 11 | }, 12 | y: { 13 | min: windowDetails.y + border.y.min, 14 | max: (windowDetails.y + border.y.min) + windowDetails.height - border.y.max 15 | } 16 | }; 17 | }, 18 | inBounds(constraints, current) { 19 | if (current.x > constraints.x.max || current.x < constraints.x.min) { 20 | return false; 21 | } 22 | if (current.y > constraints.y.max || current.y < constraints.y.min) { 23 | return false; 24 | } 25 | return true; 26 | }, 27 | clamp(val, min, max) { 28 | if (val < min) { 29 | return min; 30 | } 31 | if (val > max) { 32 | return max; 33 | } 34 | return val; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const errors = module.exports = {}; 3 | 4 | errors.BeamKeyboardError = 5 | class UserError extends Error { 6 | constructor(msg) { 7 | super(msg); 8 | this.name = this.constructor.name; 9 | } 10 | }; 11 | 12 | errors.AuthDataError = 13 | class AuthDataError extends errors.BeamKeyboardError { 14 | constructor() { 15 | super('Move your authentication data to auth.json'); 16 | } 17 | }; 18 | 19 | errors.ConfusingAuthError = 20 | class ConfusingAuthError extends errors.BeamKeyboardError { 21 | constructor() { 22 | super('Use password OR OAuth Token, not both. That is very confusing'); 23 | } 24 | }; 25 | 26 | errors.AuthError = 27 | class AuthError extends errors.BeamKeyboardError {}; 28 | 29 | errors.JSONValidationError = 30 | class JSONValidationError extends errors.BeamKeyboardError { 31 | constructor() { 32 | super('Your config file is incorrectly formatted. It must be valid JSON'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/handlers/kbm-robot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const robot = require('kbm-robot'); 3 | const keycode = require('keycode'); 4 | 5 | /* 6 | The original target application of this project won't listen to any other module 7 | other than this one, I have no idea why. That's probably the only reason this 8 | handler even exists. 9 | */ 10 | robot.startJar(); 11 | 12 | // Restart the jar ever 15 minutes 13 | setInterval(() => { 14 | robot.stopJar(); 15 | robot.startJar(); 16 | }, 1000 * 60 * 15); 17 | 18 | // Moan about using this 19 | console.log('WARNING: This handler may freeze inputs after 30 mins/ 1 hour of use. ' + 20 | 'We restart some of the module interals every 15 minutes to try and cope ' + 21 | 'But use of this handler is not reccomended'); 22 | const arrows = ['up', 'down', 'left', 'right']; 23 | function convert(key) { 24 | if (typeof key === 'number') { 25 | key = keycode(key); 26 | } 27 | if (arrows.indexOf(key) !== -1) { 28 | return key.toUpperCase(); 29 | } 30 | return key; 31 | } 32 | 33 | module.exports = { 34 | press(key) { 35 | try { 36 | robot.press(convert(key)).go(); 37 | } catch (e) { 38 | 39 | } 40 | }, 41 | release(key) { 42 | try { 43 | robot.release(convert(key)).go(); 44 | } catch (e) { 45 | 46 | } 47 | }, 48 | tap(key) { 49 | try { 50 | robot.press(convert(key)).sleep(100).release(convert(key)).go(); 51 | } catch (e) { 52 | 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /lib/handlers/keyboardz.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const keycode = require('keycode'); 3 | const keyboardz = require('keyboardz'); 4 | 5 | function isNumeric(n) { 6 | return !isNaN(parseFloat(n)) && isFinite(n); 7 | } 8 | 9 | function convert(key) { 10 | if (typeof key === 'number') { 11 | key = keycode(key); 12 | } 13 | // KEYBOARDS requires N1 for 1, N2 for 2 etc 14 | if (isNumeric(key)) { 15 | key = `N${key}`; 16 | } 17 | return key; 18 | } 19 | 20 | module.exports = { 21 | press(key) { 22 | keyboardz.holdKey(convert(key)); 23 | }, 24 | release(key) { 25 | keyboardz.releaseKey(convert(key)); 26 | }, 27 | tap(/* key*/) { 28 | // robot.keyTap(convert(key)); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/handlers/mouse/robot-js.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const robot = require('robot-js'); 3 | const windowConstraints = require('../../constraints/window.js'); 4 | 5 | /* eslint-disable babel/new-cap */ 6 | const mouse = robot.Mouse(); 7 | /* eslint-enable babel/new-cap */ 8 | 9 | let constraints = {}; 10 | const mouseExport = { 11 | constrainToWindow(windowDetails, border) { 12 | constraints = windowConstraints.createConstraints(windowDetails, border); 13 | }, 14 | leftClick() { 15 | mouseExport.checkBounds(); 16 | mouse.click(robot.BUTTON_LEFT); 17 | }, 18 | rightClick() { 19 | mouseExport.checkBounds(); 20 | mouse.click(robot.BUTTON_RIGHT); 21 | }, 22 | leftHold(action) { 23 | mouseExport.checkBounds(); 24 | mouseExport.hold(robot.BUTTON_LEFT, action); 25 | }, 26 | rightHold(action) { 27 | mouseExport.checkBounds(); 28 | mouseExport.hold(robot.BUTTON_RIGHT, action); 29 | }, 30 | hold(button, action) { 31 | if (action) { 32 | mouse.press(button); 33 | } else { 34 | mouse.release(button); 35 | } 36 | }, 37 | checkBounds() { 38 | const current = robot.Mouse.getPos(); 39 | if (!windowConstraints.inBounds(constraints, current)) { 40 | mouseExport.constrainedMove(current.x, current.y); 41 | } 42 | }, 43 | constrainedMove(x, y) { 44 | const xC = constraints.x; 45 | const yC = constraints.y; 46 | mouseExport.setPos(windowConstraints.clamp(x, xC.min, xC.max), windowConstraints.clamp(y, yC.min, yC.max)); 47 | }, 48 | moveTo(x, y) { 49 | const current = robot.getMousePos(); 50 | if (constraints) { 51 | mouseExport.constrainedMove(current.x + x, current.y + y); 52 | } else { 53 | mouseExport.setPos(current.x + x, current.y + y); 54 | } 55 | }, 56 | relativeConstrainedMove(x, y) { 57 | const xFinal = constraints.x.min + mouseExport.localToGlobal(x, constraints.x.max); 58 | const yFinal = constraints.y.min + mouseExport.localToGlobal(y, constraints.y.max); 59 | if (constraints && constraints.x && constraints.x.min) { 60 | mouseExport.constrainedMove(xFinal, yFinal); 61 | } else { 62 | mouseExport.moveTo(xFinal, yFinal); 63 | } 64 | }, 65 | localToGlobal(current, max) { 66 | return Math.abs(max * current); 67 | }, 68 | setPos(x, y) { 69 | return robot.Mouse.setPos(x, y); 70 | } 71 | }; 72 | 73 | module.exports = mouseExport; 74 | -------------------------------------------------------------------------------- /lib/handlers/mouse/robotjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const robot = require('robotjs'); 3 | const windowConstraints = require('../../contraints/window.js'); 4 | 5 | let constraints = {}; 6 | const mouse = { 7 | constrainToWindow(windowDetails) { 8 | constraints = windowConstraints.createConstraints(windowDetails); 9 | }, 10 | leftClick() { 11 | mouse.checkBounds(); 12 | robot.mouseToggle('down', 'left'); 13 | setTimeout(() => { 14 | robot.mouseToggle('up', 'left'); 15 | }, 200); 16 | }, 17 | rightClick() { 18 | mouse.checkBounds(); 19 | robot.mouseToggle('down', 'right'); 20 | setTimeout(() => { 21 | robot.mouseToggle('up', 'right'); 22 | }, 200); 23 | }, 24 | hold(button, action) { 25 | robot.mouseToggle(action ? 'down' : 'up', button); 26 | }, 27 | leftHold(action) { 28 | mouse.checkBounds(); 29 | mouse.hold('left', action); 30 | }, 31 | rightHold(action) { 32 | mouse.checkBounds(); 33 | mouse.hold('right', action); 34 | }, 35 | checkBounds() { 36 | const current = robot.getMousePos(); 37 | if (!windowConstraints.inBounds(constraints, current)) { 38 | mouse.constrainedMove(current.x, current.y); 39 | } 40 | }, 41 | constrainedMove(x, y) { 42 | const xC = constraints.x; 43 | const yC = constraints.y; 44 | robot.moveMouseSmooth(windowConstraints.clamp(x, xC.min, xC.max), windowConstraints.clamp(y, yC.min, yC.max)); 45 | }, 46 | moveTo(x, y) { 47 | const current = robot.getMousePos(); 48 | if (constraints && constraints.x && constraints.x.min) { 49 | mouse.constrainedMove(current.x + x, current.y + y); 50 | } else { 51 | robot.moveMouseSmooth(current.x + x, current.y + y); 52 | } 53 | }, 54 | relativeConstrainedMove(x, y) { 55 | const xFinal = constraints.x.min + mouse.localToGlobal(x, constraints.width); 56 | const yFinal = constraints.y.min + mouse.localToGlobal(y, constraints.height); 57 | if (constraints && constraints.x && constraints.x.min) { 58 | robot.constrainedMove(xFinal, yFinal); 59 | } else { 60 | robot.moveMouseSmooth(xFinal, yFinal); 61 | } 62 | }, 63 | localToGlobal(current, max) { 64 | return Math.min(0, current * max); 65 | } 66 | }; 67 | 68 | module.exports = mouse; 69 | -------------------------------------------------------------------------------- /lib/handlers/robot-js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const robot = require('robot-js'); 3 | 4 | const keyboard = robot.Keyboard(); 5 | const keycode = require('keycode'); 6 | 7 | const replacementMap = { 8 | esc: 'escape' 9 | }; 10 | 11 | function convert(key) { 12 | if (typeof key === 'string') { 13 | key = keycode(key); 14 | } 15 | return key; 16 | } 17 | 18 | module.exports = { 19 | press(key) { 20 | keyboard.press(convert(key)); 21 | }, 22 | release(key) { 23 | keyboard.release(convert(key)); 24 | }, 25 | tap(key) { 26 | keyboard.click(convert(key)); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/handlers/robotjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const robot = require('robotjs'); 3 | const keycode = require('keycode'); 4 | 5 | const replacementMap = { 6 | esc: 'escape' 7 | }; 8 | 9 | function convert(key) { 10 | if (typeof key === 'number') { 11 | key = keycode(key); 12 | } 13 | 14 | // Robot js complains about UP but not W, it likes lowercase though so we'll stick with that. 15 | key = key.toLowerCase(); 16 | if (replacementMap[key]) { 17 | return replacementMap[key]; 18 | } 19 | return key; 20 | } 21 | 22 | module.exports = { 23 | press(key) { 24 | robot.keyToggle(convert(key), 'down'); 25 | }, 26 | release(key) { 27 | robot.keyToggle(convert(key), 'up'); 28 | }, 29 | tap(key) { 30 | robot.keyTap(convert(key)); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /lib/handlers/screen/robotjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const robot = require('robotjs'); 3 | 4 | let constraints = {}; 5 | const borderX = 10; 6 | const borderY = 5; 7 | const mouse = { 8 | constrainToWindow(windowDetails) { 9 | if (!windowDetails.title) { 10 | return; 11 | } 12 | constraints = { 13 | x: { 14 | min: windowDetails.x + borderX, 15 | max: (windowDetails.x + borderX) + windowDetails.width - (borderX + 15) 16 | }, 17 | y: { 18 | min: windowDetails.y + 30, 19 | max: (windowDetails.y + borderY) + windowDetails.height - (borderY + 20) 20 | } 21 | }; 22 | }, 23 | leftClick() { 24 | mouse.checkBounds(); 25 | robot.mouseToggle('down', 'left'); 26 | setTimeout(() => { 27 | robot.mouseToggle('up', 'left'); 28 | }, 200); 29 | }, 30 | rightClick() { 31 | mouse.checkBounds(); 32 | robot.mouseToggle('down', 'right'); 33 | setTimeout(() => { 34 | robot.mouseToggle('up', 'right'); 35 | }, 200); 36 | }, 37 | clamp(val, min, max) { 38 | if (val < min) { 39 | return min; 40 | } 41 | if (val > max) { 42 | return max; 43 | } 44 | return val; 45 | }, 46 | checkBounds() { 47 | const current = robot.getMousePos(); 48 | if (!mouse.inBounds()) { 49 | mouse.constrainedMove(current.x, current.y); 50 | } 51 | }, 52 | inBounds() { 53 | const current = robot.getMousePos(); 54 | if (current.x > constraints.x.max || current.x < constraints.x.min) { 55 | return false; 56 | } 57 | if (current.y > constraints.y.max || current.y < constraints.y.min) { 58 | return false; 59 | } 60 | return true; 61 | }, 62 | constrainedMove(x, y) { 63 | const xC = constraints.x; 64 | const yC = constraints.y; 65 | robot.moveMouseSmooth(mouse.clamp(x, xC.min, xC.max), mouse.clamp(y, yC.min, yC.max)); 66 | }, 67 | moveTo(x, y) { 68 | const current = robot.getMousePos(); 69 | if (constraints && constraints.x && constraints.x.min) { 70 | mouse.constrainedMove(current.x + x, current.y + y); 71 | } else { 72 | robot.moveMouseSmooth(current.x + x, current.y + y); 73 | } 74 | } 75 | }; 76 | 77 | module.exports = mouse; 78 | -------------------------------------------------------------------------------- /lib/handlers/window/robot-js.js: -------------------------------------------------------------------------------- 1 | 'use sctrict'; 2 | const robot = require('robot-js'); 3 | const window = robot.Window; 4 | module.exports.getWindowInfo = function (title) { 5 | const list = window.getList(title); 6 | if (!list.length) { 7 | throw new Error(`Cannot find window: ${title}`); 8 | } 9 | const result = {}; 10 | const target = list[0]; 11 | const bounds = target.getBounds(); 12 | result.x = bounds.x; 13 | result.y = bounds.y; 14 | result.height = bounds.h; 15 | result.width = bounds.w; 16 | result.title = target.getTitle(); 17 | 18 | return result; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/handlers/window/windoze.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const windoze = require('windoze'); 3 | 4 | module.exports.getWindowInfo = function (title) { 5 | const details = windoze.getWindowDetailsByTitle(title, true); 6 | if (!details) { 7 | throw new Error(`Cannot find window: ${title}`); 8 | } 9 | return details; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/reconnector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let attempts = 1; 3 | 4 | function generateInterval(attempts) { 5 | return Math.min(30, (Math.pow(2, attempts) - 1)) * 1000; 6 | } 7 | 8 | module.exports = function (obj, reconnectMethod) { 9 | obj.on('close', () => { 10 | const delay = generateInterval(attempts); 11 | console.log(`Disconnected retrying after ${delay}`); 12 | setTimeout(() => { 13 | console.log('Reconnecting :D'); 14 | attempts += 1; 15 | reconnectMethod(); 16 | }, delay); 17 | }); 18 | obj.on('connect', () => { 19 | attempts = 1; 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/state/ControlState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Tactile = require('./Tactile'); 3 | const JoyStick = require('./JoyStick'); 4 | const util = require('../util'); 5 | 6 | /** 7 | * Control state is an easily queryable copy of the controls object 8 | * that beam provides from the controls editor. 9 | */ 10 | function ControlState(controls) { 11 | this.tactiles = []; 12 | this.joysticks = []; 13 | this.users = {}; 14 | this.qgram = []; 15 | this.status = ''; 16 | this.controlControlState = 'default'; 17 | 18 | controls.joysticks.forEach(this.createJoyStick.bind(this)); 19 | controls.tactiles.forEach(this.createTactile.bind(this)); 20 | } 21 | 22 | ControlState.prototype.createTactile = function (tactileControl) { 23 | const tactile = new Tactile(tactileControl); 24 | this.tactiles.push(tactile); 25 | return tactile; 26 | }; 27 | 28 | ControlState.prototype.createJoyStick = function (joystickControl) { 29 | const joystickObj = new JoyStick(joystickControl); 30 | this.joysticks.push(joystickObj); 31 | return joystickObj; 32 | }; 33 | 34 | ControlState.prototype.getTactile = function (keyName) { 35 | if (this.tactiles[keyName]) { 36 | return ControlState.tactiles[keyName]; 37 | } 38 | return this.createTactile(keyName); 39 | }; 40 | 41 | ControlState.prototype.getTactileByLabel = function (label) { 42 | const lowercaseLabel = label.toLowerCase(); 43 | return util.convertToArray(this.tactiles).find(tactile => { 44 | return tactile.label.toLowerCase() === lowercaseLabel; 45 | }); 46 | }; 47 | 48 | ControlState.prototype.getTactileById = function (id) { 49 | return this.tactiles.find(tactile => tactile.id === id); 50 | }; 51 | 52 | ControlState.prototype.getJoyStickById = function (id) { 53 | return this.joysticks.find(tactile => tactile.id === id); 54 | }; 55 | 56 | module.exports = ControlState; 57 | -------------------------------------------------------------------------------- /lib/state/JoyStick.js: -------------------------------------------------------------------------------- 1 | function JoyStick(JoyStickControl) { 2 | this.control = JoyStickControl; 3 | this.id = this.control.id; 4 | this.code = this.control.key; 5 | this.label = this.control.text || ''; 6 | this.clear(); 7 | } 8 | 9 | JoyStick.prototype.clear = function () { 10 | this.coordMean = {}; 11 | this.coordStddev = {}; 12 | }; 13 | module.exports = JoyStick; 14 | -------------------------------------------------------------------------------- /lib/state/Tactile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const keycode = require('keycode'); 3 | 4 | function Tactile(tactileControl) { 5 | this.control = tactileControl; 6 | this.cooldown = tactileControl.cooldown.press || 0; 7 | this.id = this.control.id; 8 | this.code = this.control.key; 9 | this.name = keycode(this.control.key); 10 | this.label = this.control.text || ''; 11 | this.percentHolding = 0; 12 | this.percentPushing = 0; 13 | this.percentReleasing = 0; 14 | this.pressFrequency = 0; 15 | this.releaseFrequency = 0; 16 | this.holding = 0; 17 | this.action = false; 18 | this.progress = 0; 19 | this.clear(); 20 | } 21 | 22 | Tactile.prototype.clear = function () { 23 | this.percentHolding = 0; 24 | this.percentPushing = 0; 25 | this.percentReleasing = 0; 26 | this.pressFrequency = 0; 27 | this.releaseFrequency = 0; 28 | this.holding = 0; 29 | this.action = false; 30 | this.progress = 0; 31 | }; 32 | 33 | Tactile.prototype.isMouseClick = function () { 34 | return this.label.toLowerCase().search('click') !== -1; 35 | }; 36 | 37 | module.exports = Tactile; 38 | -------------------------------------------------------------------------------- /lib/state/enhancer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const util = require('../util'); 3 | 4 | module.exports = function (report, controlState) { 5 | Object.freeze(report); 6 | const base = {}; 7 | base.users = Object.assign({}, report.users); 8 | if (report.tactile.length) { 9 | base.tactile = enhanceTactiles(report.tactile, controlState, report.users); 10 | } 11 | base.joystick = report.joystick.slice(0); 12 | base.screen = report.screen; 13 | return base; 14 | }; 15 | 16 | function enhanceTactiles(tactiles, controlState, users) { 17 | return tactiles.map(tactile => enhanceTactile(tactile, controlState, users)); 18 | } 19 | 20 | function enhanceTactile(tactile, controlState, users) { 21 | const enhancements = { 22 | pressFrequency: util.nullToZero(tactile.pressFrequency), 23 | releaseFrequency: util.nullToZero(tactile.releaseFrequency), 24 | holding: util.nullToZero(tactile.holding), 25 | percentHolding: util.percentage(tactile.holding, users.quorum), 26 | percentPushing: util.percentage(tactile.pressFrequency, users.quorum), 27 | percentReleasing: util.percentage(tactile.releaseFrequency, users.quorum) 28 | }; 29 | const control = controlState.getTactileById(tactile.id); 30 | const controlVariables = {}; 31 | if (control) { 32 | controlVariables.label = control.label; 33 | controlVariables.code = control.code; 34 | controlVariables.name = control.name; 35 | } 36 | return Object.assign({}, tactile, enhancements, controlVariables); 37 | } 38 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Convert a null value to a zero value. Protobuf loves sendings nulls 4 | * @param {Number} value A nullable value 5 | * @return {Number} 0 or the original value 6 | */ 7 | nullToZero(value) { 8 | if (value === null) { 9 | return 0; 10 | } 11 | return value; 12 | }, 13 | /** 14 | * Calculate a decimalized percentage of a value 15 | * @param {Number} value 16 | * @param {Number} total 17 | * @return {Number} 18 | */ 19 | percentage(value, total) { 20 | if (total <= 0 || value <= 0) { 21 | return 0; 22 | } 23 | // ABS because negative values do weird things to progress bars 24 | return Math.abs(value / total); 25 | }, 26 | convertToArray(obj) { 27 | if (!obj) { 28 | return []; 29 | } 30 | return Object.keys(obj).map(key => { 31 | return obj[key]; 32 | }); 33 | }, 34 | normalizeJoyStick(obj) { 35 | const defaultJoyStick = { 36 | X: 0, 37 | Y: 0 38 | }; 39 | return Object.assign({}, defaultJoyStick, obj); 40 | }, 41 | /** 42 | * For a given action, Supply how many people are participating in the action 43 | * and the total number of people active on the controls and recieve the percentage 44 | * Focus of the users 45 | * @param {[type]} participating [description] 46 | * @param {[type]} total [description] 47 | * @return {[type]} [description] 48 | */ 49 | calculateFocus(participating, total) { 50 | if (participating === 0 || total === 0) { 51 | return 0; 52 | } 53 | return total / participating; 54 | }, 55 | noop() {} 56 | }; 57 | -------------------------------------------------------------------------------- /lib/widgets/KeyBar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const CLI = require('clui'); 3 | const clc = require('cli-color'); 4 | const gauge = CLI.Gauge; 5 | const Line = CLI.Line; 6 | 7 | module.exports = function (tactile, maxIdLength, max) { 8 | if (!tactile) { 9 | return new Line(); 10 | } 11 | let id = Array(maxIdLength - tactile.id.toString().length).join(0) + tactile.id.toString(); 12 | let name = tactile.label; 13 | if (tactile.name) { 14 | name += ` (${tactile.name.toUpperCase()})`; 15 | } 16 | return new Line() 17 | .padding(2) 18 | .column(id, maxIdLength + 3, [clc.magenta]) 19 | .column(name, 15, [clc.cyan]) 20 | .column(gauge(tactile.percentHolding * 100, max, 20, 60), 25) 21 | .column(gauge(tactile.percentReleasing * 100, max, 20, 60), 25) 22 | .column((tactile.action) ? '▼' : '▲', 2, [(tactile.action) ? clc.green : clc.red]) 23 | .fill(); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/widgets/QGram.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const CLI = require('clui'); 3 | const clc = require('cli-color'); 4 | module.exports = function (input) { 5 | /* eslint-disable babel/new-cap */ 6 | return new CLI.Line() 7 | .padding(2) 8 | .column('QGram', 20, [clc.cyan]) 9 | .column(CLI.Sparkline(input, ''), 80) 10 | .fill(); 11 | /* eslint-enable babel/new-cap */ 12 | }; 13 | -------------------------------------------------------------------------------- /lib/widgets/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const qGram = require('./QGram'); 3 | const keyBar = require('./KeyBar'); 4 | const util = require('../util'); 5 | 6 | const CLI = require('clui'); 7 | const Line = CLI.Line; 8 | const LineBuffer = CLI.LineBuffer; 9 | 10 | function flattenQGram(qgram) { 11 | // replace qgram with just its value 12 | const flattened = qgram.map(qgram => qgram.y); 13 | if (flattened.length < 5) { 14 | while (flattened.length < 5) { 15 | flattened.push(0); 16 | } 17 | } 18 | return flattened; 19 | } 20 | module.exports = function (report) { 21 | const tactileArray = util.convertToArray(report.tactile); 22 | const maxIdLength = Math.floor(Math.log10(Math.abs(tactileArray.length))) + 1; 23 | const blankLine = new Line().fill().output(); 24 | const base = new LineBuffer(); 25 | base.addLine(blankLine); 26 | base.addLine(qGram(flattenQGram(report.users.qgram))); 27 | base.addLine(blankLine); 28 | const labels = new Line() 29 | .padding(2) 30 | .column('ID', maxIdLength + 3) 31 | .column('Key', 20) 32 | .column('Holding %', 25) 33 | .column('Releasing %', 27); 34 | base.addLine(labels); 35 | tactileArray.sort(function(a, b) { return a.id - b.id; }).forEach(tactile => { 36 | base.addLine(keyBar(tactile, maxIdLength, 100)); 37 | }); 38 | const now = new Date().toLocaleString(); 39 | base.addLine(new Line().padding(2).column(`Last Control Update:${now}`, 50).fill()); 40 | base.addLine(blankLine); 41 | base.output(); 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beam-keyboard", 3 | "version": "0.3.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "xo" 8 | }, 9 | "author": "Richard Fox", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ProbablePrime/beam-keyboard.git" 14 | }, 15 | "dependencies": { 16 | "async.cargo": "^0.5.2", 17 | "beam-client-node": "0.8.0", 18 | "beam-interactive-node": "^0.3.2", 19 | "bluebird": "^3.4.1", 20 | "cli-color": "^1.1.0", 21 | "clui": "git://github.com/metaa/clui", 22 | "deep-equal": "^1.0.1", 23 | "keycode": "^2.1.0", 24 | "robot-js": "^1.0.2" 25 | }, 26 | "xo": { 27 | "esnext": true, 28 | "rules": { 29 | "xo/filename-case": 0 30 | } 31 | }, 32 | "devDependencies": { 33 | "xo": "^0.13.0" 34 | } 35 | } 36 | --------------------------------------------------------------------------------