├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── Makefile ├── README.md ├── bin ├── buildCSS.js ├── info.js ├── make.js ├── ng.connect.static.js.patch └── static.js ├── conf ├── http.js ├── loggers.js ├── servernode.js └── sio.js ├── docs └── readme ├── examples └── server.js ├── games └── README.md ├── index.js ├── lib ├── ChannelRegistry.js ├── GameLevel.js ├── GameLoader.js ├── GameMsgGenerator.js ├── GameRouter.js ├── GameServer.js ├── Logger.js ├── ResourceManager.js ├── ServerChannel.js ├── ServerNode.js ├── SocketManager.js ├── rooms │ ├── GameRoom.js │ ├── GarageRoom.js │ ├── HostRoom.js │ ├── RequirementsRoom.js │ ├── Room.js │ └── WaitingRoom.js ├── servers │ ├── AdminServer.js │ └── PlayerServer.js └── sockets │ ├── SocketDirect.js │ └── SocketIo.js ├── log └── README ├── package.json ├── phantomjs └── openurl.js ├── postcss.config.js ├── public ├── images │ ├── .gitignore │ ├── arrow-left.png │ ├── arrow-right.png │ ├── circles-dark.png │ ├── close.png │ ├── close_small.png │ ├── delete-icon.png │ ├── google-group-scaled.png │ ├── info.png │ ├── info.svg │ ├── loading.gif │ ├── maximize.png │ ├── maximize_small.png │ ├── maximize_small2.png │ ├── minimize.png │ ├── minimize_small.png │ ├── newlogo_small.jpg │ ├── no-image.png │ ├── nodegame_logo.png │ ├── nodegame_logo_transparent.png │ └── success-icon.png ├── javascripts │ ├── nodegame-full.js │ └── nodegame-full.min.js ├── lib │ ├── bootstrap │ │ ├── bootstrap.css │ │ ├── bootstrap.js │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.js │ ├── bootstrap5 │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.rtl.css │ │ │ ├── bootstrap-grid.rtl.min.css │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.rtl.css │ │ │ ├── bootstrap-reboot.rtl.min.css │ │ │ ├── bootstrap-utilities.css │ │ │ ├── bootstrap-utilities.min.css │ │ │ ├── bootstrap-utilities.rtl.css │ │ │ ├── bootstrap-utilities.rtl.min.css │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.min.css │ │ │ ├── bootstrap.rtl.css │ │ │ └── bootstrap.rtl.min.css │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.esm.js │ │ │ ├── bootstrap.esm.js.map │ │ │ ├── bootstrap.esm.min.js │ │ │ ├── bootstrap.esm.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ ├── jquery │ │ ├── jquery-1.11.1.min.js │ │ └── jquery.min.js │ └── ua-parser │ │ └── ua-parser.js ├── pages │ ├── accessdenied.htm │ ├── auth.htm │ ├── blank.html │ ├── default.html │ └── testpage.htm ├── sounds │ └── doorbell.ogg └── stylesheets │ ├── fullheight.css │ ├── homepage.css │ ├── nodegame-v3.5.css │ ├── nodegame.css │ ├── nodegame.css.2.17.20 │ ├── nodegame.min.css │ ├── nodegame.old.css │ ├── nodegame.scss │ ├── noscript.css │ ├── reset.css │ ├── widgets.css │ └── window.css ├── ssl └── README.md ├── test ├── test.ChannelRegistry.js ├── test.ServerNode.js └── test.socketio.js └── views ├── error ├── generic.jade ├── layout.jade └── nojs.jade ├── game_template.jade ├── homepage.jade ├── index_simple.jade └── layout.jade /.eslintrc.js: -------------------------------------------------------------------------------- 1 | var OFF = 0, WARN = 1, ERROR = 2; 2 | 3 | module.exports = exports = { 4 | "env": { 5 | "es6": true, 6 | "node": true, 7 | "browser": true 8 | }, 9 | 10 | "extends": "eslint:recommended", 11 | 12 | // "ecmaFeatures": { 13 | // // env=es6 doesn't include modules, which we are using 14 | // "modules": true 15 | // }, 16 | // 17 | 18 | "rules": { 19 | "no-console": "off" 20 | }, 21 | 22 | "globals": { 23 | "W": "writable", 24 | "node": "writable", 25 | "J": "writable", 26 | "JSUS": "writable" 27 | } 28 | 29 | // 30 | // "rules": { 31 | // // Possible Errors (overrides from recommended set) 32 | // "no-extra-parens": ERROR, 33 | // "no-unexpected-multiline": ERROR, 34 | // // All JSDoc comments must be valid 35 | // "valid-jsdoc": [ ERROR, { 36 | // "requireReturn": false, 37 | // "requireReturnDescription": false, 38 | // "requireParamDescription": true, 39 | // "prefer": { 40 | // "return": "returns" 41 | // } 42 | // }], 43 | // 44 | // // Best Practices 45 | // 46 | // // Allowed a getter without setter, but all setters require getters 47 | // "accessor-pairs": [ ERROR, { 48 | // "getWithoutSet": false, 49 | // "setWithoutGet": true 50 | // }], 51 | // "block-scoped-var": WARN, 52 | // "consistent-return": ERROR, 53 | // "curly": ERROR, 54 | // "default-case": WARN, 55 | // // the dot goes with the property when doing multiline 56 | // "dot-location": [ WARN, "property" ], 57 | // "dot-notation": WARN, 58 | // "eqeqeq": [ ERROR, "smart" ], 59 | // "guard-for-in": WARN, 60 | // "no-alert": ERROR, 61 | // "no-caller": ERROR, 62 | // "no-case-declarations": WARN, 63 | // "no-div-regex": WARN, 64 | // "no-else-return": WARN, 65 | // "no-empty-label": WARN, 66 | // "no-empty-pattern": WARN, 67 | // "no-eq-null": WARN, 68 | // "no-eval": ERROR, 69 | // "no-extend-native": ERROR, 70 | // "no-extra-bind": WARN, 71 | // "no-floating-decimal": WARN, 72 | // "no-implicit-coercion": [ WARN, { 73 | // "boolean": true, 74 | // "number": true, 75 | // "string": true 76 | // }], 77 | // "no-implied-eval": ERROR, 78 | // "no-invalid-this": ERROR, 79 | // "no-iterator": ERROR, 80 | // "no-labels": WARN, 81 | // "no-lone-blocks": WARN, 82 | // "no-loop-func": ERROR, 83 | // "no-magic-numbers": WARN, 84 | // "no-multi-spaces": ERROR, 85 | // "no-multi-str": WARN, 86 | // "no-native-reassign": ERROR, 87 | // "no-new-func": ERROR, 88 | // "no-new-wrappers": ERROR, 89 | // "no-new": ERROR, 90 | // "no-octal-escape": ERROR, 91 | // "no-param-reassign": ERROR, 92 | // "no-process-env": WARN, 93 | // "no-proto": ERROR, 94 | // "no-redeclare": ERROR, 95 | // "no-return-assign": ERROR, 96 | // "no-script-url": ERROR, 97 | // "no-self-compare": ERROR, 98 | // "no-throw-literal": ERROR, 99 | // "no-unused-expressions": ERROR, 100 | // "no-useless-call": ERROR, 101 | // "no-useless-concat": ERROR, 102 | // "no-void": WARN, 103 | // // Produce warnings when something is commented as TODO or FIXME 104 | // "no-warning-comments": [ WARN, { 105 | // "terms": [ "TODO", "FIXME" ], 106 | // "location": "start" 107 | // }], 108 | // "no-with": WARN, 109 | // "radix": WARN, 110 | // "vars-on-top": ERROR, 111 | // // Enforces the style of wrapped functions 112 | // "wrap-iife": [ ERROR, "outside" ], 113 | // "yoda": ERROR, 114 | // 115 | // // Strict Mode - for ES6, never use strict. 116 | // "strict": [ ERROR, "never" ], 117 | // 118 | // // Variables 119 | // "init-declarations": [ ERROR, "always" ], 120 | // "no-catch-shadow": WARN, 121 | // "no-delete-var": ERROR, 122 | // "no-label-var": ERROR, 123 | // "no-shadow-restricted-names": ERROR, 124 | // "no-shadow": WARN, 125 | // // We require all vars to be initialized (see init-declarations) 126 | // // If we NEED a var to be initialized to undefined, 127 | // // it needs to be explicit 128 | // "no-undef-init": OFF, 129 | // "no-undef": ERROR, 130 | // "no-undefined": OFF, 131 | // "no-unused-vars": WARN, 132 | // // Disallow hoisting - let & const don't allow hoisting anyhow 133 | // "no-use-before-define": ERROR, 134 | // 135 | // // Node.js and CommonJS 136 | // "callback-return": [ WARN, [ "callback", "next" ]], 137 | // "global-require": ERROR, 138 | // "handle-callback-err": WARN, 139 | // "no-mixed-requires": WARN, 140 | // "no-new-require": ERROR, 141 | // // Use path.concat instead 142 | // "no-path-concat": ERROR, 143 | // "no-process-exit": ERROR, 144 | // "no-restricted-modules": OFF, 145 | // "no-sync": WARN, 146 | // 147 | // // ECMAScript 6 support 148 | // "arrow-body-style": [ ERROR, "always" ], 149 | // "arrow-parens": [ ERROR, "always" ], 150 | // "arrow-spacing": [ ERROR, { "before": true, "after": true }], 151 | // "constructor-super": ERROR, 152 | // "generator-star-spacing": [ ERROR, "before" ], 153 | // "no-arrow-condition": ERROR, 154 | // "no-class-assign": ERROR, 155 | // "no-const-assign": ERROR, 156 | // "no-dupe-class-members": ERROR, 157 | // "no-this-before-super": ERROR, 158 | // "no-var": WARN, 159 | // "object-shorthand": [ WARN, "never" ], 160 | // "prefer-arrow-callback": WARN, 161 | // "prefer-spread": WARN, 162 | // "prefer-template": WARN, 163 | // "require-yield": ERROR, 164 | // 165 | // // Stylistic - everything here is a warning because of style. 166 | // "array-bracket-spacing": [ WARN, "always" ], 167 | // "block-spacing": [ WARN, "always" ], 168 | // "brace-style": [ WARN, "1tbs", { "allowSingleLine": false } ], 169 | // "camelcase": WARN, 170 | // "comma-spacing": [ WARN, { "before": false, "after": true } ], 171 | // "comma-style": [ WARN, "last" ], 172 | // "computed-property-spacing": [ WARN, "never" ], 173 | // "consistent-this": [ WARN, "self" ], 174 | // "eol-last": WARN, 175 | // "func-names": WARN, 176 | // "func-style": [ WARN, "declaration" ], 177 | // "id-length": [ WARN, { "min": 2, "max": 32 } ], 178 | // "indent": [ WARN, 4 ], 179 | // "jsx-quotes": [ WARN, "prefer-double" ], 180 | // // "linebreak-style": [ WARN, "unix" ], 181 | // "lines-around-comment": [ WARN, { "beforeBlockComment": true } ], 182 | // "max-depth": [ WARN, 8 ], 183 | // "max-len": [ WARN, 132 ], 184 | // "max-nested-callbacks": [ WARN, 8 ], 185 | // "max-params": [ WARN, 8 ], 186 | // "new-cap": WARN, 187 | // "new-parens": WARN, 188 | // "no-array-constructor": WARN, 189 | // "no-bitwise": OFF, 190 | // "no-continue": OFF, 191 | // "no-inline-comments": OFF, 192 | // "no-lonely-if": WARN, 193 | // "no-mixed-spaces-and-tabs": WARN, 194 | // "no-multiple-empty-lines": WARN, 195 | // "no-negated-condition": OFF, 196 | // "no-nested-ternary": WARN, 197 | // "no-new-object": WARN, 198 | // "no-plusplus": OFF, 199 | // "no-spaced-func": WARN, 200 | // "no-ternary": OFF, 201 | // "no-trailing-spaces": WARN, 202 | // "no-underscore-dangle": WARN, 203 | // "no-unneeded-ternary": WARN, 204 | // "object-curly-spacing": [ WARN, "always" ], 205 | // "one-var": OFF, 206 | // "operator-assignment": [ WARN, "never" ], 207 | // "operator-linebreak": [ WARN, "after" ], 208 | // "padded-blocks": [ WARN, "never" ], 209 | // "quote-props": [ WARN, "consistent-as-needed" ], 210 | // // "quotes": [ WARN, "single" ], 211 | // "require-jsdoc": [ WARN, { 212 | // "require": { 213 | // "FunctionDeclaration": true, 214 | // "MethodDefinition": true, 215 | // "ClassDeclaration": false 216 | // } 217 | // }], 218 | // "semi-spacing": [ WARN, { "before": false, "after": true }], 219 | // "semi": [ ERROR, "always" ], 220 | // "sort-vars": OFF, 221 | // "space-after-keywords": [ WARN, "always" ], 222 | // "space-before-blocks": [ WARN, "always" ], 223 | // "space-before-function-paren": [ WARN, "never" ], 224 | // "space-before-keywords": [ WARN, "always" ], 225 | // "space-in-parens": [ WARN, "never" ], 226 | // "space-infix-ops": [ WARN, { "int32Hint": true } ], 227 | // "space-return-throw-case": ERROR, 228 | // "space-unary-ops": ERROR, 229 | // "spaced-comment": [ WARN, "always" ], 230 | // "wrap-regex": WARN 231 | // } 232 | }; 233 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | *~ 3 | .project 4 | .settings 5 | node_modules/ 6 | npm-debug.log 7 | .DS_Store 8 | 9 | \#* 10 | .\#* 11 | 12 | *.swp 13 | 14 | ssl/*.key 15 | ssl/*.pem 16 | ssl/*.csr 17 | ssl/cert* 18 | 19 | log/servernode 20 | log/channels 21 | log/channel 22 | log/messages 23 | log/clients 24 | log/client 25 | log/servernode.log 26 | log/channels.log 27 | log/channel.log 28 | log/messages.log 29 | log/clients.log 30 | log/client.log 31 | 32 | *.geany 33 | 34 | Session.vim 35 | 36 | .sass-cache 37 | *.css.map 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 10 5 | - 12 6 | 7 | before_install: 8 | # Get installer script. 9 | - wget https://raw.githubusercontent.com/nodeGame/nodegame/master/bin/nodegame-installer.js 10 | - chmod a+x nodegame-installer.js 11 | 12 | install: 13 | - npm install --only=dev 14 | # --branch v4 15 | - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --yes --no-parent-dir-check 16 | 17 | script: 18 | # Add nodegame-server tests here. 19 | 20 | # Test Ultimatum game. 21 | - cd node_modules/nodegame/games/ultimatum-game 22 | - ./bin/run-standalone-test-v4.sh 23 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # nodegame-server change log 2 | 3 | # 7.0.3 4 | - New build: NDDB computes indexes by default, widgets improvements. 5 | 6 | # 7.0.2 7 | - Minified CSS with cssnano. 8 | 9 | # 7.0.0 10 | - Minified nodegame-full.min.js with terser. 11 | - Upgraded: Winston3, Socket.IO 4, Bootstrap 5 (partial support). 12 | - Dropped: PhantomJS 13 | - REMOTE_DISPATCH option for waiting room. 14 | - JWT Cookies are parsed only if auth or noAuthCookie is on. 15 | - New CSS styles for survey. 16 | - New authorization mode: 'external'. 17 | - GameRoom.computeBonus: showBonus, headerAdd, and prolific options. 18 | 19 | # 6.3.0 20 | - CSS: Bomb widget is adaptive to media query, VisualRound is more compact. 21 | - Fixed renderTemplate bug when auth is on. 22 | 23 | # 6.2.2 24 | - Built with Window support for loaded images. 25 | 26 | # 6.2.0 27 | - WaitRoom dispatch uses ROTATION_OFFSET to decide which treatment starts 28 | with option "treatment_rotate" 29 | - numberOfDispatches is passed as fourth parameter to the CHOSEN_TREATMENT 30 | function. 31 | 32 | # 6.1.1 33 | - Built widgets 6.1.1. 34 | 35 | # 6.1.0 36 | - WaitRoom "DISPATCH_TO_SAME_ROOM" creates one room per treatment. 37 | - Built widgets RiskGauge and Slider fixes. 38 | - Built clients with 'exit' on stage fix. 39 | - Fixed error with loading games with aliases. 40 | - External cards in home page. 41 | - Updated package.json. 42 | - Updated CSS for InfoPanel. 43 | 44 | # 6.0.3 45 | - Fixed broken build. 46 | 47 | # 6.0.2 48 | - Built. 49 | 50 | # 6.0.1 51 | - Built. 52 | 53 | # 6.0.0 54 | - GameRoom: 55 | - updateWin method 56 | - computeBonus new defaults: say, print, dump = true 57 | - computeBonus clients option accepts ids (as well as objects) 58 | - if matcher is truthy resends role and partner information upon reconnect 59 | - GameMsgGenerator: default data is empty object (before null) 60 | - Log messages are displayed 61 | - Channels can be disabled in channel.settings.js 62 | - Default clientsLogInterval increased from 1000 to 10000 63 | - Option port accepted in init 64 | - Updated socket.io and express 65 | 66 | ## 5.8.0 67 | - Channels can be disabled with conf option in channel.settings.js. 68 | 69 | ## 5.7.1 70 | - Built client to fix bug with stage init functions. 71 | 72 | ## 5.7.0 73 | - Info query: added q=waitroom parameter and allowed non q= queries. 74 | - Fixed bot client type loading from levels folder. 75 | - Fixed accidental loading of non-default game in #ServerNode.getDefaultChannel. 76 | - Fixed missing winProperty in #GameRoom.computeBonus() (thanks pull request MarcCote). 77 | - #Servernode.parseSettings no longer retunrs a value and automatically 78 | updates the settings for all levels. 79 | 80 | ## 5.6.7 81 | - Minor. 82 | 83 | ## 5.6.6 84 | - Updated build widgets Slider and ContentBox. 85 | 86 | ## 5.6.5 87 | - Updated build widget ChoiceTable. 88 | 89 | ## 5.6.4 90 | - GameRoom.computeBonus better handles amt vs non-amt parameters. 91 | - GameRoom.computeBonus headerKeys deprecated in favor of new syntax with 92 | arrays. 93 | 94 | ## 5.6.3 95 | - GameRoom.computeBonus has an option to add disconnected players to dump 96 | file; player type is now included by default. 97 | 98 | ## 5.6.2 99 | - Updated build widgets ChoiceTable and ChoiceTableGroup. 100 | 101 | ## 5.6.1 102 | - Updated build widget BackButton. 103 | 104 | ## 5.6.0 105 | - The win property is added by default in every client 106 | - Updated build widgets (RiskGauge) and Client (PlayerList and widget.ref). 107 | 108 | ## 5.5.0 109 | - Updated CSS. 110 | - Fixed bug when logging some error with sysLogger. 111 | - Options client, append, filter for GameRoom.computeBonus. 112 | - New build includes widgets RiskGauge, CustomInputGroup. 113 | 114 | ## 5.4.0 115 | - Fixed homePage bug. 116 | - VisualStage classes. 117 | 118 | ## 5.3.1 119 | - Removed unnecessary warning with default channel --default. 120 | 121 | ## 5.3.0 122 | - Improved error messages. 123 | - Option homePage is automatically disabled when --default is set. 124 | 125 | ## 5.2.0 126 | - Server sends a stringified object with an error to monitor, if an error 127 | occurs likely due to cycles. 128 | - Creates log/ folder inside game root folder. 129 | - Log all connections/disconnections from requirements, waitroom, and each 130 | game room. 131 | - Experimental: levels can have no waitroom. 132 | - Experimental: AdminServer listener to modify live game parameters. 133 | 134 | ## 5.1.0 135 | - CustomWidget in built (updated css) 136 | 137 | ## 5.0.2 138 | - Setup content is not merged for levels, justed mixed in. 139 | - Folders in the games/ directory beginning with a dot are ignored. 140 | 141 | ## 5.0.0 142 | - Waiting room exec mode: WAIT_FOR_DISPATCH 143 | - Several improvements to handle updated Monitor interface 144 | 145 | # 4.2.2 146 | - Fixed bug when spoofing mechanism is enabled. 147 | 148 | # 4.2.1 149 | - Fixed bug introduced in 4.2.0, treatments were not correctly validated when 150 | treatment description was missing. 151 | - Added warning if treatment description is missing. 152 | 153 | ## 4.2.0 154 | - Treatment selection via Waiting Room interface. 155 | 156 | ## 4.1.0 157 | - Anti-spoofing fix. 158 | - Updated CSS removed position:absolute attribute from ng_mainframe that was 159 | causing scrollbar to flicker. 160 | 161 | ## 4.0.2 162 | - Last log sent from client (usually an error) is added to the registry (and sent to monitor). 163 | 164 | ## 4.0.1 165 | - Fixed bug that was failing to send client list to monitor. 166 | 167 | ## 4.0.0 168 | - Home Page. 169 | - Updated Css. 170 | - Improved `ServerChannel.connectBot` with new options. 171 | - Improved internal method traverseDirectory to serve info about files to monitor. 172 | - Nested directory in log/ folder are ignored. 173 | - Settings object is correctly associated with the current treatment and sent to the view's context callback. If a treatment cannot be found, it defaults to "standard" treatment (if found). 174 | - `noAuthCookie` option for games that needs to detect the id (e.g. for views) even when no authorization is in place. 175 | - GameRouter is an object of the Channel and not of the ServerNode. 176 | - Logger updated, slightly change in format. 177 | - Cleanup (removed PageManager). 178 | - Removed some of old make commands: sync, multibuild, clean. 179 | 180 | ## 3.5.7 181 | - Internal cleanup. 182 | - Better error messages for duplicated aliases. 183 | - Preparing for v4 (replyClaimId). 184 | 185 | ## 3.5.6 186 | - Fix bug reconnect in GameRoom with stageLevel = DONE. 187 | - Fix log error in GameRouter. 188 | - Improved HI handshake client-server. 189 | - Garage room has no node instance. 190 | - Throwing an error if missing auth files. 191 | 192 | ## 3.5.5 193 | - Server responds to Auth View of monitor interface. 194 | 195 | ## 3.5.4 196 | - Server responds to updated Memory View of monitor interface. 197 | 198 | ## 3.5.3 199 | - Catching stringify errors (e.g. cycles) when sending game data to monitor. 200 | 201 | ## 3.5.2 202 | - Fixed bug introduced in 3.5 to access monitor interface. 203 | 204 | ## 3.5.1 205 | - Game data/ dir is automatically created if not found. 206 | - The return value ofe game.setup is not cloned, and shared with all client types. 207 | - Credentials and secret for channel are validated. 208 | 209 | ## 3.5 210 | - Removed x-powered by tag. 211 | - Improved methods to create bots. 212 | - Bots can have their ID pre-specified. 213 | 214 | ## 3.2.3 215 | - Improved communication with monitor. 216 | 217 | ## 3.2.2 218 | - WaitingRoom can accept parameter to control which treatment to run. 219 | - AdminServer adds the stage of the logic to the reply to ROOMS_INFO. 220 | 221 | ## 3.2.1 222 | - WaitingRoom dispatching algorithm improved. Settings for pinging times can be specified in conf file. 223 | - Added the GarageRoom to the channel. Now admins are connected to it by default. Monitors do not receive so much traffic now. 224 | - Logging makes sure to stringigy its content before saving it. 225 | - SocketDirect does not stringify/unstringify its messages any more. It just passes the plain object. 226 | 227 | ## 3.1.0 228 | - Implemented policy sameStepReconnectionOnly. 229 | - Custom messages for pausing when min/max/exact handler is fired. 230 | 231 | ## 3.0.3 232 | - Update to follow client's SizeManager. 233 | 234 | ## 3.0.2 235 | - Fixing wrong nodegame-full built. 236 | 237 | ## 3.0.1 238 | - More information served with RESULTS. 239 | - All results can be downloaded as zip file. 240 | - When the handler for the correct number of players is fired, it is checked that the game is paused before calling resume. 241 | 242 | ## 3.0.0 243 | - 02/10/2016 NodeGame Version Upgrade. 244 | 245 | ## 2.14.1 246 | - More WaitingRoom options (ON_DISPATCH, ON_DISPATCHED). 247 | 248 | ## 2.14.0 249 | - Fix bug of multiple connections during pinging in WaitingRoom dispatch. 250 | 251 | ## 2.13.1 252 | - Fix bug to access monitor. 253 | 254 | ## 2.13.0 255 | - Enabled caching of resources in public/ by default (in channel.settings.js option cacheMaxAge). 256 | 257 | ## 2.12.0 258 | - Serving logs list. 259 | - Improved WaitingRoom dispatch. 260 | - Supporting incoming waiting room commands. 261 | 262 | ## 2.11.0 263 | - WaitingRoom's dispatching optimized. 264 | - Added WaitingRoom ON_CLOSE and ON_OPEN callbacks. 265 | 266 | ## 2.10.0 267 | - Logics receive the full clients object (reference to the one in the registry). 268 | - WillBeDone property on reconnect is set only if stage of reconnecting client is not behind current stage. 269 | 270 | ## 2.9.1 271 | - Cleanup. 272 | 273 | ## 2.9.0 274 | - Adapted to new GameStage.compare method in updated nodegame-client. 275 | 276 | ## 2.8.4 277 | - Extending fix in 2.8.2. 278 | 279 | ## 2.8.3 280 | - Fix terminatePhantom. 281 | 282 | ## 2.8.2 283 | - Fixing stripping sid (introduced inm 2.7.0). 284 | 285 | ## 2.8.1 286 | - Hidden files are skipped from the results of the game (data/). 287 | 288 | ## 2.8.0 289 | - AdminServer can serve results of game (data/).. 290 | 291 | ## 2.7.0 292 | - Improved reconnection/disconnection handling. 293 | - Upgraded dependencies: express, socket.io, winston 294 | - Updated SocketIo socket for new version of socket.io 295 | - Security fix: incoming socket connections must have signed cookies when authorization is on. 296 | 297 | ## 2.6.2 298 | - WaitingRoom option to PING_BEFORE_DISPATCH. 299 | 300 | ## 2.6.1 301 | - The timestamp of claim id action is stored with client object. 302 | 303 | ## 2.6.0 304 | - WaitingRoom accepts custom PLAYER_SORTING and PLAYER_GROUPING parameters. 305 | - Better error catching in WaitingRoom and GameLoader. 306 | 307 | ## 2.5.0 308 | - Phantoms have default viewport 1366x768. 309 | - Phantoms have a polyfill for the .click() method. 310 | - Optimized code for the dispatching of clients in the WaitingRoom. 311 | 312 | ## 2.4.0 313 | - Channels can be set as "default" and they are served from "/" (aliases of the same game will continue to work, but other games might not be available). 314 | - Monitor interface is not available anymore from alias urls. 315 | 316 | ## 2.3.1 317 | - Better look-up of the phantomjs directory. 318 | 319 | ## 2.3.0 320 | - Fixed resolving monitor directory correctly in GameRouter. 321 | 322 | ## 2.2.0 323 | - Query parameters passed to onConnect method in GameServer. (retro-compatible). 324 | 325 | ## 2.1.2 326 | - Built. 327 | 328 | ## 2.1.1 329 | - Handling aliases better, general cleanup in ServerNode 330 | 331 | ## 2.1.0 332 | - Better support for authorization codes. 333 | 334 | ## 2.0.0 335 | - SSL support. 336 | - Default Stager callback changed: does not call node.done(). 337 | - Added checkings if games have same name or if channel has same endpoint. 338 | - Logic client types have default callback where they do **not** call `##node.done()`. 339 | - `bin/info.js` 340 | 341 | ## 1.1.0 342 | - Fixed bug with loading context directories to create language index. 343 | - Fixed bug with channel.require nocache setting in Windows. 344 | 345 | ## 1.0.0 346 | - Callback to decorate client objects. The callback is loaded from the auth file. 347 | - Treatments are not deleted from required settints file. 348 | 349 | ## 0.9.10 350 | - Treatments are correctly built. 351 | - IP address is passed to auth function. 352 | - HandleReconnection method in GameServer forces disconnection of previously connected clients if a reconnection with the same client id is found. This can happen with non-WebSockets connections. 353 | - `enableReconnections` option for player and admin server. 354 | 355 | ## 0.9.9 356 | - Incorrect Build 357 | 358 | ## 0.9.8 359 | - Servernode.lastError 360 | 361 | ## 0.9.7 362 | - Added more options to control Phantom JS bots 363 | - Fixed disconnections on Game Over of Phantom JS bots 364 | - Documentation fixes 365 | 366 | ## 0.9.6 367 | - Added docs to index 368 | - ConnectBot and ConnectPhantom 369 | - Restored corrupted images in public 370 | 371 | ## 0.9.5 372 | - Full Bot connect api 373 | - Phantomjs connect bot 374 | - Client types loaded from game.settings 375 | - Socket Direct messaging is asynchronous 376 | - Load Auth Directory 377 | 378 | ## 0.9.4 379 | - Better integration with Jade template language 380 | - Support for internationalization 381 | - Channels files are automatically loaded from games directory 382 | - Server expects games to have the following folders: `public/`,`views/`, `server/` 383 | - LANG messages are exchanged 384 | 385 | ## 0.9.3 386 | - Added log directory to index. Necessary to run immediately after npm install 387 | 388 | ## 0.9.2 389 | - Fixed reconnection issue when client was not marked connected before trying to update it 390 | - Added a copy of patched static.js in bin/ for windows install 391 | 392 | ## 0.9.1 393 | - Refactored SocketManager to support inter-channels messages 394 | - Refactored SocketIo and SocketDirect 395 | 396 | ## 0.9 397 | - Change log of new version starts here. 398 | 399 | ## above 0.6 400 | - Configuration directories are not loaded again if erroneously added to the index 401 | 402 | ## 0.2.7 403 | - node.redirect (for AdminServer only) 404 | - better make file 405 | - cleanup 406 | 407 | ## 0.2.0 408 | - Major refactoring 409 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2018 Stefano Balietti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | doc: 2 | @./node_modules/.bin/docker index.js lib/ -o docs 3 | 4 | publish: 5 | node bin/make.js build-client -a -o nodegame-full && npm publish 6 | 7 | test: 8 | @./node_modules/.bin/mocha 9 | 10 | .PHONY: test 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodegame-server 2 | 3 | [![Build Status](https://travis-ci.org/nodeGame/nodegame-server.png?branch=master)](https://travis-ci.org/nodeGame/nodegame-server) 4 | 5 | nodegame-server is the server for [nodeGame](http://nodegame.org). 6 | 7 | Handles multiple connections, creates requirements, waiting and game rooms. 8 | 9 | ## Resources 10 | 11 | - [nodeGame documentation](https://github.com/nodeGame/nodegame/wiki) 12 | 13 | ## License 14 | 15 | [MIT](LICENSE) 16 | -------------------------------------------------------------------------------- /bin/buildCSS.js: -------------------------------------------------------------------------------- 1 | module.exports = function buildCSS(cssDir, cb) { 2 | let execFile = require('child_process').execFile; 3 | let child = execFile( 4 | 'sass', 5 | [ 'nodegame.scss', 'nodegame.css' ], 6 | { cwd: cssDir }, 7 | (error, stdout, stderr) => { 8 | if (error) { 9 | console.error(stderr.trim()); 10 | console.error('By the way...Did you install SASS globally?'); 11 | } 12 | else { 13 | console.log('nodegame.css built.'); 14 | } 15 | if (cb) cb(error); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /bin/info.js: -------------------------------------------------------------------------------- 1 | // # Exports data about nodegame modules. 2 | 3 | const J = require('JSUS').JSUS; 4 | const path = require('path'); 5 | 6 | // Directories. 7 | 8 | let ngcDir = J.resolveModuleDir('nodegame-client', __dirname); 9 | let JSUSDir = J.resolveModuleDir('JSUS', __dirname); 10 | let NDDBDir = J.resolveModuleDir('NDDB', __dirname); 11 | let shelfDir = J.resolveModuleDir('shelf.js', __dirname); 12 | let ngwindowDir = J.resolveModuleDir('nodegame-window', __dirname); 13 | let ngwidgetsDir = J.resolveModuleDir('nodegame-widgets', __dirname); 14 | let monitorDir = J.resolveModuleDir('nodegame-monitor', __dirname); 15 | let requirementsDir = J.resolveModuleDir('nodegame-requirements', __dirname); 16 | let mturkDir = J.resolveModuleDir('nodegame-mturk', __dirname); 17 | let gametemplateDir = J.resolveModuleDir('nodegame-game-template', __dirname); 18 | let generatorDir = J.resolveModuleDir('nodegame-generator', __dirname); 19 | let expressDir = J.resolveModuleDir('express', __dirname); 20 | let socketioDir = J.resolveModuleDir('socket.io', __dirname); 21 | 22 | // Build Scripts. 23 | 24 | let buildClient = require(path.resolve(ngcDir, 'bin', 'build.js')).build; 25 | let buildClientSup = 26 | require(path.resolve(ngcDir, 'bin', 'build.js')).build_support; 27 | let buildJSUS = require(path.resolve(JSUSDir, 'bin', 'build.js')).build; 28 | let buildNDDB = require(path.resolve(NDDBDir, 'bin', 'build.js')).build; 29 | let buildShelf = require(path.resolve(shelfDir, 'bin', 'build.js')).build; 30 | let buildNgWindow = require(path.resolve(ngwindowDir, 'bin', 'build.js')).build; 31 | let buildNgWidgets = 32 | require(path.resolve(ngwidgetsDir, 'bin', 'build.js')).build; 33 | let buildCSS = require('./buildCSS'); 34 | 35 | // Express and Socket.io 36 | 37 | let pkgExpress = require(path.resolve(expressDir, 'package.json')); 38 | let pkgSocketio = require(path.resolve(socketioDir, 'package.json')); 39 | 40 | // Packages. 41 | 42 | let pkgClient = require(path.resolve(ngcDir, 'package.json')); 43 | let pkgJSUS = require(path.resolve(JSUSDir, 'package.json')); 44 | let pkgNDDB = require(path.resolve(NDDBDir, 'package.json')); 45 | // let pkgShelf = require(path.resolve(shelfDir, 'package.json')); 46 | let pkgNgWindow = require(path.resolve(ngwindowDir, 'package.json')); 47 | let pkgNgWidgets = require(path.resolve(ngwidgetsDir, 'package.json')); 48 | let pkgMonitor = require(path.resolve(monitorDir, 'package.json')); 49 | let pkgRequirements = require(path.resolve(requirementsDir, 'package.json')); 50 | let pkgMturk = require(path.resolve(mturkDir, 'package.json')); 51 | let pkgGameTemplate = require(path.resolve(gametemplateDir, 'package.json')); 52 | let pkgGenerator = require(path.resolve(generatorDir, 'package.json')); 53 | 54 | 55 | // Server Folders. 56 | 57 | let rootDir = path.resolve(__dirname, '..'); 58 | let buildDir = path.resolve(rootDir, 'public', 'javascripts') + path.sep; 59 | let cssDir = path.resolve(rootDir, 'public', 'stylesheets') + path.sep; 60 | let libDir = path.resolve(rootDir, 'lib') + path.sep; 61 | let confDir = path.resolve(rootDir, 'conf') + path.sep; 62 | 63 | let { version } = require(path.resolve(rootDir, 'package.json')); 64 | 65 | module.exports = { 66 | version: version, 67 | build: { 68 | client: buildClient, 69 | clientSupport: buildClientSup, 70 | JSUS: buildJSUS, 71 | NDDB: buildNDDB, 72 | shelf: buildShelf, 73 | window: buildNgWindow, 74 | widgets: buildNgWidgets, 75 | css: buildCSS 76 | }, 77 | serverDir: { 78 | root: rootDir, 79 | build: buildDir, 80 | css: cssDir, 81 | lib: libDir, 82 | conf: confDir 83 | }, 84 | modulesVersion: { 85 | client: pkgClient.version, 86 | JSUS: pkgJSUS.version, 87 | NDDB: pkgNDDB.version, 88 | //shelf: ..., 89 | window: pkgNgWindow.version, 90 | widgets: pkgNgWidgets.version, 91 | monitor: pkgMonitor.version, 92 | requirements: pkgRequirements.version, 93 | mturk: pkgMturk.version, 94 | gameTemplate: pkgGameTemplate.version, 95 | generator: pkgGenerator.version, 96 | // Express and Socket.io 97 | express: pkgExpress.version, 98 | socketio: pkgSocketio.version 99 | }, 100 | modulesDir: { 101 | client: ngcDir, 102 | JSUS: JSUSDir, 103 | NDDB: NDDBDir, 104 | shelf: shelfDir, 105 | window: ngwindowDir, 106 | widgets: ngwidgetsDir, 107 | monitor: monitorDir, 108 | requirements: requirementsDir, 109 | mturk: mturkDir, 110 | gameTemplate: gametemplateDir, 111 | generator: generatorDir 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /bin/make.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // # nodegame-server make script 4 | 5 | var program = require('commander'), 6 | fs = require('fs-extra'), 7 | { execFile, exec } = require('child_process'), 8 | J = require('JSUS').JSUS; 9 | 10 | var info = require('./info.js'); 11 | var version = info.version; 12 | 13 | var rootDir = info.serverDir.root; 14 | var buildDir = info.serverDir.build; 15 | var libDir = info.serverDir.lib; 16 | 17 | program 18 | .version(version); 19 | 20 | program 21 | .command('build-client [options]') 22 | .description('Creates a nodegame-client custom build') 23 | // TODO: keep options update with client, or find a way to use the options 24 | // defined in the client 25 | .option('-B, --bare', 'bare naked nodegame-client (no dependencies)') 26 | .option('-J, --JSUS', 'with JSUS') 27 | .option('-N, --NDDB', 'with NDDB') 28 | .option('-W, --window', 'with nodeGame-window') 29 | .option('-w, --widgets', 'with nodeGame-widgets') 30 | .option('-d, --addons', 'with nodeGame-client addons') 31 | .option('-s, --shelf', 'with Shelf.js') 32 | .option('-e, --es5', 'with support for old browsers') 33 | .option('-a, --all', 'full build of nodeGame-client') 34 | .option('-C, --clean', 'clean build directory') 35 | .option('-A, --analyse', 'analyse build') 36 | .option('-o, --output ', 'output file (without .js)') 37 | .action(function(env, options) { 38 | var ngcDir = info.modulesDir.client; 39 | var buildClient = info.build.client; 40 | var buildDirClient = path.resolve(ngcDir, 'build'); 41 | 42 | if (options.output && path.extname(options.output) === '.js') { 43 | options.output = path.basename(options.output, '.js'); 44 | } 45 | options.clean = true; 46 | buildClient(options); 47 | J.copyFromDir(buildDirClient, buildDir, '.js'); 48 | }); 49 | 50 | program 51 | .command('build-css') 52 | .description('Calls SASS to build nodegame.css in public/stylesheets/') 53 | .action(function(options) { 54 | info.build.css(info.serverDir.css); 55 | }); 56 | 57 | program 58 | .command('doc') 59 | .description('Builds documentation files') 60 | .action(function(){ 61 | console.log('Building documentation for nodegame-server v.' + version); 62 | // http://nodejs.org/api.html#_child_processes 63 | try { 64 | let dockerDir = J.resolveModuleDir('docker', rootDir); 65 | } 66 | catch(e) { 67 | console.log('module Docker not found. Cannot build doc.'); 68 | console.log('Do \'npm install docker\' to install it.'); 69 | return false; 70 | } 71 | let command = dockerDir + 'docker -i ' + rootDir + 72 | ' index.js lib/ -o ' + rootDir + '/docs/ -u'; 73 | let child = exec(command, function (error, stdout, stderr) { 74 | if (stdout) console.log(stdout); 75 | if (stderr) console.log(stderr); 76 | if (error !== null) { 77 | console.log('build error: ' + error); 78 | } 79 | }); 80 | }); 81 | 82 | //Parsing options 83 | program.parse(process.argv); 84 | 85 | // Old code. 86 | 87 | // program 88 | // .command('clean') 89 | // .description('Removes all files from build folder') 90 | // .action(function(){ 91 | // J.emptyDir(buildDir); 92 | // }); 93 | 94 | // program 95 | // .command('refresh') 96 | // .option('-a, --all', 'copy all js and css files') 97 | // .option('-j, --js-only', 'copy only js files') 98 | // .option('-c, --css-only', 'copy only css files') 99 | // .description('Copies all .js .css files from submodules into public/') 100 | // .action(function(options) { 101 | // if (!options.all && !options.js && !options.css) { 102 | // options.all = true; 103 | // } 104 | // 105 | // if (options.all || options.js) { 106 | // try { 107 | // J.copyFromDir(buildDirClient, buildDir, '.js'); 108 | // } 109 | // catch(e) { 110 | // console.log('make refresh: could not find nodegame-client.'); 111 | // } 112 | // try { 113 | // J.copyFromDir(buildDir_ngWindow, buildDir, '.js'); 114 | // } 115 | // catch(e) { 116 | // console.log('make refresh: could not find nodegame-window.'); 117 | // } 118 | // try { 119 | // J.copyFromDir(buildDir_ngWidgets, buildDir, '.js'); 120 | // } 121 | // catch(e) { 122 | // console.log('make refresh: could not find widgets.'); 123 | // } 124 | // try { 125 | // J.copyFromDir(buildDir_JSUS, buildDir, '.js'); 126 | // } 127 | // catch(e) { 128 | // console.log('make refresh: could not find JSUS.'); 129 | // } 130 | // try { 131 | // J.copyFromDir(buildDir_NDDB, buildDir, '.js'); 132 | // } 133 | // catch(e) { 134 | // console.log('make refresh: could not find NDDB.'); 135 | // } 136 | // try { 137 | // J.copyFromDir(buildDir_shelf, buildDir, '.js'); 138 | // } 139 | // catch(e) { 140 | // console.log('make refresh: could not find shelf.js.'); 141 | // } 142 | // } 143 | // 144 | // if (options.all || options.css) { 145 | // copyCSS(); 146 | // } 147 | // 148 | // console.log('All files copied to public/'); 149 | // }); 150 | 151 | // program 152 | // .command('multibuild') 153 | // .description('Builds a set of javascript libraries for nodeGame') 154 | // .action(function(){ 155 | // console.log('Multi-build for nodegame-server v.' + version); 156 | // 157 | // var buildClientSupport = info.build.clientSupport; 158 | // 159 | // // nodegame-client 160 | // buildClient({ 161 | // clean: true, 162 | // all: true, 163 | // output: "nodegame-full", 164 | // }); 165 | // buildClient({ 166 | // bare: true, 167 | // output: "nodegame-bare", 168 | // }); 169 | // buildClient({ 170 | // output: "nodegame", 171 | // }); 172 | // // buildClientSupport({ 173 | // // all: true, 174 | // // }); 175 | // 176 | // J.copyFromDir(buildDirClient, buildDir, '.js'); 177 | // 178 | // // JSUS 179 | // build_JSUS({ 180 | // lib: ['obj','array','eval','parse','random','time','dom'], 181 | // clean: true, 182 | // }); 183 | // 184 | // J.copyFromDir(buildDir_JSUS, buildDir, '.js'); 185 | // 186 | // // NDDB 187 | // build_NDDB({ 188 | // clean: true, 189 | // all: true, 190 | // output: "nddb-full", 191 | // }); 192 | // build_NDDB({ 193 | // bare: true, 194 | // output: "nddb-bare", 195 | // }); 196 | // build_NDDB({ 197 | // JSUS: true, 198 | // output: "nddb", 199 | // }); 200 | // 201 | // J.copyFromDir(buildDir_NDDB, buildDir, '.js'); 202 | // 203 | // // Shelf.js 204 | // build_shelf({ 205 | // clean: true, 206 | // all: true, 207 | // output: "shelf-full", 208 | // }); 209 | // build_shelf({ 210 | // lib: ['amplify','cookie'], 211 | // output: "shelf-browser", 212 | // }); 213 | // build_shelf({ 214 | // lib: ['amplify'], 215 | // output: "shelf-amplify", 216 | // }); 217 | // build_shelf({ 218 | // lib: ['cookie'], 219 | // output: "shelf-cookie", 220 | // }); 221 | // build_shelf({ 222 | // lib: ['fs'], 223 | // output: "shelf-fs", 224 | // }); 225 | // 226 | // J.copyFromDir(buildDir_shelf, buildDir, '.js'); 227 | // 228 | // // ng-widgets 229 | // build_ngwidgets({ 230 | // all: true, 231 | // clean: true, 232 | // }); 233 | // 234 | // J.copyFromDir(buildDir_ngWidgets, buildDir, '.js'); 235 | // 236 | // // ng-window 237 | // build_ngwindow({ 238 | // all: true, 239 | // clean: true, 240 | // }); 241 | // 242 | // J.copyFromDir(buildDir_ngWindow, buildDir, '.js'); 243 | // 244 | // console.log('All javascript files built and copied to ' + 245 | // 'public/javascript/'); 246 | // }); 247 | 248 | 249 | // var JSUSDir = info.modulesDir.JSUS; 250 | // var NDDBDir = info.modulesDir.NDDB; 251 | // var shelfDir = info.modulesDir.shelf; 252 | // var ngwindowDir = info.modulesDir.window; 253 | // var ngwidgetsDir = info.modulesDir.widgets; 254 | // var build_JSUS = info.build.JSUS; 255 | // var build_NDDB = info.build.NDDB; 256 | // var build_shelf = info.build.shelf; 257 | // var build_ngwindow = info.build.window; 258 | // var build_ngwidgets = info.build.widgets; 259 | // var buildDir_JSUS = JSUSDir + 'build/'; 260 | // var buildDir_NDDB = NDDBDir + 'build/'; 261 | // var buildDir_shelf = shelfDir + 'build/'; 262 | // var buildDir_ngWindow = ngwindowDir + 'build/'; 263 | // var buildDir_ngWidgets = ngwidgetsDir + 'build/'; 264 | 265 | // program 266 | // .command('sync [options]') 267 | // .description('Sync with the specified target directory (must exists)') 268 | // .option('-a, --all', 'sync /lib and /conf folders (default)') 269 | // .option('-l, --lib', 'sync the /lib folder') 270 | // .option('-c, --conf', 'sync the /conf folder') 271 | // .action(function(path, options) { 272 | // var confDir = info.serverDir.conf; 273 | // 274 | // if ('undefined' === typeof options) { 275 | // options = {}; 276 | // options.all = true; 277 | // } 278 | // 279 | // if (options.all) { 280 | // copyDirTo(confDir, path + '/conf/'); 281 | // copyDirTo(libDir, path + '/lib/'); 282 | // } 283 | // 284 | // else if (options.conf) { 285 | // copyDirTo(confDir, path); 286 | // } 287 | // 288 | // else if (options.lib) { 289 | // copyDirTo(libDir, path); 290 | // } 291 | // 292 | // console.log('Done.'); 293 | // 294 | // }); 295 | 296 | 297 | // function copyDirTo(inputDir, targetDir) { 298 | // 299 | // if (!targetDir) { 300 | // console.log('You must specify a target directory ' + 301 | // 'for the \'sync\' command'); 302 | // return; 303 | // } 304 | // 305 | // targetDir = path.resolve(targetDir); 306 | // 307 | // if (!fs.existsSync(targetDir)) { 308 | // console.log(targetDir + ' does not exist'); 309 | // return false; 310 | // } 311 | // 312 | // var stats = fs.lstatSync(targetDir); 313 | // if (!stats.isDirectory()) { 314 | // console.log(targetDir + ' is not a directory'); 315 | // return false; 316 | // } 317 | // 318 | // targetDir = targetDir + '/'; 319 | // 320 | // console.log('nodegame-server v.' + version + ': syncing ' + inputDir + 321 | // ' with ' + targetDir); 322 | // 323 | // J.copyDirSyncRecursive(inputDir, targetDir); 324 | // } 325 | 326 | // function copyCSS(options, cb) { 327 | // var asyncQueue = new J.getQueue(); 328 | // try { 329 | // asyncQueue.add('window'); 330 | // J.copyFromDir(ngwindowDir + 'css/', cssDir, '.css', function() { 331 | // asyncQueue.remove('window'); 332 | // }); 333 | // } 334 | // catch(e) { 335 | // console.log('make refresh: could not find nodegame-window css dir.'); 336 | // asyncQueue.remove('window'); 337 | // } 338 | // try { 339 | // asyncQueue.add('widgets'); 340 | // J.copyFromDir(ngwidgetsDir + 'css/', cssDir, '.css', function() { 341 | // asyncQueue.remove('widgets'); 342 | // }); 343 | // } 344 | // catch(e) { 345 | // console.log('make refresh: could not find nodegame-window css dir.'); 346 | // asyncQueue.remove('widgets'); 347 | // } 348 | // 349 | // if (cb) { 350 | // asyncQueue.onReady(cb); 351 | // } 352 | // } 353 | -------------------------------------------------------------------------------- /bin/ng.connect.static.js.patch: -------------------------------------------------------------------------------- 1 | 129c129,131 2 | < path = normalize(join(root, path)); 3 | --- 4 | > //path = normalize(join(root, path)); 5 | > // Node.js v0.10.20 workaround: 6 | > path = normalize(join(root || "", path)); 7 | -------------------------------------------------------------------------------- /bin/static.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Connect - staticProvider 4 | * Copyright(c) 2010 Sencha Inc. 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var fs = require('fs') 14 | , path = require('path') 15 | , join = path.join 16 | , basename = path.basename 17 | , normalize = path.normalize 18 | , utils = require('../utils') 19 | , Buffer = require('buffer').Buffer 20 | , parse = require('url').parse 21 | , mime = require('mime'); 22 | 23 | /** 24 | * Static file server with the given `root` path. 25 | * 26 | * Examples: 27 | * 28 | * var oneDay = 86400000; 29 | * 30 | * connect( 31 | * connect.static(__dirname + '/public') 32 | * ).listen(3000); 33 | * 34 | * connect( 35 | * connect.static(__dirname + '/public', { maxAge: oneDay }) 36 | * ).listen(3000); 37 | * 38 | * Options: 39 | * 40 | * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 41 | * - `hidden` Allow transfer of hidden files. defaults to false 42 | * - `redirect` Redirect to trailing "/" when the pathname is a dir 43 | * 44 | * @param {String} root 45 | * @param {Object} options 46 | * @return {Function} 47 | * @api public 48 | */ 49 | 50 | exports = module.exports = function static(root, options){ 51 | options = options || {}; 52 | 53 | // root required 54 | if (!root) throw new Error('static() root path required'); 55 | options.root = root; 56 | 57 | return function static(req, res, next) { 58 | options.path = req.url; 59 | options.getOnly = true; 60 | send(req, res, next, options); 61 | }; 62 | }; 63 | 64 | /** 65 | * Expose mime module. 66 | */ 67 | 68 | exports.mime = mime; 69 | 70 | /** 71 | * Respond with 416 "Requested Range Not Satisfiable" 72 | * 73 | * @param {ServerResponse} res 74 | * @api private 75 | */ 76 | 77 | function invalidRange(res) { 78 | var body = 'Requested Range Not Satisfiable'; 79 | res.setHeader('Content-Type', 'text/plain'); 80 | res.setHeader('Content-Length', body.length); 81 | res.statusCode = 416; 82 | res.end(body); 83 | } 84 | 85 | /** 86 | * Attempt to tranfer the requseted file to `res`. 87 | * 88 | * @param {ServerRequest} 89 | * @param {ServerResponse} 90 | * @param {Function} next 91 | * @param {Object} options 92 | * @api private 93 | */ 94 | 95 | var send = exports.send = function(req, res, next, options){ 96 | options = options || {}; 97 | if (!options.path) throw new Error('path required'); 98 | 99 | // setup 100 | var maxAge = options.maxAge || 0 101 | , ranges = req.headers.range 102 | , head = 'HEAD' == req.method 103 | , get = 'GET' == req.method 104 | , root = options.root ? normalize(options.root) : null 105 | , redirect = false === options.redirect ? false : true 106 | , getOnly = options.getOnly 107 | , fn = options.callback 108 | , hidden = options.hidden 109 | , done; 110 | 111 | // replace next() with callback when available 112 | if (fn) next = fn; 113 | 114 | // ignore non-GET requests 115 | if (getOnly && !get && !head) return next(); 116 | 117 | // parse url 118 | var url = parse(options.path) 119 | , path = decodeURIComponent(url.pathname) 120 | , type; 121 | 122 | // null byte(s) 123 | if (~path.indexOf('\0')) return utils.badRequest(res); 124 | 125 | // when root is not given, consider .. malicious 126 | if (!root && ~path.indexOf('..')) return utils.forbidden(res); 127 | 128 | // join / normalize from optional root dir 129 | //path = normalize(join(root, path)); 130 | // Node.js v0.10.20 workaround: 131 | path = normalize(join(root || "", path)); 132 | 133 | // malicious path 134 | if (root && 0 != path.indexOf(root)) return fn 135 | ? fn(new Error('Forbidden')) 136 | : utils.forbidden(res); 137 | 138 | // index.html support 139 | if (normalize('/') == path[path.length - 1]) path += 'index.html'; 140 | 141 | // "hidden" file 142 | if (!hidden && '.' == basename(path)[0]) return next(); 143 | 144 | fs.stat(path, function(err, stat){ 145 | // mime type 146 | type = mime.lookup(path); 147 | 148 | // ignore ENOENT 149 | if (err) { 150 | if (fn) return fn(err); 151 | return 'ENOENT' == err.code 152 | ? next() 153 | : next(err); 154 | // redirect directory in case index.html is present 155 | } else if (stat.isDirectory()) { 156 | if (!redirect) return next(); 157 | res.statusCode = 301; 158 | res.setHeader('Location', url.pathname + '/'); 159 | res.end('Redirecting to ' + url.pathname + '/'); 160 | return; 161 | } 162 | 163 | // header fields 164 | if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); 165 | if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); 166 | if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString()); 167 | if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat)); 168 | if (!res.getHeader('content-type')) { 169 | var charset = mime.charsets.lookup(type); 170 | res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); 171 | } 172 | res.setHeader('Accept-Ranges', 'bytes'); 173 | 174 | // conditional GET support 175 | if (utils.conditionalGET(req)) { 176 | if (!utils.modified(req, res)) { 177 | req.emit('static'); 178 | return utils.notModified(res); 179 | } 180 | } 181 | 182 | var opts = {}; 183 | var chunkSize = stat.size; 184 | 185 | // we have a Range request 186 | if (ranges) { 187 | ranges = utils.parseRange(stat.size, ranges); 188 | // valid 189 | if (ranges) { 190 | // TODO: stream options 191 | // TODO: multiple support 192 | opts.start = ranges[0].start; 193 | opts.end = ranges[0].end; 194 | chunkSize = opts.end - opts.start + 1; 195 | res.statusCode = 206; 196 | res.setHeader('Content-Range', 'bytes ' 197 | + opts.start 198 | + '-' 199 | + opts.end 200 | + '/' 201 | + stat.size); 202 | // invalid 203 | } else { 204 | return fn 205 | ? fn(new Error('Requested Range Not Satisfiable')) 206 | : invalidRange(res); 207 | } 208 | } 209 | 210 | res.setHeader('Content-Length', chunkSize); 211 | 212 | // transfer 213 | if (head) return res.end(); 214 | 215 | // stream 216 | var stream = fs.createReadStream(path, opts); 217 | req.emit('static', stream); 218 | stream.pipe(res); 219 | 220 | // callback 221 | if (fn) { 222 | function callback(err) { done || fn(err); done = true } 223 | req.on('close', callback); 224 | stream.on('end', callback); 225 | } 226 | }); 227 | }; 228 | -------------------------------------------------------------------------------- /conf/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # http.js 3 | * Copyright(c) 2023 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Configuration file for Express server in nodegame-server 7 | */ 8 | module.exports = configure; 9 | 10 | // ## Global scope 11 | 12 | const express = require('express'); 13 | const path = require('path'); 14 | const J = require('nodegame-client').JSUS; 15 | 16 | const mime = require('express').static.mime; 17 | 18 | // const bodyParser = require('body-parser'); 19 | const cookieParser = require('cookie-parser'); 20 | const errorHandler = require('errorhandler'); 21 | 22 | // var session = require('cookie-session')({ secret: 'secret' }); 23 | 24 | /** 25 | * ### configure 26 | * 27 | * Defines standard routes for the HTTP server 28 | * 29 | * @param {object} options The object containing the custom settings 30 | */ 31 | function configure(app, servernode) { 32 | 33 | let rootDir = servernode.rootDir; 34 | let publicDir = path.join(rootDir, 'public'); 35 | let basepath = servernode.basepath || ''; 36 | let resourceManager = servernode.resourceManager; 37 | 38 | app.set('views', path.resolve(rootDir, 'views')); 39 | app.set('view engine', 'jade'); 40 | app.set('view options', {layout: false}); 41 | 42 | 43 | if (process.env.NODE_ENV === 'development') { 44 | app.use(express.static(publicDir)); 45 | app.use(errorHandler()); 46 | } 47 | else { 48 | app.use(express.static(publicDir, { maxAge: 3600000 })); 49 | } 50 | 51 | app.disable('x-powered-by'); 52 | app.enable("jsonp callback"); 53 | 54 | app.use(cookieParser()); 55 | // app.use(bodyParser()); 56 | // app.use(bodyParser.json()); 57 | // app.use(bodyParser.urlencoded({ extended: true })); 58 | 59 | app.use(express.json()); 60 | app.use(express.urlencoded({ extended: true })); 61 | 62 | function sendFromPublic(type, req, res, headers) { 63 | let file = req.params[0]; 64 | if (!file) return; 65 | 66 | // Build path in `public/`. 67 | file = path.join(type, file); 68 | 69 | let mimeType, charset, filePath; 70 | 71 | // Build headers. 72 | if (!headers) { 73 | mimeType = mime.lookup(file); 74 | charset = mime.charsets.lookup(mimeType); 75 | headers = { 'Content-Type': mimeType }; 76 | if (charset) headers.charset = charset; 77 | } 78 | 79 | // If it is not text, it was not cached. 80 | if (headers['Content-Type'].substring(0,4) !== 'text') { 81 | filePath = path.resolve(publicDir, file); 82 | res.sendFile(filePath); 83 | return; 84 | } 85 | 86 | // Already found in `public/` and cached. 87 | resourceManager.getFromPublic('/', file, function(cachedFile) { 88 | 89 | // File found in public (cached or loaded). 90 | if (cachedFile) { 91 | res.set(headers); 92 | res.status(200).send(cachedFile); 93 | } 94 | else { 95 | // Send 404. 96 | res.status(404).send('File not found.'); 97 | } 98 | }); 99 | } 100 | 101 | app.get(basepath + '/javascripts/*', function(req, res) { 102 | sendFromPublic('javascripts', req, res, { 103 | 'Content-Type': 'text/javascript', 104 | 'charset': 'utf-8' 105 | }); 106 | }); 107 | 108 | app.get(basepath + '/sounds/*', function(req, res) { 109 | sendFromPublic('sounds', req, res, { 110 | 'Content-Type': 'sound/ogg' 111 | }); 112 | }); 113 | 114 | app.get(basepath + '/stylesheets/*', function(req, res) { 115 | sendFromPublic('stylesheets', req, res, { 116 | 'Content-Type': 'text/css', 117 | 'charset': 'utf-8' 118 | }); 119 | }); 120 | 121 | 122 | app.get(basepath + '/images/*', function(req, res) { 123 | sendFromPublic('images', req, res, { 124 | 'Content-Type': 'image/png' 125 | }); 126 | }); 127 | 128 | app.get(basepath + '/pages/*', function(req, res) { 129 | sendFromPublic('pages', req, res, { 130 | 'Content-Type': 'text/html', 131 | 'charset': 'utf-8' 132 | }); 133 | }); 134 | 135 | app.get(basepath + '/lib/*', function(req, res) { 136 | sendFromPublic('lib', req, res); 137 | }); 138 | 139 | app.get(basepath + '/', function(req, res, next) { 140 | 141 | // Must be first. 142 | if (req.query) { 143 | let q = req.query.q; 144 | if (q) { 145 | if (!servernode.enableInfoQuery) { 146 | res.status(403).end(); 147 | return; 148 | } 149 | 150 | let game = req.query.game; 151 | game = servernode.channels[game]; 152 | 153 | switch(q) { 154 | case 'info': 155 | res.status(200).send(servernode.info); 156 | break; 157 | 158 | case 'channels': 159 | // res.header("Access-Control-Allow-Origin", "*"); 160 | // res.header("Access-Control-Allow-Headers", 161 | // "X-Requested-With"); 162 | res.status(200).send(servernode.info.channels); 163 | break; 164 | 165 | case 'games': 166 | res.status(200).send(servernode.info.games); 167 | break; 168 | 169 | case 'waitroom': 170 | if (!game) { 171 | // TODO: Return info for all games or 172 | // take into account default channel. 173 | res.status(400).send("You must specify a valid game."); 174 | } 175 | else { 176 | let level = req.query.level; 177 | if (level) { 178 | game = game.gameLevels[level]; 179 | if (!game) { 180 | res.status(400).send("Level not found: " + 181 | level); 182 | break; 183 | } 184 | } 185 | 186 | let room = game.waitingRoom; 187 | 188 | if (!room) { 189 | res.status(400) 190 | .send("Channel/level has no waitroom."); 191 | } 192 | else { 193 | let nPlayers = room.clients.playerConnected.size(); 194 | res.status(200).send({ 195 | nPlayers: nPlayers, 196 | open: room.isRoomOpen() 197 | }); 198 | } 199 | } 200 | break; 201 | 202 | // @experimental 203 | case 'clients': 204 | if (!game) { 205 | res.status(400).send("You must specify a valid game."); 206 | } 207 | else { 208 | let nClients = game.registry.clients.connectedPlayer; 209 | nClients = nClients.size(); 210 | res.status(200).send({ nClients: nClients }); 211 | } 212 | break; 213 | 214 | // @experimental 215 | case 'highscore': 216 | if (!game) { 217 | res.status(400).send("You must specify a valid game."); 218 | } 219 | else { 220 | res.status(200).send({ highscore: game.highscore }); 221 | } 222 | break; 223 | 224 | default: 225 | res.status(400).send('Unknown query received.'); 226 | } 227 | return; 228 | } 229 | } 230 | 231 | if (servernode.defaultChannel) { 232 | next(); 233 | return; 234 | } 235 | 236 | if (servernode.homePage === false || 237 | servernode.homePage.enabled === false) { 238 | 239 | res.render('index_simple', { 240 | title: 'Yay! nodeGame server is running.' 241 | }); 242 | } 243 | else { 244 | let gamesObj = servernode.info.games; 245 | let listOfGames = J.keys(gamesObj); 246 | // Remove aliases. 247 | let filteredGames = listOfGames.filter(function(name) { 248 | // WAS: 249 | // return (!gamesObj[name].disabled && !gamesObj[name].errored && 250 | // (!gamesObj[name].alias || 251 | // gamesObj[name].alias.indexOf(name) === -1)); 252 | let g = gamesObj[name]; 253 | if (g.disabled || g.errored) return false; 254 | if (g.info.card === false) return false; 255 | if (g.alias && g.alias.indexOf(name) !== -1) return false; 256 | return true; 257 | }); 258 | if (J.isArray(servernode.homePage.cardsOrder)) { 259 | filteredGames = 260 | servernode.homePage.cardsOrder.filter(function(name) { 261 | if (filteredGames.indexOf(name) !== -1) return true; 262 | servernode.logger.error('homePage.cardsOrder ' + 263 | 'game not found: ' + name); 264 | }); 265 | } 266 | else { 267 | filteredGames.sort(); 268 | } 269 | 270 | let games = []; 271 | let colors = servernode.homePage.colors; 272 | 273 | let i = 0; 274 | for (let j = 0; j < filteredGames.length; j++) { 275 | let name = filteredGames[j]; 276 | // Mixout name and description from package.json 277 | // if not in card, or if no card is defined. 278 | let card = J.mixout(gamesObj[name].info.card || {}, { 279 | name: name.charAt(0).toUpperCase() + name.slice(1), 280 | description: gamesObj[name].info.description 281 | }); 282 | 283 | if (i >= colors.length) i = 0; 284 | let color = card.color || colors[i]; 285 | 286 | games.push({ 287 | // If someone rename `card.name` the link still needs 288 | // to point to name. 289 | _name: name, 290 | name: card.name, 291 | color: color, 292 | url: card.url, 293 | description: card.description, 294 | publication: card.publication, 295 | wiki: card.wiki, 296 | icon: card.icon 297 | }); 298 | i++; 299 | } 300 | let externalCards = servernode.homePage.externalCards; 301 | if (externalCards && externalCards.length) { 302 | for (let j = 0; j < externalCards.length; j++) { 303 | externalCards[j].color = colors[i++]; 304 | externalCards[j].external = true; 305 | games.push(externalCards[j]); 306 | } 307 | } 308 | res.render('homepage', { 309 | title: servernode.homePage.title, 310 | games: games, 311 | nodeGameCard: servernode.homePage.nodeGameCard, 312 | footerContent: servernode.homePage.footerContent, 313 | logo: servernode.homePage.logo 314 | }); 315 | } 316 | 317 | 318 | 319 | }); 320 | 321 | return true; 322 | } 323 | -------------------------------------------------------------------------------- /conf/loggers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # loggers.js 3 | * Copyright(c) 2021 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Configuration file for Winston.js in nodegame-server 7 | */ 8 | 9 | module.exports = configure; 10 | 11 | const path = require('path'); 12 | const winston = require('winston'); 13 | 14 | // Variable loggers is winston.loggers. 15 | function configure(loggers, logDir) { 16 | let format = winston.format; 17 | let logLevel = winston.level; 18 | let transports = winston.transports; 19 | 20 | const logFormatter = format.printf((info) => { 21 | let { timestamp, level, stack, message } = info; 22 | message = stack || message; 23 | return `${timestamp} ${level}: ${message}`; 24 | }); 25 | 26 | // const msgFormatter = format.printf((info) => { 27 | // let { timestamp, level, message } = info; 28 | // message = stack || message; 29 | // return `${timestamp} ${level}: ${message}`; 30 | // }); 31 | 32 | let consoleFormat = format.combine(format.colorize(), format.simple(), 33 | format.timestamp(), logFormatter); 34 | 35 | let msgFormat = format.combine(format.simple()); 36 | 37 | // ServerNode. 38 | loggers.add('servernode', { 39 | format: winston.format.simple(), 40 | transports: [ 41 | new transports.Console({ 42 | level: logLevel, 43 | // colorize: true 44 | format: consoleFormat 45 | }), 46 | new transports.File({ 47 | format: winston.format.simple(), 48 | level: logLevel, 49 | timestamp: true, 50 | filename: path.join(logDir, 'servernode.log'), 51 | maxsize: 1000000, 52 | maxFiles: 10 53 | }) 54 | ] 55 | 56 | }); 57 | 58 | // Channel. 59 | loggers.add('channel', { 60 | // format: winston.format.simple(), 61 | // format: format.errors({ stack: true }), 62 | transports: [ 63 | new transports.Console({ 64 | level: logLevel, 65 | // colorize: true 66 | format: consoleFormat 67 | }), 68 | new transports.File({ 69 | format: winston.format.simple(), 70 | level: logLevel, 71 | timestamp: true, 72 | filename: path.join(logDir, 'channels.log'), 73 | maxsize: 1000000, 74 | maxFiles: 10 75 | }) 76 | ] 77 | }); 78 | 79 | // Messages. 80 | // Make custom levels and only File transports for messages. 81 | loggers.add('messages', { 82 | levels: { 83 | // Log none. 84 | none: 0, 85 | // All DATA msgs. 86 | data: 1, 87 | // All SET and DATA msgs. 88 | set: 3, 89 | // All SET, GET and DATA msgs. 90 | get: 5, 91 | // All SETUP, SET, GET and DATA msgs. 92 | setup: 7, 93 | // All messages, but **not** PLAYER_UPDATE, SERVERCOMMAND and ALERT. 94 | game: 9, 95 | // All messages. 96 | all: 11 97 | }, 98 | transports: [ 99 | new transports.File({ 100 | level: 'all', 101 | timestamp: true, 102 | maxsize: 1000000, 103 | filename: path.join(logDir, 'messages.log'), 104 | format: msgFormat 105 | }) 106 | ] 107 | }); 108 | 109 | // Clients. 110 | loggers.add('clients', { 111 | // format: format.errors({ stack: true }), 112 | transports: [ 113 | new transports.Console({ 114 | level: logLevel, 115 | // colorize: true 116 | format: consoleFormat 117 | }), 118 | new transports.File({ 119 | // format: consoleFormat, 120 | format: winston.format.simple(), 121 | level: 'silly', 122 | timestamp: true, 123 | filename: path.join(logDir, 'clients.log') 124 | }) 125 | ] 126 | }); 127 | 128 | return true; 129 | } 130 | -------------------------------------------------------------------------------- /conf/servernode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # servernode.js 3 | * 4 | * Copyright(c) 2020 Stefano Balietti 5 | * MIT Licensed 6 | * 7 | * Configuration file for ServerNode in nodegame-server. 8 | * 9 | * @see ServerNode 10 | */ 11 | module.exports = configure; 12 | 13 | const path = require('path'); 14 | 15 | function configure(servernode) { 16 | 17 | let rootDir = servernode.rootDir; 18 | if (!rootDir) { 19 | servernode.logger.error('configure servernode: rootDir not found'); 20 | return false; 21 | } 22 | 23 | // 1. Servernode configuration. 24 | 25 | // The name of the server. 26 | servernode.name = "nodeGame server"; 27 | 28 | // Displays home page with list of games. 29 | // The whole home page displayed can be changed by modifying 30 | // file: views/homepage.jade 31 | servernode.homePage = { 32 | 33 | // If FALSE, a generic message is displayed instead. Default: TRUE. 34 | enabled: true, 35 | 36 | // The title to be displayed in the top bar. 37 | title: "nodeGame v" + servernode.nodeGameVersion + " Showcase", 38 | 39 | // The name of logo file. Empty no logo. Default: "nodegame_logo.png". 40 | logo: "nodegame_logo.png", 41 | 42 | // The background colors of the cards to access the games. 43 | // Colors are repeated in the same order if there are more games 44 | // than colors. 45 | colors: [ 'teal', 'green', 'indigo', 'blue' ], 46 | 47 | // Array containing cards for external games. 48 | externalCards: [ 49 | // Format: 50 | // { 51 | // name: "Game Name", 52 | // demo: "https://game.address", 53 | // url: "https://github.com/myproject", 54 | // description: "blah blah", 55 | // publication: "https://link.to.publication.com", 56 | // wiki: "https://wikipedia.com/blah", 57 | // icon: "ac_unit" // See https://materializecss.com/icons.html 58 | // } 59 | ], 60 | 61 | // Default alphabetical, or customize with an array of game names. 62 | // Games not listed here are excluded. 63 | // cardsOrder: [ 'game1', 'game2', 'game3' ], 64 | 65 | // Displays a nodeGame card at last. Default: TRUE. 66 | nodeGameCard: true, 67 | 68 | // Displays nodeGame info in footer. Default: TRUE. 69 | footerContent: true 70 | 71 | }; 72 | 73 | // Immediately disabling homePage if ServerNode was launched 74 | // with option --default. 75 | if (servernode._defaultChannel) { 76 | servernode.logger.verbose('homePage disabled by option --default'); 77 | servernode.homePage = false; 78 | } 79 | 80 | // Default games directory. 81 | servernode.defaultGamesDir = path.join(rootDir, 'games'); 82 | 83 | // Array of games directories. They will be scanned sequentially 84 | // at loading time, and every subfolder containing a package.json 85 | // file will be added as a new game. 86 | // Important: games found in folders down in lower positions of the 87 | // array can override games defined before. 88 | servernode.gamesDirs = [ servernode.defaultGamesDir ]; 89 | 90 | if (process && process.env.PORT) { 91 | // If app is running on a cloud service (e.g. Heroku) 92 | // then the assigned port has to be used. 93 | servernode.port = process.env.PORT; 94 | } 95 | else { 96 | // Port of the HTTP server and the Socket.IO app. 97 | servernode.port = '8080'; 98 | } 99 | 100 | // The maximum number of channels allowed. 0 means unlimited. 101 | servernode.maxChannels = 0; 102 | 103 | // The maximum number of listeners allowed. 0 means unlimited. 104 | // Every channel defines 1 or more game servers, and each defines 105 | // a certain number of Node.JS listeners. 106 | servernode.maxListeners = 0; 107 | 108 | // If TRUE, it is possible to get information from the server by 109 | // via querystring (?q=). 110 | servernode.enableInfoQuery = false; 111 | 112 | // Url prefix do add in front of every route. Useful if nodegame is used 113 | // together with a reverse proxy engine like Ngix. (e.g. /mybasepath). 114 | servernode.basepath = null; 115 | 116 | // 2. Channels configuration. 117 | 118 | // AdminServer default options. 119 | let adminServer = { 120 | 121 | // A PLAYER_UPDATE / PCONNECT / PDISCONNECT message will be sent to 122 | // all clients connected to the PlayerServer endpoint when: 123 | notify: { 124 | 125 | // A new authorized client connects; 126 | onConnect: true, 127 | 128 | // A client enters a new stage; 129 | onStageUpdate: false, 130 | 131 | // A client changes stageLevel (e.g. INIT, CALLBACK_EXECUTED); 132 | onStageLevelUpdate: false, 133 | 134 | // A client is LOADED (this option is needed in combination with 135 | // the option syncOnLoaded used on the clients). It is much less 136 | // expensive than setting onStageLevelUpdate = true; 137 | onStageLoadedUpdate: false 138 | }, 139 | 140 | // Restricts the available recipient options. 141 | canSendTo: { 142 | 143 | // ALL 144 | all: true, 145 | 146 | // CHANNEL 147 | ownChannel: true, 148 | 149 | // ROOM 150 | ownRoom: true, 151 | 152 | // CHANNEL_XXX 153 | anyChannel: true, 154 | 155 | // ROOM_XXX 156 | anyRoom: true 157 | }, 158 | 159 | // If the recipient of a _gameMsg_ is an array, it can contain at most 160 | // _maxMsgRecipients_ elements. 161 | maxMsgRecipients: 1000, 162 | 163 | // Commented for the moment. 164 | 165 | // // All messages exchanged between players will be forwarded to the 166 | // // clients connected to the admin endpoint (ignores the _to_ field). 167 | // forwardAllMessages: true, 168 | // 169 | // // If TRUE, players can invoke GET commands on admins. 170 | // // TODO: rename in a more generic way, or distinguish get from admins 171 | // // and get from players. 172 | // getFromAdmins: false, 173 | 174 | // Allow setting data (e.g. startingRoom, client type) in the query 175 | // data of a SocketIO connection. 176 | sioQuery: true, 177 | 178 | // Enable reconnections. 179 | // If TRUE, clients will be matched with previous game history based on 180 | // the clientId found in their cookies. 181 | enableReconnections: true 182 | }; 183 | 184 | // PlayerServer default options. 185 | let playerServer = { 186 | 187 | // A PLAYER_UPDATE / PCONNECT / PDISCONNECT message will be sent to 188 | // all clients connected to the PlayerServer endpoint when: 189 | notify: { 190 | 191 | // A new authorized client connects; 192 | onConnect: false, 193 | 194 | // A client enters a new stage; 195 | onStageUpdate: false, 196 | 197 | // A client changes stageLevel (e.g. INIT, CALLBACK_EXECUTED); 198 | onStageLevelUpdate: false, 199 | 200 | // A client is LOADED (this option is needed in combination with 201 | // the option syncOnLoaded used on the clients). It is much less 202 | // expensive than setting onStageLevelUpdate = true; 203 | onStageLoadedUpdate: false 204 | }, 205 | 206 | // Restricts the available recipient options. 207 | canSendTo: { 208 | 209 | // ALL 210 | all: false, 211 | 212 | // CHANNEL 213 | ownChannel: false, 214 | 215 | // ROOM 216 | ownRoom: true, 217 | 218 | // CHANNEL_XXX 219 | anyChannel: false, 220 | 221 | // ROOM_XXX 222 | anyRoom: false 223 | }, 224 | 225 | // If the recipient of a _gameMsg_ is an array, it can contain at most 226 | // _maxMsgRecipients_ elements. 227 | maxMsgRecipients: 10, 228 | 229 | // All messages exchanged between players will be forwarded to the 230 | // clients connected to the admin endpoint (ignores the _to_ field). 231 | forwardAllMessages: true, 232 | 233 | // If TRUE, players can invoke GET commands on admins. 234 | // TODO: rename in a more generic way, or distinguish get from admins 235 | // and get from players. 236 | getFromAdmins: false, 237 | 238 | // See above, in admin defaults. 239 | sioQuery: true, 240 | 241 | // See above, in the admin defaults. 242 | enableReconnections: true, 243 | 244 | // Anti-spoofing, extra check to see if msg.from matches socket.id 245 | // on SocketIo socket connections. Spoofed messages are logged 246 | // normally, and an additional log entry with id and from msg is added. 247 | antiSpoofing: true 248 | }; 249 | 250 | // Defaults option for a channel. 251 | servernode.defaults.channel = { 252 | // Common options. 253 | // Which sockets will be enabled. 254 | sockets: { 255 | sio: true, 256 | direct: true 257 | }, 258 | accessDeniedUrl: '/pages/accessdenied.htm', 259 | // Admin and Player server specific options. 260 | // Can overwrite common ones. 261 | playerServer: playerServer, 262 | adminServer: adminServer 263 | }; 264 | 265 | // Returns TRUE to signal successful configuration of the server. 266 | return true; 267 | } 268 | -------------------------------------------------------------------------------- /conf/sio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # sio.js 3 | * Copyright(c) 2018 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Configuration file for Socket.io server in nodegame-server 7 | */ 8 | module.exports = configure; 9 | 10 | function configure(sio, servernode) { 11 | 12 | // Possible transports values are the base-names of the files in 13 | // node_modules/socket.io/lib/transports/. 14 | 15 | //sio.set('transports', ['websocket']); 16 | 17 | // This is good for speeding up IE8: 18 | //sio.set('transports', ['xhr-polling']); 19 | 20 | return true; 21 | } 22 | -------------------------------------------------------------------------------- /docs/readme: -------------------------------------------------------------------------------- 1 | Documentation temporarily removed, please refer to the project website: http://nodegame.org 2 | 3 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Example of Launcher file for nodeGame Server 3 | * Copyright(c) 2014 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Start File for nodeGame server with conf. 7 | * 8 | * Last update 05/02/2014 9 | */ 10 | 11 | // Load the Node.js path object. 12 | var path = require('path'); 13 | 14 | var ServerNode = require('nodegame-server').ServerNode; 15 | 16 | var options = { 17 | 18 | // defines an additional configuration directory 19 | confDir: './conf', 20 | 21 | // configures the ServerNode instance 22 | servernode: function(servernode) { 23 | servernode.verbosity = 10; 24 | servernode.gamesDirs.push('./games'); 25 | return true; 26 | }, 27 | 28 | // configure the logging system 29 | loggers: function(loggers) { 30 | // See winston configuration README 31 | // https://github.com/flatiron/winston/blob/master/README.md 32 | return true; 33 | }, 34 | 35 | // configure the HTTP server 36 | http: function(http) { 37 | // See express web server configuration api 38 | // http://expressjs.com/api.html 39 | return true; 40 | }, 41 | 42 | // configure the Socket.io server 43 | sio: function(sio) { 44 | // See Socket.io configuration wiki 45 | // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO 46 | return true; 47 | } 48 | } 49 | // Start the server 50 | 51 | // Input parameter is optional 52 | var sn = new ServerNode(options); 53 | 54 | // Add the game channel. 55 | var ultimatum = sn.addChannel({ 56 | name: 'ultimatum', 57 | admin: 'ultimatum/admin', 58 | player: 'ultimatum', 59 | verbosity: 100, 60 | // If TRUE, players can invoke GET commands on admins. 61 | getFromAdmins: true, 62 | // Unauthorized clients will be redirected here. 63 | // (defaults: "/pages/accessdenied.htm") 64 | accessDeniedUrl: '/ultimatum/unauth.htm' 65 | }); 66 | 67 | // Creates the room that will spawn the games for the channel. 68 | var logicPath = path.resolve('./mygames/ultimatum/server/game.room.js'); 69 | var gameRoom = ultimatum.createWaitingRoom({ 70 | logicPath: logicPath, 71 | name: 'gameRoom' 72 | }); 73 | 74 | // The game room will spawn sub-games as soon as enough players arrive. 75 | 76 | // Exports the whole ServerNode. 77 | module.exports = sn; 78 | -------------------------------------------------------------------------------- /games/README.md: -------------------------------------------------------------------------------- 1 | Add here new games 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # nodeGame-server 3 | * Copyright(c) 2014 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Server component for nodeGame 7 | * 8 | * http://www.nodegame.org 9 | */ 10 | exports.ServerNode = require('./lib/ServerNode.js'); 11 | 12 | // Exposing submodules. 13 | exports.express = require('express'); 14 | exports.winston = require('winston'); 15 | exports.sio = require('socket.io'); 16 | -------------------------------------------------------------------------------- /lib/GameLevel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # GameLevel 3 | * Copyright(c) 2016 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Wrapper of for a set of waiting, requirements and game rooms 7 | * 8 | * http://www.nodegame.org 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global 14 | 15 | module.exports = GameLevel; 16 | 17 | /** 18 | * ## GameLevel constructor 19 | * 20 | * Creates an instance of GameLevel 21 | * 22 | * @param {object} options Configuration object 23 | * @param {ServerChannel} channel The ServerChannel instance 24 | */ 25 | function GameLevel(options, channel) { 26 | 27 | /** 28 | * ### GameLevel.channel 29 | * 30 | * Reference to the Channel 31 | */ 32 | this.channel = channel; 33 | 34 | /** 35 | * ### GameLevel.registry 36 | * 37 | * The registry of all connected clients 38 | * 39 | * @see ChannelRegistry 40 | */ 41 | this.registry = this.channel.registry; 42 | 43 | /** 44 | * ### GameLevel.options 45 | * 46 | * Configuration options 47 | */ 48 | this.options = options; 49 | 50 | /** 51 | * ### GameLevel.name 52 | * 53 | * The name of the level 54 | */ 55 | this.name = options.name; 56 | 57 | /** 58 | * ### GameLevel.sysLogger 59 | * 60 | * The logger for the channel 61 | */ 62 | this.sysLogger = this.channel.sysLogger; 63 | 64 | /** 65 | * ### GameLevel.waitingRoom 66 | * 67 | * The waiting room for the channel. 68 | * 69 | * @see GameLevel.createWaitingRoom 70 | */ 71 | this.waitingRoom = null; 72 | 73 | /** 74 | * ### GameLevel.requirementsRoom 75 | * 76 | * The requirements room for the channel. 77 | * 78 | * @see GameLevel.createRequirementsRoom 79 | */ 80 | this.requirementsRoom = null; 81 | 82 | /** 83 | * ### GameLevel.gameRooms 84 | * 85 | * Associative container for all the game rooms in the channel. 86 | * 87 | * @see GameLevel.createGameRoom 88 | */ 89 | this.gameRooms = {}; 90 | 91 | // Creates waiting and requriements room. 92 | if (options.requirements && options.requirements.enabled) { 93 | this.createRequirementsRoom(options.requirements); 94 | } 95 | if (options.waitroom) { 96 | this.createWaitingRoom(options.waitroom); 97 | } 98 | 99 | // Sets the entry room, if either requirements or waiting is created. 100 | if (this.requirementsRoom) this.setEntryRoom(this.requirementsRoom); 101 | else if (this.waitingRoom) this.setEntryRoom(this.waitingRoom); 102 | } 103 | 104 | // ## GameLevel methods 105 | 106 | /** 107 | * ### GameLevel.getEntryRoom 108 | * 109 | * Returns the entry room, or null if none is set 110 | * 111 | * @return {Room} The entry room. Could be of any type. 112 | * 113 | * @see GameLevel.entryRoom 114 | * @see GameLevel.setEntryRoom 115 | */ 116 | GameLevel.prototype.getEntryRoom = function() { 117 | return this.entryRoom || null; 118 | }; 119 | 120 | /** 121 | * ### GameLevel.setEntryRoom 122 | * 123 | * Sets the entry room for the level 124 | * 125 | * @param {string|Room} room The entry room 126 | * 127 | * @see GameLevel.entryRoom 128 | * @see GameLevel.getEntryRoom 129 | */ 130 | GameLevel.prototype.setEntryRoom = function(room) { 131 | if ('string' === typeof room) { 132 | if (!this.gameRooms[room]) { 133 | throw new Error('GameLevel.setEntryRoom: room is not in level: ' + 134 | room); 135 | } 136 | room = this.gameRooms[room]; 137 | } 138 | else if ('object' !== typeof room) { 139 | throw new Error('GameLevel.setEntryRoom: room must be string or ' + 140 | 'object. Found: ' + room); 141 | } 142 | 143 | return this.entryRoom = room; 144 | }; 145 | 146 | /** 147 | * ### GameLevel.createGameRoom 148 | * 149 | * Creates a new game room using the channel api and stores a reference to it 150 | * 151 | * @param {object} conf Config object, acceptable by GameRoom constructor 152 | * 153 | * @return {GameRoom} The new GameRoom 154 | * 155 | * @see ServerChannel.createGameRoom 156 | */ 157 | GameLevel.prototype.createGameRoom = function(conf) { 158 | var room; 159 | conf = conf || {}; 160 | conf.gameLevel = this.name; 161 | room = this.channel.createGameRoom(conf); 162 | this.gameRooms[room.name] = room; 163 | return room; 164 | }; 165 | 166 | /** 167 | * ### GameLevel.createWaitingRoom 168 | * 169 | * Creates the waiting room using the channel api and stores a reference to it 170 | * 171 | * If a waiting room is already existing an error will be thrown. 172 | * 173 | * @param {object} settings Config object 174 | * 175 | * @return {WaitingRoom} The waiting room 176 | * 177 | * @see WaitingRoom 178 | * @see ServerChannel.createWaitingRoom 179 | */ 180 | GameLevel.prototype.createWaitingRoom = function(settings) { 181 | var room; 182 | if (this.waitingRoom) { 183 | throw new Error('GameLevel.createWaitingRoom: waiting room already ' + 184 | 'existing. Level: ' + this.name); 185 | } 186 | settings = settings || {}; 187 | settings.gameLevel = this.name; 188 | settings.name = settings.name || 'waiting_' + this.name; 189 | room = this.channel.createWaitingRoom(settings); 190 | this.waitingRoom = room; 191 | return room; 192 | }; 193 | 194 | /** 195 | * ### GameLevel.createRequirementsRoom 196 | * 197 | * Creates the requirements room using the channel api and stores a reference 198 | * 199 | * If a requirements room is already existing an error will be thrown. 200 | * 201 | * @param {object} settings Config object 202 | * 203 | * @return {RequirementsRoom} The requirements room 204 | * 205 | * @see RequirementsRoom 206 | * @see ServerChannel.createWaitingRoom 207 | */ 208 | GameLevel.prototype.createRequirementsRoom = function(settings) { 209 | var room; 210 | if (this.requirementsRoom) { 211 | throw new Error('GameLevel.requirementsRoom: requirements room ' + 212 | 'already existing. Level: ' + this.name); 213 | } 214 | settings = settings || {}; 215 | settings.gameLevel = this.name; 216 | settings.name = settings.name || 'requirements_' + this.name; 217 | room = this.channel.createRequirementsRoom(settings); 218 | this.requirementsRoom = room; 219 | return room; 220 | }; 221 | -------------------------------------------------------------------------------- /lib/GameMsgGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # GameMsgGenerator 3 | * Copyright(c) 2018 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Game messages generator 7 | * 8 | * TODO: obfuscate everything? Use SERVER and MONITOR and 'channel name'? 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global scope 14 | 15 | module.exports = GameMsgGenerator; 16 | 17 | const ngc = require('nodegame-client'); 18 | const GameMsg = ngc.GameMsg; 19 | const constants = ngc.constants; 20 | 21 | 22 | /** 23 | * ## GameMsgGenerator constructor 24 | * 25 | * Creates a new instance of GameMsgGenerator 26 | * 27 | * @param {GameServer} gameServer Reference to the Game Server where 28 | * the generator is instantiated 29 | */ 30 | function GameMsgGenerator(gameServer) { 31 | this.session = gameServer.session; 32 | this.sender = gameServer.name; 33 | this.stage = null; 34 | } 35 | 36 | // ## GameMsgGenerator methods 37 | 38 | /** 39 | * ### GameMsgGenerator.create 40 | * 41 | * Primitive for creating any type of game-message 42 | * 43 | * Merges a set of default settings with the object passed 44 | * as input parameter. 45 | * 46 | * @param {object} msg A stub of game message 47 | * 48 | * @return {GameMsg} The newly created message 49 | */ 50 | GameMsgGenerator.prototype.create = function(msg) { 51 | var priority; 52 | 53 | if ('undefined' !== typeof msg.priority) { 54 | priority = msg.priority; 55 | } 56 | else if (msg.target === constants.target.GAMECOMMAND || 57 | msg.target === constants.target.REDIRECT || 58 | msg.target === constants.target.PCONNECT || 59 | msg.target === constants.target.PDISCONNECT || 60 | msg.target === constants.target.PRECONNECT) { 61 | 62 | priority = 1; 63 | } 64 | else { 65 | priority = 0; 66 | } 67 | 68 | return new GameMsg({ 69 | session: 70 | 'undefined' !== typeof msg.session ? msg.session : this.session, 71 | stage: msg.stage || this.stage, 72 | action: msg.action || constants.action.SAY, 73 | target: msg.target || constants.target.DATA, 74 | from: msg.from || this.sender, 75 | to: 'undefined' !== typeof msg.to ? msg.to : 'SERVER', 76 | text: 'undefined' !== typeof msg.text ? "" + msg.text : null, 77 | data: 'undefined' !== typeof msg.data ? msg.data : {}, 78 | priority: priority, 79 | reliable: msg.reliable || 1 80 | }); 81 | }; 82 | 83 | /** 84 | * ### GameMsgGenerator.obfuscate 85 | * 86 | * Overwrites the session, stage, and from properties of a game message 87 | * 88 | * @param {GameMsg} msg The game message 89 | * 90 | * @return {GameMsg} The obfuscated message 91 | */ 92 | GameMsgGenerator.prototype.obfuscate = function(msg) { 93 | msg.session = this.session; 94 | msg.stage = this.stage; 95 | msg.from = this.sender; 96 | return msg; 97 | }; 98 | -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Logger 3 | * Copyright(c) 2017 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Interface to Winston.js loggers 7 | * 8 | * @see Winston 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global scope 14 | 15 | const winston = require('winston'); 16 | 17 | module.exports = LoggerManager; 18 | 19 | /** 20 | * ## LoggerManager constructor 21 | * 22 | * Creates an instance of LoggerManager 23 | */ 24 | function LoggerManager() {} 25 | 26 | /** 27 | * ## LoggerManager.get 28 | * 29 | * Returns a new Logger object with given name and options 30 | * 31 | * Available loggers: 'server', 'channel', 'clients', 'messages'. 32 | * 33 | * @param {string} logger The name of the logger, e.g. 'channel' 34 | * @param {object} options Optional. Configuration option. An important 35 | * options is the name of the logger, which will be added to every log entry 36 | * 37 | * @return {Logger} The logger object 38 | * 39 | * @see Logger 40 | */ 41 | LoggerManager.get = function(logger, options) { 42 | return new Logger(logger, options); 43 | }; 44 | 45 | /** 46 | * ## Logger constructor 47 | * 48 | * Creates an instance of Logger 49 | * 50 | * @param {string} logger The name of the logger, e.g. 'channel' 51 | * @param {object} options Optional. Configuration options 52 | */ 53 | function Logger(logger, options) { 54 | options = options || {}; 55 | 56 | /** 57 | * ### Logger.logger 58 | * 59 | * The Winston logger 60 | * 61 | * @see Winston 62 | */ 63 | this.logger = winston.loggers.get(logger); 64 | 65 | /** 66 | * ### Logger.name 67 | * 68 | * The name of the logger (might be the name of the channel). 69 | * 70 | * @see ServerChannel 71 | */ 72 | this.name = options.name; 73 | 74 | // Set the appropriate log function. 75 | if (logger === 'messages') this.log = this.logMsg; 76 | else this.log = this.logText; 77 | } 78 | 79 | // ## Logger methods. 80 | 81 | /** 82 | * ### Logger.logText 83 | * 84 | * Default log function for text 85 | * 86 | * Logs to stdout and / or to file depending on configuration. 87 | * 88 | * @param {string|mixed} text The string to log. If not a string, 89 | * it is stringified 90 | * @param {string|number} level The log level for this log 91 | * @param {object} meta Additional meta-data to be logged. `meta.name` 92 | * is automatically added, if the logger has a name. 93 | * 94 | * @see Winston 95 | */ 96 | Logger.prototype.logText = function(text, level, meta = {}) { 97 | if ('string' !== typeof text) text = JSON.stringify(text); 98 | if (this.name) meta.name = this.name; 99 | this.logger.log(level || 'info', text, meta); 100 | }; 101 | 102 | /** 103 | * ### Logger.logMsg 104 | * 105 | * Log function for game messages 106 | * 107 | * Logs a game message as Winston metadata, and add the `name` attribute 108 | * containing the name of the logger (if any). 109 | * 110 | * Logs to stdout and / or to file depending on configuration. 111 | * 112 | * @param {GameMsg} gm The game message 113 | * @param {string} type A string associated with the type of message, 114 | * for example: 'in' or 'out'. 115 | * @param {string} gmString Optional. The gm already stringified. 116 | * 117 | * @see GameMsg 118 | * @see Winston 119 | */ 120 | Logger.prototype.logMsg = function(gm, type, gmString) { 121 | var level, curLevel; 122 | 123 | // TODO: this might not be accurate in winston v3. 124 | curLevel = this.logger.levels[this.logger.level]; 125 | if (curLevel === 0) return; 126 | 127 | // Important! The numeric values are hard-coded and copied from 128 | if (gm.target === 'DATA') { 129 | level = 'data'; 130 | } 131 | else if (curLevel < 3) { 132 | return; 133 | } 134 | else if (gm.action === 'set') { 135 | level = 'set'; 136 | } 137 | else if (curLevel < 5) { 138 | return; 139 | } 140 | else if (gm.action === 'get') { 141 | level = 'get'; 142 | } 143 | else if (curLevel < 7) { 144 | return; 145 | } 146 | else if (gm.target === 'setup') { 147 | level = 'setup'; 148 | } 149 | else if (curLevel < 9) { 150 | return; 151 | } 152 | else if (gm.target !== 'PLAYER_UPDATE' && 153 | gm.target !== 'SERVERCOMMAND' && 154 | gm.target !== 'ALERT') { 155 | 156 | level = 'game'; 157 | } 158 | else if (curLevel < 11) { 159 | return; 160 | } 161 | else { 162 | level = 'all'; 163 | } 164 | 165 | if (!gmString) gmString = JSON.stringify(gm); 166 | 167 | // // Manual clone. 168 | // meta.id = gm.id; 169 | // meta.session = gm.session; 170 | // if (gm.stage && 'object' === typeof gm.stage) { 171 | // meta.stage = gm.stage.stage + '.' + 172 | // gm.stage.step + '.' + gm.stage.round; 173 | // } 174 | // else { 175 | // meta.stage = gm.stage; 176 | // } 177 | // meta.action = gm.action; 178 | // meta.target = gm.target; 179 | // meta.from = gm.from; 180 | // meta.to = gm.to; 181 | // meta.text = gm.text; 182 | // 183 | // if (meta.data && 184 | // 'object' === typeof meta.data || 185 | // 'function' === typeof meta.data) { 186 | // 187 | // meta.data = JSON.stringify(gm.data); 188 | // } 189 | // else { 190 | // meta.data = gm.data; 191 | // } 192 | // 193 | // meta.data = gm.data; 194 | // meta.priority = gm.priority; 195 | // meta.reliable = gm.reliable; 196 | // meta.created = gm.created; 197 | // meta.forward = gm.forward; 198 | 199 | this.logger.log(level, gmString, { 200 | channel: this.name, 201 | type: type 202 | }); 203 | }; 204 | -------------------------------------------------------------------------------- /lib/rooms/GarageRoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Garage Room 3 | * Copyright(c) 2018 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Garage room, does nothing, just keep clients in. 7 | * 8 | * http://www.nodegame.org 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global scope 14 | module.exports = GarageRoom; 15 | 16 | const Room = require('./Room'); 17 | 18 | GarageRoom.prototype.__proto__ = Room.prototype; 19 | GarageRoom.prototype.constructor = GarageRoom; 20 | 21 | function GarageRoom(config) { 22 | config = config || {}; 23 | config.acm = { 24 | notify: { 25 | onConnect: false, 26 | onStageUpdate: false, 27 | onStageLevelUpdate: false, 28 | onStageLoadedUpdated: false 29 | } 30 | }; 31 | // No node instance in the garage. 32 | config.node = null; 33 | Room.call(this, 'Garage', config); 34 | this.registerRoom(); 35 | } 36 | 37 | // Empty methods. 38 | 39 | GarageRoom.prototype.setupGame = function() { 40 | // Nothing. 41 | }; 42 | GarageRoom.prototype.startGame = function() { 43 | // Nothing. 44 | }; 45 | GarageRoom.prototype.pauseGame = function() { 46 | // Nothing. 47 | }; 48 | GarageRoom.prototype.resumeGame = function() { 49 | // Nothing. 50 | }; 51 | GarageRoom.prototype.stopGame = function() { 52 | // Nothing. 53 | }; 54 | -------------------------------------------------------------------------------- /lib/rooms/HostRoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Host Room 3 | * Copyright(c) 2019 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * An host room keeps the client before moving them to another room. 7 | * 8 | * Examples: requirements and waiting room. 9 | * 10 | * http://nodegame.org 11 | */ 12 | 13 | "use strict"; 14 | 15 | // ## Global scope 16 | module.exports = HostRoom; 17 | 18 | const J = require('JSUS').JSUS; 19 | 20 | const Room = require('./Room'); 21 | 22 | HostRoom.prototype.__proto__ = Room.prototype; 23 | HostRoom.prototype.constructor = HostRoom; 24 | 25 | function HostRoom(type, config) { 26 | config = config || {}; 27 | if (!config.acm) { 28 | config.acm = { 29 | notify: { 30 | onConnect: false, 31 | onStageUpdate: false, 32 | onStageLevelUpdate: false, 33 | onStageLoadedUpdated: false 34 | } 35 | }; 36 | } 37 | Room.call(this, type, config); 38 | this.registerRoom(); 39 | } 40 | 41 | /** 42 | * ### HostRoom.setupGame 43 | * 44 | * Setups the logic game objectx 45 | * 46 | * @param {object} mixinConf Optional. Additional options to pass to the node 47 | * instance of the room. Will override default settings of the game 48 | */ 49 | HostRoom.prototype.setupGame = function(mixinConf) { 50 | var game, channel, node, room, settings, runtimeConf; 51 | 52 | if (mixinConf && 'object' !== typeof mixinConf) { 53 | throw new TypeError(this.roomType + 54 | 'Room.setupGame: mixinConf must be ' + 55 | 'object or undefined. Found: ' + mixinConf); 56 | } 57 | node = this.node; 58 | channel = this.channel; 59 | settings = this.settings; 60 | runtimeConf = this.runtimeConf; 61 | room = this; 62 | 63 | // Require game configuration for the logic. 64 | game = require(this.logicPath)(settings, room, runtimeConf); 65 | 66 | if ('object' !== typeof game) { 67 | throw new Error(this.roomType + 'Room.setupGame: room ' + this.name + 68 | ': logicPath did not return a valid game ' + 69 | 'object. Found: ' + game); 70 | } 71 | 72 | // Mixin-in nodeGame options. 73 | if (mixinConf) J.mixin(game, mixinConf); 74 | 75 | // Setup must be called before connect. 76 | node.setup('nodegame', game); 77 | 78 | // Connect logic to server, if not already connected. E.g., after a stop 79 | // command it is generally still connected. Or the node object can be 80 | // passed in the constructor and be already connected. 81 | if (!node.socket.isConnected()) { 82 | // Mixin-in nodeGame options. 83 | node.socket.setSocketType('SocketDirect', { 84 | socket: channel.adminServer.socketManager.sockets.direct 85 | }); 86 | node.connect(null, { 87 | startingRoom: this.name, 88 | clientType: 'logic' 89 | }); 90 | } 91 | }; 92 | 93 | 94 | /** 95 | * ### HostRoom.startGame 96 | * 97 | * Starts the game, optionally starts connected players 98 | * 99 | * @param {boolean} startPlayers If TRUE, sends a start command to all players. 100 | * Default: False. 101 | */ 102 | HostRoom.prototype.startGame = function(startPlayers) { 103 | var node; 104 | node = this.node; 105 | 106 | if (!node.game.isStartable()) { 107 | this.channel.sysLogger.log( 108 | this.roomType + 'Room.startGame: game cannot be started.', 'warn'); 109 | return; 110 | } 111 | node.game.start(); 112 | if (startPlayers) { 113 | this.clients.player.each(function(p) { 114 | node.remoteCommand('start', p.id); 115 | }); 116 | } 117 | }; 118 | 119 | /** 120 | * ### HostRoom.pauseGame 121 | * 122 | * Pauses the game, optionally pauses connected players 123 | * 124 | * @param {boolean} pausePlayers If TRUE, sends a pause command to all players. 125 | * Default: False. 126 | */ 127 | HostRoom.prototype.pauseGame = function(pausePlayers) { 128 | var node; 129 | node = this.node; 130 | 131 | if (!node.game.isPausable()) { 132 | this.channel.sysLogger.log(this.roomType + 133 | 'Room.pauseGame: game cannot be paused.', 134 | 'warn'); 135 | return; 136 | } 137 | 138 | node.game.pause(); 139 | if (pausePlayers) { 140 | this.clients.player.each(function(p) { 141 | node.remoteCommand('pause', p.id); 142 | }); 143 | } 144 | }; 145 | 146 | /** 147 | * ### HostRoom.resumeGame 148 | * 149 | * Resumes the game, optionally resumes connected players 150 | * 151 | * @param {boolean} resumePlayers If TRUE, sends a resume command to 152 | * all players. Default: False. 153 | */ 154 | HostRoom.prototype.resumeGame = function(resumePlayers) { 155 | var node; 156 | node = this.node; 157 | 158 | if (!node.game.isResumable()) { 159 | this.channel.sysLogger.log(this.roomType + 160 | 'Room.resumeGame: game cannot be resumed.', 161 | 'warn'); 162 | return; 163 | } 164 | 165 | node.game.resume(); 166 | if (resumePlayers) { 167 | this.clients.player.each(function(p) { 168 | node.remoteCommand('resume', p.id); 169 | }); 170 | } 171 | }; 172 | 173 | /** 174 | * ### HostRoom.stopGame 175 | * 176 | * Stops the game, optionally stops connected players 177 | * 178 | * @param {boolean} stopPlayers If TRUE, sends a stop command to all players. 179 | * Default: False. 180 | */ 181 | HostRoom.prototype.stopGame = function(stopPlayers) { 182 | var node; 183 | node = this.node; 184 | 185 | if (!node.game.isStoppable()) { 186 | this.channel.sysLogger.log(this.roomType + 187 | 'Room.stopGame: game cannot be stopped.', 188 | 'warn'); 189 | return; 190 | } 191 | 192 | node.game.stop(); 193 | if (stopPlayers) { 194 | this.clients.player.each(function(p) { 195 | node.remoteCommand('stop', p.id); 196 | }); 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /lib/rooms/RequirementsRoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Requirements Room 3 | * Copyright(c) 2019 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Requirements room representation 7 | * 8 | * http://www.nodegame.org 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global scope 14 | module.exports = RequirementsRoom; 15 | 16 | var ngt = require('nodegame-game-template'); 17 | 18 | var HostRoom = require('./HostRoom'); 19 | 20 | RequirementsRoom.prototype.__proto__ = HostRoom.prototype; 21 | RequirementsRoom.prototype.constructor = RequirementsRoom; 22 | 23 | function RequirementsRoom(config) { 24 | HostRoom.call(this, 'Requirements', config); 25 | if (!this.logicPath) { 26 | this.logicPath = ngt.resolve('requirements/requirements.room.js'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/rooms/Room.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Room 3 | * Copyright(c) 2019 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Basic room representation 7 | * 8 | * http://www.nodegame.org 9 | */ 10 | 11 | "use strict"; 12 | 13 | // ## Global scope 14 | module.exports = Room; 15 | 16 | const ngc = require('nodegame-client'); 17 | const PlayerList = ngc.PlayerList; 18 | const J = require('JSUS').JSUS; 19 | const fs = require('fs'); 20 | const path = require('path'); 21 | 22 | /** 23 | * ## verifyInput 24 | * 25 | * Sanity check for Room constructor parameters 26 | * 27 | * @param {object} config Configuration object 28 | * 29 | * @return {string} Returns a text representing an error, if found 30 | */ 31 | function verifyInput(config) { 32 | if ('object' !== typeof config) { 33 | return 'config must be object.'; 34 | } 35 | if ('string' !== typeof config.group) { 36 | return 'config.group must be string.'; 37 | } 38 | if ('string' !== typeof config.name) { 39 | return 'config.name must be string.'; 40 | } 41 | if (config.clients && !(config.clients instanceof PlayerList)) { 42 | return 'config.clients must be PlayerList or undefined.'; 43 | } 44 | if (config.logicPath && 'string' !== typeof config.logicPath) { 45 | return 'config.logicPath must be string or undefined.'; 46 | } 47 | if ('object' !== typeof config.channel) { 48 | return 'config.channel must be object.'; 49 | } 50 | if (config.runtimeConf && 'object' !== typeof config.runtimeConf) { 51 | return 'config.channel must be object.'; 52 | } 53 | return; 54 | } 55 | 56 | var roomTypes = { 57 | 'Basic': '', // TODO: is it still needed? 58 | 'Requirements': '', 59 | 'Game': '', 60 | 'Waiting': '', 61 | 'Garage': '' 62 | }; 63 | 64 | 65 | /** 66 | * ## Room constructor 67 | * 68 | * Creates a new instance of Room 69 | * 70 | * @param {object} config Initialization values for the Room object 71 | */ 72 | function Room(type, config) { 73 | var res; 74 | 75 | if (!(type in roomTypes)) { 76 | throw new Error('Room: unknown room type: ' + type); 77 | } 78 | 79 | res = verifyInput(config); 80 | if (res) throw new TypeError(type + 'Room: ' + res); 81 | 82 | /** 83 | * ### Room.roomType 84 | * 85 | * Room type flag 86 | */ 87 | this.roomType = type; 88 | 89 | /** 90 | * ### Room.roomType 91 | * 92 | * GameRoom flag 93 | */ 94 | this.gameRoom = type === 'Game'; 95 | 96 | /** 97 | * ### Room.name 98 | * 99 | * Name of the room 100 | */ 101 | this.name = config.name; 102 | 103 | /** 104 | * ### Room.group 105 | * 106 | * The group to which the game room belongs 107 | */ 108 | this.group = config.group; 109 | 110 | /** 111 | * ### Room.channel 112 | * 113 | * Reference to the room's ServerChannel 114 | */ 115 | this.channel = config.channel; 116 | 117 | /** 118 | * ### Room.gameName 119 | * 120 | * The name of the game played in this room 121 | */ 122 | this.gameName = this.channel.gameName; 123 | 124 | /** 125 | * ### Room.game 126 | * 127 | * Reference to the game info object contained in servernode 128 | */ 129 | this.game = this.channel.servernode.getGamesInfo(this.gameName); 130 | 131 | if (!this.game) { 132 | throw new Error(type + 'Room: game not found: ' + this.gameName); 133 | } 134 | 135 | /** 136 | * ### Room.gameLevel 137 | * 138 | * Game paths for the room 139 | */ 140 | this.gameLevel = config.gameLevel; 141 | 142 | if (this.gameLevel && !this.game.levels[this.gameLevel]) { 143 | throw new Error(type + 'Room: game ' + this.gameName + ' game level ' + 144 | 'not found: ' + this.gameLevel); 145 | } 146 | 147 | /** 148 | * ### Room.channelOptions 149 | * 150 | * Action control management for player server 151 | * 152 | * It can overwrite default player server settings. 153 | */ 154 | if (J.isEmpty(config.acm)) this.acm = this.channel.playerServer.acm; 155 | else this.acm = J.merge(this.channel.playerServer.acm, config.acm); 156 | if (!this.acm.notify) this.acm.notify = {}; 157 | 158 | /** 159 | * ### Room.clients 160 | * 161 | * PlayerList containing players and admins inside the room 162 | */ 163 | this.clients = new ngc.PlayerList(); 164 | 165 | // Adding views to distinguish admins and players. 166 | // Note: this.node.player will be added to admins. 167 | this.clients.view('admin', function(p) { 168 | return p.admin ? p.id : undefined; 169 | }); 170 | this.clients.view('player', function(p) { 171 | return !p.admin ? p.id : undefined; 172 | }); 173 | this.clients.view('playerConnected', function(p) { 174 | return !p.admin && p.connected ? p.id : undefined; 175 | }); 176 | 177 | // Importing initial clients, if any. 178 | if (config.clients && config.clients.length) { 179 | this.clients.importDB(config.clients); 180 | } 181 | 182 | /** 183 | * ### Room.parentRoom 184 | * 185 | * Name of the room's parent or null 186 | */ 187 | this.parentRoom = null; 188 | 189 | /** 190 | * ### Room.logicPath 191 | * 192 | * Game logic path for the room 193 | */ 194 | this.logicPath = config.logicPath || null; 195 | 196 | /** 197 | * ### Room.runtimeConf 198 | * 199 | * Extra configuration that can be accessed by the logic. 200 | */ 201 | this.runtimeConf = config.runtimeConf || {}; 202 | 203 | /** 204 | * ### Room.settings 205 | * 206 | * Settings for the actual game room (node instance) 207 | */ 208 | this.settings = config.settings || {}; 209 | 210 | /** 211 | * ### Room.node 212 | * 213 | * node instance 214 | */ 215 | this.node = config.node === null ? null : config.node || ngc.getClient(); 216 | 217 | /** 218 | * ### Room.id 219 | * 220 | * Random global unique room identifier. 221 | * 222 | * @see ServerNode.rooms 223 | */ 224 | this.id = null; 225 | 226 | // Code for Logging Client Events 227 | ///////////////////////////////// 228 | 229 | // TODO: Should it be in an own object? 230 | 231 | /** 232 | * ### Room.timeoutSave 233 | * 234 | * Contains a reference to timeout object saving the cache 235 | */ 236 | this.timeoutSave = null; 237 | 238 | /** 239 | * ### Room.cacheToSave 240 | * 241 | * A string waiting to be flushed to fs 242 | * 243 | * @see ServerNode.timeoutSave 244 | * @see ServerNode.cacheDumpInterval 245 | */ 246 | this.cacheToSave = ''; 247 | 248 | /** 249 | * ### Room.cacheDumpInterval 250 | * 251 | * Saves the cache every X milliseconds 252 | */ 253 | this.cacheDumpInterval = config.logClientsInterval || 10000; 254 | 255 | /** 256 | * ### Room.logClients 257 | * 258 | * If TRUE, clients events will be logged 259 | */ 260 | this.logClients = config.logClients || false; 261 | 262 | /** 263 | * ### Room.logClientsExtra 264 | * 265 | * A function adding extra properties info the clients log. 266 | */ 267 | this.logClientsExtra = config.logClientsExtra || null; 268 | 269 | /** 270 | * ### Room.logFilePath 271 | * 272 | * Path to the log file where the cache is dumped. 273 | */ 274 | this.logFilePath = path.join(this.channel.getGameDir(), 275 | 'log', (this.name + '.csv')); 276 | 277 | /** 278 | * ### Room._addNewLine 279 | * 280 | * Internal flag if the log should add a new line at the beginning. 281 | * 282 | * @api private 283 | */ 284 | this._newLineLog = fs.existsSync(this.logFilePath); 285 | /////////////////////////////////////////////////////////////// 286 | } 287 | 288 | // ## Room methods 289 | 290 | /** 291 | * ### Room.registerRoom 292 | * 293 | * Creates a random unique id for the room and save a reference in servernode 294 | */ 295 | Room.prototype.registerRoom = function() { 296 | this.id = J.uniqueKey(this.channel.servernode.rooms); 297 | if (!this.id) { 298 | throw new TypeError(this.roomType + 'Room ' + this.name + ' failed ' + 299 | 'to generate random global unique identifier'); 300 | } 301 | // Register room globally. 302 | this.channel.servernode.rooms[this.id] = this; 303 | }; 304 | 305 | /** 306 | * ### Room.size 307 | * 308 | * Returns the number of players|admin in the room 309 | * 310 | * @param {string} mod Optional. Modifies the return value. Available values: 311 | * 312 | * - 'player-connected': (default) The number of players currently connected 313 | * 314 | * @return {number} The number of clients connected 315 | * 316 | * @experimental 317 | */ 318 | Room.prototype.size = function(mod) { 319 | mod = mod || 'playerConnected'; 320 | if (mod && 'string' !== typeof mod) { 321 | throw new TypeError('Room.size: mod must be string or undefined. ' + 322 | 'Found: ' + mod); 323 | } 324 | 325 | switch(mod) { 326 | case 'playerConnected': 327 | return this.clients.playerConnected.size(); 328 | } 329 | }; 330 | 331 | /** 332 | * ### Room.update 333 | * 334 | * Updates a live settings value 335 | * 336 | * Throws an error if update is not possible, e.g., property not existing. 337 | * 338 | * @param {string} prop The name of the property to update 339 | * @param {mixed} value The updated value 340 | * 341 | * @return {mixed} The old value of the updated property 342 | * 343 | * @experimental 344 | */ 345 | Room.prototype.update = function(prop, value) { 346 | var old; 347 | if ('string' !== typeof prop) { 348 | throw new TypeError(this.roomType + 'Room ' + this.name + '.update: ' + 349 | 'prop must be string. Found: ' + prop); 350 | } 351 | if (!this.hasOwnProperty(prop)) { 352 | throw new TypeError(this.roomType + 'Room ' + this.name + '.update: ' + 353 | 'property not found: ' + prop); 354 | } 355 | old = this[prop]; 356 | this[prop] = value; 357 | // TODO: emit it somehow. 358 | return old; 359 | }; 360 | 361 | /** 362 | * ### Room.log 363 | * 364 | * Logs a string to file inside the logs folder 365 | * 366 | * @param {array} arr Array of values to log to be joined with a comma 367 | * 368 | * @experimental 369 | */ 370 | Room.prototype.log = function(arr) { 371 | var that, str; 372 | 373 | if (!this.logFilePath) { 374 | throw new Error(this.roomType + 'Room ' + this.name + '.log: ' + 375 | 'logFilePath is null'); 376 | } 377 | // See if we need to make a new line. 378 | if (this._newLineLog) this.cacheToSave += "\n"; 379 | else this._newLineLog = true; 380 | // Prepare log string. 381 | str = new Date().toISOString(); 382 | str = str.substring(0, str.length - 5); 383 | // Log: Time,Session,Data. 384 | this.cacheToSave += str + "," + this.channel.session + "," + arr.join(','); 385 | if (!this.timeoutSave) { 386 | that = this; 387 | this.timeoutSave = setTimeout(function() { 388 | var txt; 389 | txt = that.cacheToSave; 390 | that.cacheToSave = ''; 391 | that.timeoutSave = null; 392 | fs.appendFile(that.logFilePath, txt, function(err) { 393 | if (err) { 394 | console.log(txt); 395 | console.log(err); 396 | } 397 | }); 398 | }, this.cacheDumpInterval); 399 | } 400 | }; 401 | 402 | /** 403 | * ### Room.logClientEvent 404 | * 405 | * Checks the room and channel settings and eventually logs a client event 406 | * 407 | * @param {object} p A player object 408 | * @param {string} event The client event, e.g., "connect" 409 | * 410 | * @experimental 411 | */ 412 | Room.prototype.logClientEvent = function(p, event) { 413 | var log; 414 | if (this.logClients) { 415 | log = [ Date.now(), event, p.id ]; 416 | if (this.logClientsExtra) { 417 | log = log.concat(this.logClientsExtra(p)); 418 | } 419 | this.log(log); 420 | } 421 | }; 422 | -------------------------------------------------------------------------------- /lib/sockets/SocketDirect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # SocketDirect 3 | * Copyright(c) 2016 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Manages the communication with clients as objects in memory 7 | * 8 | * Message passing is handles with a shared object between the 9 | * client and this socket. 10 | * 11 | * @see SocketIO 12 | * @see GameServer 13 | * @see nodegame-client/lib/sockets/SocketDirect.js 14 | */ 15 | 16 | "use strict"; 17 | 18 | // Global scope 19 | 20 | module.exports = SocketDirect; 21 | 22 | const ngc = require('nodegame-client'); 23 | const J = ngc.JSUS; 24 | 25 | /** 26 | * ## SocketDirect constructor 27 | * 28 | * Creates an instance of SocketDirect 29 | * 30 | * @param {GameServer} server A GameServer instance 31 | */ 32 | function SocketDirect(gameServer) { 33 | this.name = 'direct'; 34 | this.gameServer = gameServer; 35 | this.clients = {}; 36 | 37 | // Must be unique for every socket. 38 | // If this changes we need change also ChannelRegistry.isRemote(). 39 | this.sidPrefix = gameServer.ADMIN_SERVER ? 'DA' : 'DP'; 40 | } 41 | 42 | 43 | // ## SocketDirect methods 44 | 45 | /** 46 | * ### SocketDirect.generateID 47 | * 48 | * Generates a unique identifier for newly connecting clients 49 | * 50 | * All Identiers starts with the same prefix. 51 | * 52 | * @return {string} A unique identifier 53 | * 54 | * @see SocketDirect.sidPrefix 55 | * @see SocketDirect.clients 56 | */ 57 | SocketDirect.prototype.generateID = function() { 58 | return J.uniqueKey(this.clients, this.sidPrefix); 59 | }; 60 | 61 | /** 62 | * ### SocketDirect.connect 63 | * 64 | * Registers a newly connected client 65 | * 66 | * @param {SocketDirect} clientSocket The Socket of the connecting client 67 | * @param {object} options Configuration options 68 | * 69 | * @return {boolean} TRUE if the server accepts the connection 70 | * 71 | * @see GameServer.onConnect 72 | */ 73 | SocketDirect.prototype.connect = function(clientSocket, options) { 74 | var sid, res; 75 | options = options || {}; 76 | 77 | // Generate the socket id for this connection. 78 | sid = this.generateID(); 79 | 80 | // Adds the socket temporarily (GameServer will start 81 | // sending a message before this methods exits). 82 | this.clients[sid] = clientSocket; 83 | 84 | // Ask GameServer to register the client with the ChannelRegistry. 85 | res = this.gameServer.onConnect(sid, this, options.handshake || {}, 86 | options.clientType, options.startingRoom, 87 | options.id); 88 | 89 | // If an error occurred in the GameServer or ChannelRegistry 90 | // we delete the socket. 91 | if (!res) delete this.clients[sid]; 92 | return res; 93 | }; 94 | 95 | /** 96 | * ### SocketDirect.message 97 | * 98 | * Delivers an incoming message to the server 99 | * 100 | * @param {GameMSg} msg A game message 101 | */ 102 | SocketDirect.prototype.message = function(msg) { 103 | var that; 104 | 105 | that = this; 106 | // Async message handling to avoid recursion problems 107 | // (e.g. Game.publishUpdate calling itself). 108 | setTimeout(function() { 109 | that.gameServer.onMessage(msg); 110 | }, 0); 111 | }; 112 | 113 | /** 114 | * ### SocketDirect.disconnect 115 | * 116 | * Disconnects the client registered under the specified socket id 117 | * 118 | * @param {string} sid The socket id of the client to disconnect 119 | */ 120 | SocketDirect.prototype.disconnect = function(sid) { 121 | var socket = this.clients[sid]; 122 | delete this.clients[sid]; 123 | this.gameServer.onDisconnect(sid, socket); 124 | }; 125 | 126 | /** 127 | * ### SocketDirect.attachListeners 128 | * 129 | * Activates the socket to accepts incoming connections 130 | */ 131 | SocketDirect.prototype.attachListeners = function() {}; 132 | 133 | /** 134 | * ### SocketDirect.send 135 | * 136 | * Sends a game message to the client registered under the specified socket id 137 | * 138 | * The _to_ field of the game message is usually different from the _sid_ and 139 | * it is not used to locate the client. 140 | * 141 | * @param {GameMsg} msg The game message 142 | * @param {string} sid The socket id of the receiver 143 | * @param {boolean} broadcast If TRUE, the message will not be sent to 144 | * the client with sid = sid. Default: FALSE 145 | * @return {boolean} TRUE, on success 146 | */ 147 | SocketDirect.prototype.send = function(msg, sid, broadcast) { 148 | // var rel; 149 | var client; 150 | var sysLogger, msgLogger; 151 | 152 | 153 | if (sid === 'SERVER' || sid === null) { 154 | sysLogger.log('SocketDirect.send: Trying to send a msg to nobody.', 155 | 'error'); 156 | return false; 157 | } 158 | 159 | broadcast = broadcast || false; 160 | 161 | sysLogger = this.gameServer.sysLogger; 162 | msgLogger = this.gameServer.msgLogger; 163 | 164 | if (sid === 'ALL' && broadcast) { 165 | sysLogger.log('SocketDirect.send: Incompatible options: ' + 166 | 'broadcast = true and sid = ALL', 'error'); 167 | return false; 168 | } 169 | 170 | // Cleanup msg, if necessary. 171 | if (msg._to) { 172 | if (!msg._originalTo) { 173 | sysLogger.log('SocketDirect.send: _to field found, but no ' + 174 | '_originalTo.', 'error'); 175 | return false; 176 | } 177 | msg.to = msg._orginalTo; 178 | delete msg._to; 179 | delete msg._originalTo; 180 | } 181 | 182 | if (msg._sid) delete msg._sid; 183 | if (msg._channelName) delete msg._channelName; 184 | if (msg._roomName) delete msg._roomName; 185 | 186 | // Not used at the moment (keep for future implementation). 187 | // rel = msg.reliable; 188 | 189 | // Send to ALL. 190 | if (sid === 'ALL') { 191 | 192 | for (client in this.clients) { 193 | if (this.clients.hasOwnProperty(client)) { 194 | this.clients[client].message(msg); 195 | } 196 | } 197 | 198 | sysLogger.log('Msg ' + msg.toSMS() + ' sent to ' + sid); 199 | msgLogger.log(msg, 'out-ALL'); 200 | } 201 | 202 | // Either send to a specific client(1), or to ALL but a specific client(2). 203 | else { 204 | 205 | // (1) 206 | if (!broadcast) { 207 | client = this.clients[sid]; 208 | 209 | if (client) { 210 | client.message(msg); 211 | sysLogger.log('Msg ' + msg.toSMS() + ' sent to ' + sid); 212 | msgLogger.log(msg, 'out-ID'); 213 | } 214 | else { 215 | sysLogger.log('SocketDirect.send: msg not sent. Unexisting ' + 216 | 'recipient: ' + sid, 'error'); 217 | return false; 218 | } 219 | } 220 | // (2) 221 | else { 222 | 223 | for (client in this.clients) { 224 | if (this.clients.hasOwnProperty(client)) { 225 | // No self-send 226 | if (client !== sid) { 227 | this.clients[client].message(msg); 228 | sysLogger.log('Msg ' + msg.toSMS() + ' sent to ' + sid); 229 | msgLogger.log(msg, 'out-BROADCAST'); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | return true; 236 | }; 237 | -------------------------------------------------------------------------------- /lib/sockets/SocketIo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # SocketIo 3 | * Copyright(c) 2021 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Handles network connections through Socket.IO 7 | */ 8 | 9 | "use strict"; 10 | 11 | // Global scope 12 | 13 | module.exports = SocketIo; 14 | 15 | /** 16 | * ## SocketIo constructor 17 | * 18 | * Creates an instance of SocketIo 19 | * 20 | * @param {GameServer} server A GameServer instance 21 | * 22 | * @see GameServer 23 | */ 24 | function SocketIo(gameServer) { 25 | 26 | /** 27 | * ### SocketIo.name 28 | * 29 | * The name of the socket. 30 | */ 31 | this.name = 'sio'; 32 | 33 | /** 34 | * ### Socket.gameServer 35 | * 36 | * Reference to the game server in which the socket is created. 37 | * 38 | * @see GameServer 39 | */ 40 | this.gameServer = gameServer; 41 | 42 | /** 43 | * ### Socket.sio 44 | * 45 | * Reference to the Socket.IO app of the game server 46 | * 47 | * @see GameServer 48 | */ 49 | this.sio = this.gameServer.sio; 50 | 51 | /** 52 | * ### SocketIo.sioChannel 53 | * 54 | * The Socket.IO internal channel 55 | */ 56 | this.sioChannel = null; 57 | 58 | /** 59 | * ### SocketIo.sidPrefix 60 | * 61 | * The prefix to be added to the id of every client connected to the socket 62 | * 63 | * Must have exactly 2 characters and be unique for every socket. 64 | */ 65 | this.sidPrefix = this.gameServer.ADMIN_SERVER ? 'SA' : 'SP'; 66 | 67 | /** 68 | * ### SocketIo.sidPrefixStrip 69 | * 70 | * The prefix to be stripped from sid for internal operations 71 | * 72 | * Sids are represented as SA/endpoint#sid, but in sockets.connected 73 | * they are saved as /#sid. Note: endpoints already start with a /. 74 | * 75 | * Notice: this depends on how Socket.io represets sid internally. 76 | * If they change the internal structure it needs to be adapted. 77 | * 78 | * @see SocketIo.disconnect 79 | */ 80 | this.sidPrefixStrip = this.sidPrefix + this.gameServer.endpoint; 81 | 82 | /** 83 | * ### SocketIo.sidPrefixStripLen 84 | * 85 | * The length of the prefix to be stripped from sid for internal operations 86 | * 87 | * @see SocketIo.sidPrefixStrip 88 | * @see SocketIo.disconnect 89 | */ 90 | this.sidPrefixStripLen = this.sidPrefixStrip.length; 91 | } 92 | 93 | 94 | // ## SocketIo methods 95 | 96 | /** 97 | * ### SocketIo.attachListeners 98 | * 99 | * Activates the socket to accepts incoming connections 100 | */ 101 | SocketIo.prototype.attachListeners = function() { 102 | var that; 103 | that = this; 104 | this.sioChannel = this.sio.of(this.gameServer.endpoint).on('connection', 105 | function(socket) { 106 | var res, prefixedSid; 107 | var startingRoom, clientType; 108 | 109 | // console.log('hello!', socket.handshake.decoded_token.name); 110 | 111 | prefixedSid = that.sidPrefix + socket.id; 112 | 113 | if (that.gameServer.options.sioQuery && socket.handshake.query) { 114 | startingRoom = socket.handshake.query.startingRoom; 115 | clientType = socket.handshake.query.clientType; 116 | 117 | // TODO: check the implications of allowing setting ID here. 118 | // var clientId; 119 | // clientId = socket.handshake.query.id; 120 | 121 | // Cleanup clientType (sometimes browsers add quotes). 122 | if (clientType && 123 | ((clientType.charAt(0) === '"' && 124 | clientType.charAt(clientType.length-1) === '"') || 125 | (clientType.charAt(0) === "'" && 126 | clientType.charAt(clientType.length-1) === "'"))) { 127 | 128 | clientType = clientType.substr(1,clientType.length -2); 129 | } 130 | } 131 | 132 | // Add information about the IP in the headers. 133 | // This might change in different versions of Socket.IO 134 | // socket.handshake.headers.address = socket.handshake.address; 135 | 136 | res = that.gameServer.onConnect(prefixedSid, that, 137 | socket.handshake, clientType, 138 | startingRoom); 139 | 140 | if (res) { 141 | socket.on('message', function(msgStr) { 142 | var gameMsg; 143 | gameMsg = that.gameServer.secureParse(msgStr); 144 | if (!gameMsg) return; 145 | 146 | // Anti-spoofing. 147 | if (that.gameServer.antiSpoofing && 148 | gameMsg.sid !== socket.id) { 149 | 150 | // We log it here because we skip onMessage. 151 | that.gameServer.msgLogger.log(gameMsg, 'in', msgStr); 152 | // Find out the id of the spoofer. 153 | res = that.gameServer.registry.sid; 154 | if (!res) { 155 | res = '[no-sid-yet]'; 156 | } 157 | else { 158 | res = res.get(gameMsg.sid); 159 | res = res ? res.id : '[id-not-found]'; 160 | } 161 | that.gameServer.sysLogger.log('SocketIo socket ' + 162 | 'spoofed msg detected: ' + 163 | gameMsg.id + ' from ' + 164 | res); 165 | return; 166 | } 167 | that.gameServer.onMessage(gameMsg, msgStr); 168 | }); 169 | socket.on('disconnect', function() { 170 | that.gameServer.onDisconnect(prefixedSid, socket); 171 | }); 172 | } 173 | }); 174 | 175 | this.sio.sockets.on("shutdown", that.gameServer.onShutdown); 176 | }; 177 | 178 | /** 179 | * ### SocketIo.disconnect 180 | * 181 | * Disconnects the client registered under the specified socket id 182 | * 183 | * @param {string} sid The socket id of the client to disconnect 184 | */ 185 | SocketIo.prototype.disconnect = function(sid) { 186 | var socket, strippedSid; 187 | 188 | // Remove socket name from sid (2 chars + channel name). 189 | strippedSid = this.parseSid(sid); 190 | 191 | // Ste: was. 192 | //socket = this.sio.sockets.sockets[strippedSid]; 193 | // Ste: was. 194 | // socket = this.sio.sockets.connected[strippedSid]; // full strip. 195 | // v2 196 | // socket = this.sioChannel.connected[sid.slice(2)]; 197 | socket = this.sioChannel.sockets.get(sid.slice(2)); 198 | 199 | if (!socket) { 200 | throw new Error('SocketIo.disconnect: ' + 201 | 'socket not found for sid "' + sid + '". Stripped: ' + 202 | strippedSid 203 | ); 204 | } 205 | 206 | socket.disconnect(true); 207 | // This is already triggered by the method above. 208 | // this.gameServer.onDisconnect(sid, socket); 209 | }; 210 | 211 | /** 212 | * ### SocketIo.send 213 | * 214 | * Sends a game message to the client registered under the specified socket id 215 | * 216 | * The _to_ field of the game message is usually different from the _sid_ and 217 | * it is not used to locate the client. 218 | * 219 | * @param {GameMsg} gameMsg The game message 220 | * @param {string} sid The socket id of the receiver 221 | * @param {boolean} broadcast If TRUE, the message will not be sent to 222 | * the client with sid = sid. Default: FALSE 223 | * @return {boolean} TRUE, on success 224 | */ 225 | SocketIo.prototype.send = function(gameMsg, sid, broadcast) { 226 | var msg, client, rel, strippedSid; 227 | var sysLogger, msgLogger; 228 | 229 | if (sid === 'SERVER' || sid === null) { 230 | sysLogger.log('SocketIo.send: Trying to send msg to ' + 231 | 'nobody: ' + sid, 'error'); 232 | return false; 233 | } 234 | 235 | broadcast = broadcast || false; 236 | 237 | sysLogger = this.gameServer.sysLogger; 238 | msgLogger = this.gameServer.msgLogger; 239 | 240 | if (sid === 'ALL' && broadcast) { 241 | sysLogger.log('SocketIo.send: Incompatible options: broadcast = true ' + 242 | 'and sid = ALL', 'error'); 243 | return false; 244 | } 245 | 246 | // Cleanup msg, if necessary. 247 | if (gameMsg._to) { 248 | if (!gameMsg._originalTo) { 249 | sysLogger.log('SocketIo.send: _to field found, but no _originalTo ', 250 | 'error'); 251 | return false; 252 | } 253 | gameMsg.to = gameMsg._orginalTo; 254 | delete gameMsg._to; 255 | delete gameMsg._originalTo; 256 | } 257 | 258 | if (gameMsg._sid) delete gameMsg._sid; 259 | if (gameMsg._channelName) delete gameMsg._channelName; 260 | if (gameMsg._roomName) delete gameMsg._roomName; 261 | 262 | rel = gameMsg.reliable; 263 | msg = gameMsg.stringify(); 264 | 265 | // Send to ALL. 266 | if (sid === 'ALL') { 267 | if (rel) { 268 | this.sioChannel.json.send(msg); 269 | } 270 | else { 271 | this.sioChannel.volatile.json.send(msg); 272 | } 273 | sysLogger.log('Msg ' + gameMsg.toSMS() + ' sent to ALL'); 274 | msgLogger.log(gameMsg, 'out-ALL'); 275 | } 276 | 277 | // Either send to a specific client(1), or to ALL but a specific client(2). 278 | else { 279 | // Remove socket name from sid (exactly 2 characters, see parseSid). 280 | strippedSid = sid.slice(2); 281 | 282 | // (1) 283 | if (!broadcast) { 284 | 285 | // Ste: v2 286 | // client = this.sioChannel.connected[strippedSid]; 287 | client = this.sioChannel.sockets.get(strippedSid); 288 | 289 | if (!client) { 290 | sysLogger.log('SocketIo.send: msg ' + gameMsg.toSMS() + 291 | ' not sent. Unexisting recipient sid: ' + 292 | strippedSid, 'error'); 293 | return false; 294 | } 295 | 296 | // v2 297 | // if (rel) { 298 | // client.json.send(msg); 299 | // } 300 | // else { 301 | // client.volatile.json.send(msg); 302 | // } 303 | 304 | client.send(msg); 305 | 306 | sysLogger.log('Msg ' + gameMsg.toSMS() + ' sent to sid ' + 307 | strippedSid); 308 | msgLogger.log(gameMsg, 'out-ID'); 309 | 310 | } 311 | // (2) 312 | else { 313 | 314 | // for (clientSid in this.sioChannel.sockets) { 315 | this.sioChannel.sockets.forEach((client, clientSid) => { 316 | 317 | if (strippedSid === clientSid) return; 318 | 319 | // if (rel) { 320 | // client.json.send(msg); 321 | // } 322 | // else { 323 | // client.volatile.json.send(msg); 324 | // } 325 | 326 | client.send(msg); 327 | 328 | sysLogger.log('Msg ' + gameMsg.toSMS() + ' sent to sid ' + 329 | clientSid); 330 | msgLogger.log(gameMsg, 'out-BROADCAST'); 331 | }); 332 | } 333 | } 334 | return true; 335 | }; 336 | 337 | /** 338 | * ### SocketIo.parseSid 339 | * 340 | * Parses a socket id (sid) from nodeGame to Socket.io format 341 | * 342 | * This returns the format found in `this.sio.sockets.connected`. 343 | * In `sioChannel.connected` the format is sid.slice(2). 344 | * 345 | * @param {string} sid The sid to parse 346 | * 347 | * @return {string} The Socket.io-formatted sid 348 | */ 349 | SocketIo.prototype.parseSid = function(sid) { 350 | return '/' + sid.slice(this.sidPrefixStripLen); 351 | }; 352 | -------------------------------------------------------------------------------- /log/README: -------------------------------------------------------------------------------- 1 | Servernode default log dir. Can be changed in ServerNode configuration by setting logDir = /your/log/dir 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodegame-server", 3 | "description": "nodeGame server for the browser and node.js", 4 | "version": "7.0.3", 5 | "main": "index.js", 6 | "homepage": "http://nodegame.org", 7 | "keywords": [ 8 | "game", 9 | "multiplayer", 10 | "experiment", 11 | "behavioral", 12 | "socket.io", 13 | "websockets" 14 | ], 15 | "author": "Stefano Balietti ", 16 | "contributors": [ 17 | { 18 | "name": "Stefano Balietti", 19 | "email": "futur.dorko@gmail.com" 20 | } 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/nodeGame/nodegame-server.git" 25 | }, 26 | "dependencies": { 27 | "archiver": "*", 28 | "body-parser": "*", 29 | "commander": "^7.0.0", 30 | "cookie-parser": "*", 31 | "cookie-session": "*", 32 | "errorhandler": "*", 33 | "express": "4.17.1", 34 | "jade": "1.11.0", 35 | "JSON": "1.0.0", 36 | "jsonwebtoken": "8.5.1", 37 | "JSUS": "^1.1.0", 38 | "NDDB": "^3.0.0", 39 | "nodegame-game-template": "*", 40 | "nodegame-monitor": "*", 41 | "request": "2.88.0", 42 | "shelf.js": ">= 0.3.7", 43 | "socket.io": "4.1.3", 44 | "winston": "3.3.3" 45 | }, 46 | "devDependencies": { 47 | "cssnano": "^5.0.8", 48 | "mocha": ">=4.0.1", 49 | "postcss": "^8.3.11", 50 | "postcss-cli": "^9.0.1", 51 | "should": ">=13.1.3" 52 | }, 53 | "engines": { 54 | "node": ">= 10.0" 55 | }, 56 | "license": "MIT", 57 | "scripts": { 58 | "test": "make test" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /phantomjs/openurl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # openurl 3 | * Copyright(c) 2015 Stefano Balietti 4 | * MIT Licensed 5 | * 6 | * Open a phantomjs instance to the specified url 7 | * 8 | * This file is executed via `#ServerChannel.connectPhantom()`. 9 | * 10 | * http://www.nodegame.org 11 | */ 12 | 13 | "use strict"; 14 | 15 | var webpage = require('webpage'); 16 | var system = require('system'); 17 | 18 | var url, page; 19 | 20 | if (system.args.length < 2) { 21 | console.log('Need URL!'); 22 | phantom.exit(); 23 | } 24 | url = system.args[1]; 25 | 26 | page = webpage.create(); 27 | 28 | page.viewportSize = { 29 | width: 1366, 30 | height: 768 31 | }; 32 | 33 | 34 | page.onConsoleMessage = function(msg) { 35 | if (msg === 'PHANTOMJS EXITING') phantom.exit(); 36 | console.log(msg); 37 | }; 38 | 39 | page.open(url, function() { 40 | setTimeout(function() { 41 | page.evaluate(function() { 42 | 43 | // PhantomJS does not have the .click() method. 44 | if (!HTMLElement.prototype.click) { 45 | HTMLElement.prototype.click = function() { 46 | var ev = document.createEvent('MouseEvent'); 47 | ev.initMouseEvent( 48 | 'click', 49 | /*bubble*/true, /*cancelable*/true, 50 | window, null, 51 | 0, 0, 0, 0, /*coordinates*/ 52 | false, false, false, false, /*modifier keys*/ 53 | 0/*button=left*/, null 54 | ); 55 | this.dispatchEvent(ev); 56 | }; 57 | } 58 | 59 | node.events.ee.ng.on('GAME_OVER', function() { 60 | console.log('PHANTOMJS EXITING'); 61 | }); 62 | }); 63 | }, 1000); 64 | }); 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('cssnano')({ 4 | preset: 'default', 5 | }), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /public/images/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/.gitignore -------------------------------------------------------------------------------- /public/images/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/arrow-left.png -------------------------------------------------------------------------------- /public/images/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/arrow-right.png -------------------------------------------------------------------------------- /public/images/circles-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/circles-dark.png -------------------------------------------------------------------------------- /public/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/close.png -------------------------------------------------------------------------------- /public/images/close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/close_small.png -------------------------------------------------------------------------------- /public/images/delete-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/delete-icon.png -------------------------------------------------------------------------------- /public/images/google-group-scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/google-group-scaled.png -------------------------------------------------------------------------------- /public/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/info.png -------------------------------------------------------------------------------- /public/images/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 59 | 67 | ? 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /public/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/loading.gif -------------------------------------------------------------------------------- /public/images/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/maximize.png -------------------------------------------------------------------------------- /public/images/maximize_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/maximize_small.png -------------------------------------------------------------------------------- /public/images/maximize_small2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/maximize_small2.png -------------------------------------------------------------------------------- /public/images/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/minimize.png -------------------------------------------------------------------------------- /public/images/minimize_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/minimize_small.png -------------------------------------------------------------------------------- /public/images/newlogo_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/newlogo_small.jpg -------------------------------------------------------------------------------- /public/images/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/no-image.png -------------------------------------------------------------------------------- /public/images/nodegame_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/nodegame_logo.png -------------------------------------------------------------------------------- /public/images/nodegame_logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/nodegame_logo_transparent.png -------------------------------------------------------------------------------- /public/images/success-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/images/success-icon.png -------------------------------------------------------------------------------- /public/lib/bootstrap5/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | @media (prefers-reduced-motion: no-preference) { 15 | :root { 16 | scroll-behavior: smooth; 17 | } 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font-family: var(--bs-body-font-family); 23 | font-size: var(--bs-body-font-size); 24 | font-weight: var(--bs-body-font-weight); 25 | line-height: var(--bs-body-line-height); 26 | color: var(--bs-body-color); 27 | text-align: var(--bs-body-text-align); 28 | background-color: var(--bs-body-bg); 29 | -webkit-text-size-adjust: 100%; 30 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 31 | } 32 | 33 | hr { 34 | margin: 1rem 0; 35 | color: inherit; 36 | background-color: currentColor; 37 | border: 0; 38 | opacity: 0.25; 39 | } 40 | 41 | hr:not([size]) { 42 | height: 1px; 43 | } 44 | 45 | h6, h5, h4, h3, h2, h1 { 46 | margin-top: 0; 47 | margin-bottom: 0.5rem; 48 | font-weight: 500; 49 | line-height: 1.2; 50 | } 51 | 52 | h1 { 53 | font-size: calc(1.375rem + 1.5vw); 54 | } 55 | @media (min-width: 1200px) { 56 | h1 { 57 | font-size: 2.5rem; 58 | } 59 | } 60 | 61 | h2 { 62 | font-size: calc(1.325rem + 0.9vw); 63 | } 64 | @media (min-width: 1200px) { 65 | h2 { 66 | font-size: 2rem; 67 | } 68 | } 69 | 70 | h3 { 71 | font-size: calc(1.3rem + 0.6vw); 72 | } 73 | @media (min-width: 1200px) { 74 | h3 { 75 | font-size: 1.75rem; 76 | } 77 | } 78 | 79 | h4 { 80 | font-size: calc(1.275rem + 0.3vw); 81 | } 82 | @media (min-width: 1200px) { 83 | h4 { 84 | font-size: 1.5rem; 85 | } 86 | } 87 | 88 | h5 { 89 | font-size: 1.25rem; 90 | } 91 | 92 | h6 { 93 | font-size: 1rem; 94 | } 95 | 96 | p { 97 | margin-top: 0; 98 | margin-bottom: 1rem; 99 | } 100 | 101 | abbr[title], 102 | abbr[data-bs-original-title] { 103 | -webkit-text-decoration: underline dotted; 104 | text-decoration: underline dotted; 105 | cursor: help; 106 | -webkit-text-decoration-skip-ink: none; 107 | text-decoration-skip-ink: none; 108 | } 109 | 110 | address { 111 | margin-bottom: 1rem; 112 | font-style: normal; 113 | line-height: inherit; 114 | } 115 | 116 | ol, 117 | ul { 118 | padding-left: 2rem; 119 | } 120 | 121 | ol, 122 | ul, 123 | dl { 124 | margin-top: 0; 125 | margin-bottom: 1rem; 126 | } 127 | 128 | ol ol, 129 | ul ul, 130 | ol ul, 131 | ul ol { 132 | margin-bottom: 0; 133 | } 134 | 135 | dt { 136 | font-weight: 700; 137 | } 138 | 139 | dd { 140 | margin-bottom: 0.5rem; 141 | margin-left: 0; 142 | } 143 | 144 | blockquote { 145 | margin: 0 0 1rem; 146 | } 147 | 148 | b, 149 | strong { 150 | font-weight: bolder; 151 | } 152 | 153 | small { 154 | font-size: 0.875em; 155 | } 156 | 157 | mark { 158 | padding: 0.2em; 159 | background-color: #fcf8e3; 160 | } 161 | 162 | sub, 163 | sup { 164 | position: relative; 165 | font-size: 0.75em; 166 | line-height: 0; 167 | vertical-align: baseline; 168 | } 169 | 170 | sub { 171 | bottom: -0.25em; 172 | } 173 | 174 | sup { 175 | top: -0.5em; 176 | } 177 | 178 | a { 179 | color: #0d6efd; 180 | text-decoration: underline; 181 | } 182 | a:hover { 183 | color: #0a58ca; 184 | } 185 | 186 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 187 | color: inherit; 188 | text-decoration: none; 189 | } 190 | 191 | pre, 192 | code, 193 | kbd, 194 | samp { 195 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 196 | font-size: 1em; 197 | direction: ltr /* rtl:ignore */; 198 | unicode-bidi: bidi-override; 199 | } 200 | 201 | pre { 202 | display: block; 203 | margin-top: 0; 204 | margin-bottom: 1rem; 205 | overflow: auto; 206 | font-size: 0.875em; 207 | } 208 | pre code { 209 | font-size: inherit; 210 | color: inherit; 211 | word-break: normal; 212 | } 213 | 214 | code { 215 | font-size: 0.875em; 216 | color: #d63384; 217 | word-wrap: break-word; 218 | } 219 | a > code { 220 | color: inherit; 221 | } 222 | 223 | kbd { 224 | padding: 0.2rem 0.4rem; 225 | font-size: 0.875em; 226 | color: #fff; 227 | background-color: #212529; 228 | border-radius: 0.2rem; 229 | } 230 | kbd kbd { 231 | padding: 0; 232 | font-size: 1em; 233 | font-weight: 700; 234 | } 235 | 236 | figure { 237 | margin: 0 0 1rem; 238 | } 239 | 240 | img, 241 | svg { 242 | vertical-align: middle; 243 | } 244 | 245 | table { 246 | caption-side: bottom; 247 | border-collapse: collapse; 248 | } 249 | 250 | caption { 251 | padding-top: 0.5rem; 252 | padding-bottom: 0.5rem; 253 | color: #6c757d; 254 | text-align: left; 255 | } 256 | 257 | th { 258 | text-align: inherit; 259 | text-align: -webkit-match-parent; 260 | } 261 | 262 | thead, 263 | tbody, 264 | tfoot, 265 | tr, 266 | td, 267 | th { 268 | border-color: inherit; 269 | border-style: solid; 270 | border-width: 0; 271 | } 272 | 273 | label { 274 | display: inline-block; 275 | } 276 | 277 | button { 278 | border-radius: 0; 279 | } 280 | 281 | button:focus:not(:focus-visible) { 282 | outline: 0; 283 | } 284 | 285 | input, 286 | button, 287 | select, 288 | optgroup, 289 | textarea { 290 | margin: 0; 291 | font-family: inherit; 292 | font-size: inherit; 293 | line-height: inherit; 294 | } 295 | 296 | button, 297 | select { 298 | text-transform: none; 299 | } 300 | 301 | [role=button] { 302 | cursor: pointer; 303 | } 304 | 305 | select { 306 | word-wrap: normal; 307 | } 308 | select:disabled { 309 | opacity: 1; 310 | } 311 | 312 | [list]::-webkit-calendar-picker-indicator { 313 | display: none; 314 | } 315 | 316 | button, 317 | [type=button], 318 | [type=reset], 319 | [type=submit] { 320 | -webkit-appearance: button; 321 | } 322 | button:not(:disabled), 323 | [type=button]:not(:disabled), 324 | [type=reset]:not(:disabled), 325 | [type=submit]:not(:disabled) { 326 | cursor: pointer; 327 | } 328 | 329 | ::-moz-focus-inner { 330 | padding: 0; 331 | border-style: none; 332 | } 333 | 334 | textarea { 335 | resize: vertical; 336 | } 337 | 338 | fieldset { 339 | min-width: 0; 340 | padding: 0; 341 | margin: 0; 342 | border: 0; 343 | } 344 | 345 | legend { 346 | float: left; 347 | width: 100%; 348 | padding: 0; 349 | margin-bottom: 0.5rem; 350 | font-size: calc(1.275rem + 0.3vw); 351 | line-height: inherit; 352 | } 353 | @media (min-width: 1200px) { 354 | legend { 355 | font-size: 1.5rem; 356 | } 357 | } 358 | legend + * { 359 | clear: left; 360 | } 361 | 362 | ::-webkit-datetime-edit-fields-wrapper, 363 | ::-webkit-datetime-edit-text, 364 | ::-webkit-datetime-edit-minute, 365 | ::-webkit-datetime-edit-hour-field, 366 | ::-webkit-datetime-edit-day-field, 367 | ::-webkit-datetime-edit-month-field, 368 | ::-webkit-datetime-edit-year-field { 369 | padding: 0; 370 | } 371 | 372 | ::-webkit-inner-spin-button { 373 | height: auto; 374 | } 375 | 376 | [type=search] { 377 | outline-offset: -2px; 378 | -webkit-appearance: textfield; 379 | } 380 | 381 | /* rtl:raw: 382 | [type="tel"], 383 | [type="url"], 384 | [type="email"], 385 | [type="number"] { 386 | direction: ltr; 387 | } 388 | */ 389 | ::-webkit-search-decoration { 390 | -webkit-appearance: none; 391 | } 392 | 393 | ::-webkit-color-swatch-wrapper { 394 | padding: 0; 395 | } 396 | 397 | ::file-selector-button { 398 | font: inherit; 399 | } 400 | 401 | ::-webkit-file-upload-button { 402 | font: inherit; 403 | -webkit-appearance: button; 404 | } 405 | 406 | output { 407 | display: inline-block; 408 | } 409 | 410 | iframe { 411 | border: 0; 412 | } 413 | 414 | summary { 415 | display: list-item; 416 | cursor: pointer; 417 | } 418 | 419 | progress { 420 | vertical-align: baseline; 421 | } 422 | 423 | [hidden] { 424 | display: none !important; 425 | } 426 | 427 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /public/lib/bootstrap5/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /public/lib/bootstrap5/css/bootstrap-reboot.rtl.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | @media (prefers-reduced-motion: no-preference) { 15 | :root { 16 | scroll-behavior: smooth; 17 | } 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font-family: var(--bs-body-font-family); 23 | font-size: var(--bs-body-font-size); 24 | font-weight: var(--bs-body-font-weight); 25 | line-height: var(--bs-body-line-height); 26 | color: var(--bs-body-color); 27 | text-align: var(--bs-body-text-align); 28 | background-color: var(--bs-body-bg); 29 | -webkit-text-size-adjust: 100%; 30 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 31 | } 32 | 33 | hr { 34 | margin: 1rem 0; 35 | color: inherit; 36 | background-color: currentColor; 37 | border: 0; 38 | opacity: 0.25; 39 | } 40 | 41 | hr:not([size]) { 42 | height: 1px; 43 | } 44 | 45 | h6, h5, h4, h3, h2, h1 { 46 | margin-top: 0; 47 | margin-bottom: 0.5rem; 48 | font-weight: 500; 49 | line-height: 1.2; 50 | } 51 | 52 | h1 { 53 | font-size: calc(1.375rem + 1.5vw); 54 | } 55 | @media (min-width: 1200px) { 56 | h1 { 57 | font-size: 2.5rem; 58 | } 59 | } 60 | 61 | h2 { 62 | font-size: calc(1.325rem + 0.9vw); 63 | } 64 | @media (min-width: 1200px) { 65 | h2 { 66 | font-size: 2rem; 67 | } 68 | } 69 | 70 | h3 { 71 | font-size: calc(1.3rem + 0.6vw); 72 | } 73 | @media (min-width: 1200px) { 74 | h3 { 75 | font-size: 1.75rem; 76 | } 77 | } 78 | 79 | h4 { 80 | font-size: calc(1.275rem + 0.3vw); 81 | } 82 | @media (min-width: 1200px) { 83 | h4 { 84 | font-size: 1.5rem; 85 | } 86 | } 87 | 88 | h5 { 89 | font-size: 1.25rem; 90 | } 91 | 92 | h6 { 93 | font-size: 1rem; 94 | } 95 | 96 | p { 97 | margin-top: 0; 98 | margin-bottom: 1rem; 99 | } 100 | 101 | abbr[title], 102 | abbr[data-bs-original-title] { 103 | -webkit-text-decoration: underline dotted; 104 | text-decoration: underline dotted; 105 | cursor: help; 106 | -webkit-text-decoration-skip-ink: none; 107 | text-decoration-skip-ink: none; 108 | } 109 | 110 | address { 111 | margin-bottom: 1rem; 112 | font-style: normal; 113 | line-height: inherit; 114 | } 115 | 116 | ol, 117 | ul { 118 | padding-right: 2rem; 119 | } 120 | 121 | ol, 122 | ul, 123 | dl { 124 | margin-top: 0; 125 | margin-bottom: 1rem; 126 | } 127 | 128 | ol ol, 129 | ul ul, 130 | ol ul, 131 | ul ol { 132 | margin-bottom: 0; 133 | } 134 | 135 | dt { 136 | font-weight: 700; 137 | } 138 | 139 | dd { 140 | margin-bottom: 0.5rem; 141 | margin-right: 0; 142 | } 143 | 144 | blockquote { 145 | margin: 0 0 1rem; 146 | } 147 | 148 | b, 149 | strong { 150 | font-weight: bolder; 151 | } 152 | 153 | small { 154 | font-size: 0.875em; 155 | } 156 | 157 | mark { 158 | padding: 0.2em; 159 | background-color: #fcf8e3; 160 | } 161 | 162 | sub, 163 | sup { 164 | position: relative; 165 | font-size: 0.75em; 166 | line-height: 0; 167 | vertical-align: baseline; 168 | } 169 | 170 | sub { 171 | bottom: -0.25em; 172 | } 173 | 174 | sup { 175 | top: -0.5em; 176 | } 177 | 178 | a { 179 | color: #0d6efd; 180 | text-decoration: underline; 181 | } 182 | a:hover { 183 | color: #0a58ca; 184 | } 185 | 186 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 187 | color: inherit; 188 | text-decoration: none; 189 | } 190 | 191 | pre, 192 | code, 193 | kbd, 194 | samp { 195 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 196 | font-size: 1em; 197 | direction: ltr ; 198 | unicode-bidi: bidi-override; 199 | } 200 | 201 | pre { 202 | display: block; 203 | margin-top: 0; 204 | margin-bottom: 1rem; 205 | overflow: auto; 206 | font-size: 0.875em; 207 | } 208 | pre code { 209 | font-size: inherit; 210 | color: inherit; 211 | word-break: normal; 212 | } 213 | 214 | code { 215 | font-size: 0.875em; 216 | color: #d63384; 217 | word-wrap: break-word; 218 | } 219 | a > code { 220 | color: inherit; 221 | } 222 | 223 | kbd { 224 | padding: 0.2rem 0.4rem; 225 | font-size: 0.875em; 226 | color: #fff; 227 | background-color: #212529; 228 | border-radius: 0.2rem; 229 | } 230 | kbd kbd { 231 | padding: 0; 232 | font-size: 1em; 233 | font-weight: 700; 234 | } 235 | 236 | figure { 237 | margin: 0 0 1rem; 238 | } 239 | 240 | img, 241 | svg { 242 | vertical-align: middle; 243 | } 244 | 245 | table { 246 | caption-side: bottom; 247 | border-collapse: collapse; 248 | } 249 | 250 | caption { 251 | padding-top: 0.5rem; 252 | padding-bottom: 0.5rem; 253 | color: #6c757d; 254 | text-align: right; 255 | } 256 | 257 | th { 258 | text-align: inherit; 259 | text-align: -webkit-match-parent; 260 | } 261 | 262 | thead, 263 | tbody, 264 | tfoot, 265 | tr, 266 | td, 267 | th { 268 | border-color: inherit; 269 | border-style: solid; 270 | border-width: 0; 271 | } 272 | 273 | label { 274 | display: inline-block; 275 | } 276 | 277 | button { 278 | border-radius: 0; 279 | } 280 | 281 | button:focus:not(:focus-visible) { 282 | outline: 0; 283 | } 284 | 285 | input, 286 | button, 287 | select, 288 | optgroup, 289 | textarea { 290 | margin: 0; 291 | font-family: inherit; 292 | font-size: inherit; 293 | line-height: inherit; 294 | } 295 | 296 | button, 297 | select { 298 | text-transform: none; 299 | } 300 | 301 | [role=button] { 302 | cursor: pointer; 303 | } 304 | 305 | select { 306 | word-wrap: normal; 307 | } 308 | select:disabled { 309 | opacity: 1; 310 | } 311 | 312 | [list]::-webkit-calendar-picker-indicator { 313 | display: none; 314 | } 315 | 316 | button, 317 | [type=button], 318 | [type=reset], 319 | [type=submit] { 320 | -webkit-appearance: button; 321 | } 322 | button:not(:disabled), 323 | [type=button]:not(:disabled), 324 | [type=reset]:not(:disabled), 325 | [type=submit]:not(:disabled) { 326 | cursor: pointer; 327 | } 328 | 329 | ::-moz-focus-inner { 330 | padding: 0; 331 | border-style: none; 332 | } 333 | 334 | textarea { 335 | resize: vertical; 336 | } 337 | 338 | fieldset { 339 | min-width: 0; 340 | padding: 0; 341 | margin: 0; 342 | border: 0; 343 | } 344 | 345 | legend { 346 | float: right; 347 | width: 100%; 348 | padding: 0; 349 | margin-bottom: 0.5rem; 350 | font-size: calc(1.275rem + 0.3vw); 351 | line-height: inherit; 352 | } 353 | @media (min-width: 1200px) { 354 | legend { 355 | font-size: 1.5rem; 356 | } 357 | } 358 | legend + * { 359 | clear: right; 360 | } 361 | 362 | ::-webkit-datetime-edit-fields-wrapper, 363 | ::-webkit-datetime-edit-text, 364 | ::-webkit-datetime-edit-minute, 365 | ::-webkit-datetime-edit-hour-field, 366 | ::-webkit-datetime-edit-day-field, 367 | ::-webkit-datetime-edit-month-field, 368 | ::-webkit-datetime-edit-year-field { 369 | padding: 0; 370 | } 371 | 372 | ::-webkit-inner-spin-button { 373 | height: auto; 374 | } 375 | 376 | [type=search] { 377 | outline-offset: -2px; 378 | -webkit-appearance: textfield; 379 | } 380 | 381 | [type="tel"], 382 | [type="url"], 383 | [type="email"], 384 | [type="number"] { 385 | direction: ltr; 386 | } 387 | ::-webkit-search-decoration { 388 | -webkit-appearance: none; 389 | } 390 | 391 | ::-webkit-color-swatch-wrapper { 392 | padding: 0; 393 | } 394 | 395 | ::file-selector-button { 396 | font: inherit; 397 | } 398 | 399 | ::-webkit-file-upload-button { 400 | font: inherit; 401 | -webkit-appearance: button; 402 | } 403 | 404 | output { 405 | display: inline-block; 406 | } 407 | 408 | iframe { 409 | border: 0; 410 | } 411 | 412 | summary { 413 | display: list-item; 414 | cursor: pointer; 415 | } 416 | 417 | progress { 418 | vertical-align: baseline; 419 | } 420 | 421 | [hidden] { 422 | display: none !important; 423 | } 424 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ -------------------------------------------------------------------------------- /public/lib/bootstrap5/css/bootstrap-reboot.rtl.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ -------------------------------------------------------------------------------- /public/pages/accessdenied.htm: -------------------------------------------------------------------------------- 1 | 2 | Access Denied 3 | 4 |
5 | I am sorry, but your connection has not been authorized. If you think this is a mistake please contact the server administrator. 6 |
7 | 8 | -------------------------------------------------------------------------------- /public/pages/auth.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /public/pages/blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /public/pages/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /public/pages/testpage.htm: -------------------------------------------------------------------------------- 1 | 2 | Test Page 3 | 4 | 5 |
6 | Just a test. 7 |
8 | 9 | -------------------------------------------------------------------------------- /public/sounds/doorbell.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodeGame/nodegame-server/d1dff8ecc800f87b584d9648cb74d467304e9d89/public/sounds/doorbell.ogg -------------------------------------------------------------------------------- /public/stylesheets/fullheight.css: -------------------------------------------------------------------------------- 1 | /* This is a legacy css. Not needed in in v4. */ 2 | 3 | html { 4 | height: 98%; 5 | } 6 | body { 7 | padding:0; 8 | height: 98%; 9 | width: 98%; 10 | margin: auto; 11 | } -------------------------------------------------------------------------------- /public/stylesheets/homepage.css: -------------------------------------------------------------------------------- 1 | col { 2 | padding: 5em; 3 | } 4 | 5 | nav { 6 | height: 60px; 7 | background-color: #EEE; 8 | } 9 | 10 | /* Makes sure it is always centered */ 11 | nav .brand-logo { 12 | left: 50%; 13 | -webkit-transform: translateX(-50%); 14 | transform: translateX(-50%); 15 | } 16 | 17 | /* This makes the card panel larger */ 18 | .card-panel { 19 | box-sizing: content-box; 20 | } 21 | 22 | nav .brand-logo { 23 | margin-top: 0px; 24 | white-space: nowrap; 25 | color: #444444; 26 | } 27 | 28 | nav .brand-logo img { 29 | margin-top: 0.3em; 30 | margin-right: 0.5em; 31 | } 32 | 33 | ul, li { 34 | display: inline; 35 | margin-right: 10px; 36 | } 37 | 38 | .footer { 39 | box-shadow: 0 50vh 0 50vh #484c56; 40 | margin-top: 130px; 41 | padding-top: 20px; 42 | color: #fff; 43 | /*background-color: #0d47a1;*/ 44 | background-color: #484c56; 45 | min-height: 200px; 46 | height: 4em; 47 | } 48 | 49 | .top { 50 | padding-top: 10px; 51 | color: #484c56; 52 | background-color: #fcfcff; 53 | width: 100%; 54 | height: 60px; 55 | } 56 | 57 | body { 58 | background-image: url("../images/circles-dark.png"); 59 | background-repeat: repeat; 60 | } 61 | 62 | #forum-button { 63 | height: 30px; 64 | border: 1px solid #CCC; 65 | box-shadow: 0; 66 | border-radius: 5px; 67 | vertical-align:top; 68 | background: #FFFFFD; 69 | font-size: 12px; 70 | font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 71 | } 72 | 73 | 74 | .media { 75 | margin-left: 15px; 76 | } 77 | 78 | #forum-button { 79 | height: 27px; 80 | border: 1px solid #CCC; 81 | margin-left: 10px; 82 | box-shadow: 0; 83 | border-radius: 5px; 84 | vertical-align: top; 85 | background: #FFFFFD; 86 | font-size: 12px; 87 | font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 88 | } 89 | 90 | #forum-button span{ 91 | vertical-align: top; 92 | font-size: 12px; 93 | margin-left: 3px; 94 | color: black; 95 | } 96 | 97 | #twitter-widget-0 { 98 | margin-right: 10px; 99 | } 100 | 101 | footer a { 102 | font-weight: 400; 103 | text-decoration: underline; 104 | } -------------------------------------------------------------------------------- /public/stylesheets/nodegame-v3.5.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stylesheet for nodeGame window 3 | Copyright(c) 2017 Stefano Balietti 4 | MIT Licensed 5 | 6 | www.nodegame.org 7 | --- 8 | */ 9 | 10 | body { 11 | margin: auto; 12 | padding: 0 5px; 13 | font-family: 'Source Sans Pro', Arial, Helvetica, sans-serif; 14 | font-size: 16px; 15 | color: black; 16 | } 17 | 18 | /* mainframe */ 19 | 20 | iframe#ng_mainframe { 21 | border: 0; 22 | padding: 10px; 23 | } 24 | 25 | .ng_mainframe-header-vertical-r { 26 | height: 100%; 27 | float: left; 28 | width: 86%; 29 | } 30 | 31 | .ng_mainframe-header-vertical-l { 32 | height: 100%; 33 | float: right; 34 | width: 86%; 35 | } 36 | 37 | .ng_mainframe-header-horizontal { 38 | height: 88%; 39 | width: 98%; 40 | clear: both; 41 | } 42 | 43 | /* header */ 44 | 45 | div#ng_header { 46 | padding: 2px 10px; 47 | color: black; 48 | border: 0; 49 | margin: 0; 50 | } 51 | 52 | .ng_header_position-horizontal-t, .ng_header_position-horizontal-b { 53 | width: 98%; 54 | height: 10%; 55 | } 56 | 57 | .ng_header_position-vertical-r { 58 | float: right; 59 | width: 10%; 60 | } 61 | 62 | .ng_header_position-vertical-l { 63 | float: left; 64 | width: 10%; 65 | } 66 | 67 | div#ng_waitScreen { 68 | z-index: 10000; 69 | filter: alpha(opacity=50); /*older IE*/ 70 | filter:progid:DXImageTransform.Microsoft.Alpha(opacity=50); /* IE */ 71 | -moz-opacity: .50; /*older Mozilla*/ 72 | -khtml-opacity: 0.5; /*older Safari*/ 73 | opacity: 0.5; /*supported by current Mozilla, Safari, and Opera*/ 74 | background-color:#000000; 75 | position:fixed; 76 | top:0px; left:0px; width:100%; height:100%; 77 | color:#FFFFFF; 78 | text-align:center; 79 | vertical-align:middle; 80 | font-weight: bold; 81 | font-size: 30px; 82 | padding: 20px; 83 | } 84 | 85 | canvas { 86 | border: 1px #CCC solid; 87 | } 88 | 89 | label { 90 | display: block; 91 | float: left; 92 | min-width: 200px; 93 | } 94 | input[type="range"] { 95 | margin-bottom: 5px; 96 | margin-left: 5px 97 | } 98 | 99 | /* General nodeGame */ 100 | 101 | .strong { 102 | font-weight: bold; 103 | } 104 | 105 | .underline { 106 | text-decoration: underline; 107 | } 108 | 109 | .strike { 110 | text-decoration: line-through; 111 | } 112 | 113 | .hand { 114 | cursor: pointer; 115 | cursor: hand; 116 | } 117 | 118 | /* 119 | Stylesheet for nodeGame widgets 120 | Copyright(c) 2014 Stefano Balietti 121 | MIT Licensed 122 | 123 | www.nodegame.org 124 | --- 125 | */ 126 | 127 | /* .ng_widget {} */ 128 | 129 | /* Language Selector */ 130 | .languageselector { 131 | float: center; 132 | width: 160px; 133 | 134 | } 135 | 136 | .languageselector > .panel-body > form > label { 137 | font-size: 20px; 138 | font-weight: normal; 139 | } 140 | 141 | .languageselector > .panel-body > form > label.selectedButtonLabel { 142 | font-weight: bold; 143 | } 144 | 145 | /* Visual Timer */ 146 | 147 | .visualtimer { 148 | float: right; 149 | min-width: 100px; 150 | width: auto; 151 | } 152 | 153 | .visualtimer > .panel-body > div { 154 | font-size: 40px; 155 | text-align: center; 156 | } 157 | 158 | .visualtimer > .panel-body > div > div.waitTimerTitle { 159 | font-size: 15px; 160 | } 161 | 162 | .visualtimer > .panel-body > div > div.waitTimerBody { 163 | font-size: 20px; 164 | text-align: center; 165 | } 166 | 167 | /* Visual Round */ 168 | 169 | .visualround { 170 | float: right; 171 | min-width: 100px; 172 | width: auto; 173 | } 174 | .visualround > .panel-body > div > div.rounddiv { 175 | font-size: 40px; 176 | text-align: center; 177 | } 178 | 179 | .visualround > .panel-body > div > div.stagediv { 180 | font-size: 20px; 181 | text-align: center; 182 | } 183 | 184 | .visualround > .panel-body > div > div > .number { 185 | 186 | } 187 | 188 | .visualround > .panel-body > div > div > span.text { 189 | font-size: 40%; 190 | text-align: center; 191 | 192 | } 193 | 194 | .visualround > .panel-body > div > div > div.title { 195 | font-size: 50%; 196 | text-align: center; 197 | } 198 | 199 | /* Visual State */ 200 | 201 | .visualstage { 202 | float: right; 203 | min-width: 200px; 204 | width: auto; 205 | } 206 | 207 | /* State Display */ 208 | 209 | .statedisplay { 210 | float: right; 211 | width: 300px; 212 | } 213 | 214 | .statedisplay > .panel-body > div { 215 | width: auto; 216 | font-size: 16px; 217 | } 218 | 219 | /* Money Talks */ 220 | 221 | .moneytalks { 222 | float: right; 223 | width: auto; 224 | min-width: 100px; 225 | } 226 | 227 | .moneytalks > .panel-body > span.moneytalksmoney { 228 | font-size: 40px; 229 | text-align: center; 230 | } 231 | 232 | .moneytalks > .panel-body > span.moneytalkscurrency { 233 | font-size: 12px; 234 | text-align: center; 235 | } 236 | 237 | /* Done Button */ 238 | 239 | .donebutton { 240 | float: right; 241 | width: auto; 242 | min-width: 100px; 243 | text-align: center; 244 | } 245 | 246 | .donebutton > .panel-body { 247 | font-size: 20px; 248 | text-align: center; 249 | } 250 | 251 | /* Chat */ 252 | 253 | 254 | .chat { 255 | overflow-y: scroll; 256 | width: 450px; 257 | border: 1px solid #CCC; 258 | padding: 5px; 259 | } 260 | 261 | .chat > .panel-body > textarea { 262 | height: 100px; 263 | width: 400px; 264 | } 265 | 266 | .chat_me { 267 | font-weight: bold; 268 | } 269 | 270 | /* Feedback */ 271 | .feedback > .panel-body > textarea { 272 | padding: 5px; 273 | font-size: 16px; 274 | width: 98%; 275 | height: 300px; 276 | border: 1px solid #CCC; 277 | } 278 | 279 | /* Requirements */ 280 | 281 | .requirements { 282 | font-size: 1.2em; 283 | } 284 | 285 | .requirements-success { 286 | font-weight: bold; 287 | color: green; 288 | padding-top: 15px; 289 | padding-bottom: 15px; 290 | } 291 | 292 | .requirements-fail { 293 | font-weight: bold; 294 | color: red; 295 | padding-top: 15px; 296 | padding-bottom: 15px; 297 | } 298 | 299 | 300 | 301 | /* Waiting Room */ 302 | .waitingroom { 303 | font-size: 1.2em; 304 | } 305 | 306 | .waitingroom > .panel-body { 307 | text-align: center; 308 | } 309 | 310 | .waitingroom > .panel-body > div#timer-div { 311 | margin-top: 30px; 312 | } 313 | 314 | .waitingroom > .panel-body > div#player-count-div > p#player-count { 315 | font-size: 40px; 316 | clear: both; 317 | margin-top: 30px; 318 | } 319 | 320 | .waitingroom > .panel-body > div > .visualtimer { 321 | float: none; 322 | display: inline; 323 | font-size: 40px; 324 | margin: 0; 325 | } 326 | 327 | .waitingroom > .panel-body > div > span#span_dots { 328 | font-size: 40px; 329 | } 330 | 331 | /** ChoiceTable **/ 332 | 333 | div.choicetable, div.choicetable div.panel-heading, div.choicetable div.panel-body { 334 | border: 0; 335 | padding: 0; 336 | } 337 | 338 | span.choicetable-maintext { 339 | margin: 0; 340 | margin-top: 20px !important; 341 | padding: 0; 342 | display: block; 343 | text-align: left; 344 | } 345 | 346 | textarea.choicemanager-freetext, 347 | textarea.choicetable-freetext { 348 | margin: 0; 349 | margin-top: 20px !important; 350 | padding: 5px; 351 | text-align: left; 352 | float: left; 353 | } 354 | 355 | table.choicetable td { 356 | word-wrap: break-word; 357 | border: 1px solid #333; 358 | text-align: center; 359 | padding: 5px; 360 | width: 40px; 361 | background: white; 362 | } 363 | 364 | table.clickable td:hover { 365 | background: #FFE; 366 | cursor: pointer; 367 | cursor: hand; 368 | } 369 | 370 | table.choicetable td.selected { 371 | background: #333 !important; 372 | color: #FFF; 373 | border: 1px solid #333 !important; 374 | } 375 | 376 | table.choicetable td.choicetable-left, 377 | table.choicetable td.choicetable-right { 378 | border: 0; 379 | background: inherit; 380 | padding-right: 15px; 381 | text-align: right; 382 | } 383 | 384 | table.choicetable td.choicetable-left:hover, 385 | table.choicetable td.choicetable-right:hover { 386 | background: inherit; 387 | cursor: default; 388 | } 389 | 390 | 391 | /* Mood */ 392 | 393 | div.moodgauge { 394 | margin: 0 auto; 395 | text-align: center; 396 | } 397 | 398 | div.moodgauge span.choicetable-maintext, div#quiz div.moodgauge table.choicetable { 399 | text-align: center; 400 | } 401 | 402 | div.moodgauge span.choicetable-maintext { 403 | margin-bottom: 30px; 404 | font-weight: bold; 405 | } 406 | 407 | div.moodgauge table.choicetable { 408 | margin: 0 auto; 409 | text-align: center; 410 | border-spacing: 0 5px; 411 | border-collapse: separate; 412 | } 413 | 414 | div.moodgauge table.choicetable td { 415 | width: 80px !important; 416 | } 417 | 418 | div.moodgauge table.choicetable td.choicetable-left { 419 | min-width: 260px !important; 420 | } 421 | 422 | div.moodgauge span.emotion { 423 | font-weight: bold; 424 | margin-right: 10px; 425 | } 426 | 427 | div.moodgauge table.choicetable td.choicetable-right { 428 | width: 100px; 429 | } 430 | 431 | /* SVO */ 432 | 433 | div.svogauge table.choicetable { 434 | margin: 0 auto; 435 | text-align: center; 436 | border-spacing: 0 30px; 437 | border-collapse: separate; 438 | } 439 | 440 | div.svogauge table td { 441 | width: 60px !important; 442 | } 443 | 444 | div.svogauge span.choicetable-maintext { 445 | margin-bottom: 10px; 446 | font-size: 20px; 447 | } 448 | 449 | div.svogauge hr { 450 | margin: 10px auto; 451 | } 452 | 453 | div.svogauge td.choicetable-left hr { 454 | border-color: black; 455 | } 456 | -------------------------------------------------------------------------------- /public/stylesheets/nodegame.old.css: -------------------------------------------------------------------------------- 1 | /* nodeGame css */ 2 | body { 3 | margin: 0; 4 | font-size: 18px; 5 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; } 6 | 7 | 8 | #ng_mainframe { 9 | /*overflow:hidden; */ 10 | position: absolute; 11 | height: 100%; 12 | width: 100%; 13 | padding: 0; 14 | /*max-width: 800px; 15 | width: 100%; 16 | min-height: 600px; 17 | change to use the js resize code */ 18 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | /* padding: 20px; */ 20 | display: block; 21 | margin: 0 auto; } 22 | 23 | 24 | .ng_mainframe-header-vertical-r { 25 | padding-top: 0 !important; 26 | padding-bottom: 0 !important; 27 | } 28 | 29 | .ng_mainframe-header-vertical-l { 30 | padding-top: 0 !important; 31 | padding-bottom: 0 !important; 32 | } 33 | 34 | .ng_mainframe-header-horizontal-t { 35 | padding-left: 0 !important; 36 | padding-right: 0 !important; 37 | padding-bottom: 0 !important; 38 | } 39 | 40 | .ng_mainframe-header-horizontal-b { 41 | padding-left: 0 !important; 42 | padding-right: 0 !important; 43 | padding-top: 0 !important; 44 | } 45 | 46 | .ng_header_position-horizontal-t { 47 | width: 100% !important; 48 | height: auto !important; 49 | margin: 0 auto !important; 50 | top: 0 !important; 51 | bottom: auto !important; 52 | } 53 | 54 | .ng_header_position-horizontal-b { 55 | width: 100% !important; 56 | height: auto !important; 57 | margin: 0 auto !important; 58 | top: auto !important; 59 | bottom: 0 !important; 60 | } 61 | 62 | .ng_header_position-vertical-r { 63 | width: 125px !important; 64 | height: 100% !important; 65 | margin: 0 !important; 66 | right: 0 !important; 67 | left: auto !important; 68 | } 69 | 70 | .ng_header_position-vertical-l { 71 | width: 125px !important; 72 | height: 100% !important; 73 | margin: 0 !important; 74 | right: auto !important; 75 | left: 0 !important; 76 | } 77 | 78 | 79 | .h1, 80 | .h2, 81 | .h3, 82 | .h4, 83 | .h5, 84 | .h6, 85 | h1, 86 | h2, 87 | h3, 88 | h4, 89 | h5, 90 | h6 { 91 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; } 92 | 93 | p { 94 | font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; } 95 | 96 | #ng_header { 97 | position: fixed !important; 98 | height: auto; 99 | z-index: 1000; 100 | margin: 0 auto; /* Will not center vertically and won't work in IE6/7. */ 101 | left: 0; 102 | right: 0; 103 | width: 100%; 104 | padding: 10px 15px; 105 | background-color: white; 106 | box-shadow: 0 0 15px lightgrey; 107 | /* max-width: 800px; */ 108 | margin: 0 auto; 109 | border-radius: 0 0 5px 5px; } 110 | 111 | #ng_header::after { 112 | content: ""; 113 | clear: both; 114 | display: table; } 115 | 116 | #ng_header .ng_widget { 117 | float: left; 118 | margin-right: 20px; 119 | margin-bottom: 0; 120 | border: none; 121 | box-shadow: none; 122 | padding: 10px; } 123 | #ng_header .ng_widget .panel-heading, 124 | #ng_header .ng_widget .no-panel-heading { 125 | border: none; 126 | background-color: white; 127 | padding: 0; 128 | font-weight: bold; } 129 | #ng_header .ng_widget .panel-body { 130 | padding: 0; } 131 | #ng_header .ng_widget:last-child { 132 | margin-right: 0; } 133 | #ng_header .donebutton { 134 | float: right; 135 | padding: 0; 136 | margin: 0; } 137 | #ng_header .donebutton .panel-heading, 138 | #ng_header .donebutton .no-panel-heading { 139 | display: none; } 140 | #ng_header .moneytalks .panel-heading, 141 | #ng_header .moneytalks .no-panel-heading, 142 | #ng_header .visualround .panel-heading, 143 | #ng_header .visualround .no-panel-heading, 144 | #ng_header .visualtimer .panel-heading, 145 | #ng_header .visualtimer .no-panel-heading { 146 | float: left; 147 | margin-right: 10px; } 148 | #ng_header .moneytalks .panel-body, 149 | #ng_header .moneytalks .no-panel-body, 150 | #ng_header .visualround .panel-body, 151 | #ng_header .visualround .no-panel-body, 152 | #ng_header .visualtimer .panel-body, 153 | #ng_header .visualtimer .no-panel-body { 154 | float: left; } 155 | 156 | #ng_header-top { 157 | 158 | } 159 | 160 | 161 | #ng_waitScreen { 162 | z-index: 10000; 163 | filter: alpha(opacity=50); 164 | /*older IE*/ 165 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=50); 166 | /* IE */ 167 | -moz-opacity: 0.50; 168 | /*older Mozilla*/ 169 | -khtml-opacity: 0.5; 170 | /*older Safari*/ 171 | opacity: 0.5; 172 | /*supported by current Mozilla, Safari, and Opera*/ 173 | background-color: #000000; 174 | position: fixed; 175 | top: 0; 176 | left: 0; 177 | width: 100%; 178 | height: 100%; 179 | color: #FFFFFF; 180 | text-align: center; 181 | vertical-align: middle; 182 | font-weight: bold; 183 | font-size: 30px; 184 | padding: 20px; } 185 | 186 | vas { 187 | border: 1px #CCC solid; } 188 | 189 | label { 190 | display: block; 191 | float: left; 192 | min-width: 200px; } 193 | 194 | input[type="range"] { 195 | margin-bottom: 5px; 196 | margin-left: 5px; } 197 | 198 | /* General nodeGame */ 199 | .strong { 200 | font-weight: bold; } 201 | 202 | .underline { 203 | text-decoration: underline; } 204 | 205 | .strike { 206 | text-decoration: line-through; } 207 | 208 | .hand { 209 | cursor: pointer; 210 | cursor: hand; } 211 | 212 | /* 213 | Stylesheet for nodeGame widgets 214 | Copyright(c) 2017 Stefano Balietti 215 | MIT Licensed 216 | 217 | www.nodegame.org 218 | --- 219 | */ 220 | /* Visual Round */ 221 | .visualround .title { 222 | font-weight: bold; } 223 | 224 | /* Language Selector */ 225 | .languageselector { 226 | float: center; 227 | width: 160px; } 228 | 229 | .languageselector > .panel-body > form > label { 230 | font-size: 20px; 231 | font-weight: normal; } 232 | 233 | .languageselector > .panel-body > form > label.selectedButtonLabel { 234 | font-weight: bold; } 235 | 236 | /* Chat */ 237 | .chat { 238 | overflow-y: scroll; 239 | width: 450px; 240 | border: 1px solid #CCC; 241 | padding: 5px; } 242 | 243 | .chat > .panel-body > textarea { 244 | height: 100px; 245 | width: 400px; } 246 | 247 | .chat_me { 248 | font-weight: bold; } 249 | 250 | /* Feedback */ 251 | .feedback > .panel-body > textarea { 252 | padding: 5px; 253 | font-size: 16px; 254 | width: 98%; 255 | height: 300px; 256 | border: 1px solid #CCC; } 257 | 258 | /* Requirements */ 259 | .requirements { 260 | font-size: 1.2em; } 261 | 262 | .requirements-success { 263 | font-weight: bold; 264 | color: green; 265 | padding-top: 15px; 266 | padding-bottom: 15px; } 267 | 268 | .requirements-fail { 269 | font-weight: bold; 270 | color: red; 271 | padding-top: 15px; 272 | padding-bottom: 15px; } 273 | 274 | /* Waiting Room */ 275 | .waitingroom { 276 | font-size: 1.2em; } 277 | 278 | .waitingroom > .panel-body { 279 | text-align: center; } 280 | 281 | .waitingroom > .panel-body > div#timer-div { 282 | margin-top: 30px; } 283 | 284 | .waitingroom > .panel-body > div#player-count-div > p#player-count { 285 | font-size: 40px; 286 | clear: both; 287 | margin-top: 30px; } 288 | 289 | .waitingroom > .panel-body > div > .visualtimer { 290 | float: none; 291 | display: inline; 292 | font-size: 40px; 293 | margin: 0; } 294 | 295 | .waitingroom > .panel-body > div > span#span_dots { 296 | font-size: 40px; } 297 | 298 | /** ChoiceTable **/ 299 | div.choicetable, 300 | div.choicetable div.panel-body, 301 | div.choicetable div.panel-heading { 302 | border: 0; 303 | padding: 0; } 304 | 305 | span.choicetable-maintext { 306 | margin: 20px 0 0 !important; 307 | padding: 0; 308 | display: block; 309 | text-align: left; } 310 | 311 | textarea.choicemanager-freetext, 312 | textarea.choicetable-freetext { 313 | margin: 20px 0 0 !important; 314 | padding: 5px; 315 | text-align: left; 316 | float: left; } 317 | 318 | table.choicetable td { 319 | word-wrap: break-word; 320 | border: 1px solid #333; 321 | text-align: center; 322 | padding: 5px; 323 | width: 40px; 324 | background: white; } 325 | 326 | table.clickable td:hover { 327 | background: #FFE; 328 | cursor: pointer; 329 | cursor: hand; } 330 | 331 | table.choicetable td.selected { 332 | background: #333 !important; 333 | color: #FFF; 334 | border: 1px solid #333 !important; } 335 | 336 | table.choicetable td.choicetable-left, 337 | table.choicetable td.choicetable-right { 338 | border: 0; 339 | background: inherit; 340 | padding-right: 15px; 341 | text-align: right; } 342 | 343 | table.choicetable td.choicetable-left:hover, 344 | table.choicetable td.choicetable-right:hover { 345 | background: inherit; 346 | cursor: default; } 347 | 348 | /* Mood */ 349 | div.moodgauge { 350 | margin: 0 auto; 351 | text-align: center; } 352 | 353 | div#quiz div.moodgauge table.choicetable, 354 | div.moodgauge span.choicetable-maintext { 355 | text-align: center; } 356 | 357 | div.moodgauge span.choicetable-maintext { 358 | margin-bottom: 30px; 359 | font-weight: bold; } 360 | 361 | div.moodgauge table.choicetable { 362 | margin: 0 auto; 363 | text-align: center; 364 | border-spacing: 0 5px; 365 | border-collapse: separate; } 366 | 367 | div.moodgauge table.choicetable td { 368 | width: 80px !important; } 369 | 370 | div.moodgauge table.choicetable td.choicetable-left { 371 | min-width: 260px !important; } 372 | 373 | div.moodgauge span.emotion { 374 | font-weight: bold; 375 | margin-right: 10px; } 376 | 377 | div.moodgauge table.choicetable td.choicetable-right { 378 | width: 100px; } 379 | 380 | /* SVO */ 381 | div.svogauge table.choicetable { 382 | margin: 0 auto; 383 | text-align: center; 384 | border-spacing: 0 30px; 385 | border-collapse: separate; } 386 | 387 | div.svogauge table td { 388 | width: 60px !important; } 389 | 390 | div.svogauge span.choicetable-maintext { 391 | margin-bottom: 10px; 392 | font-size: 20px; } 393 | 394 | div.svogauge hr { 395 | margin: 10px auto; } 396 | 397 | div.svogauge td.choicetable-left hr { 398 | border-color: black; } 399 | 400 | /* EmailForm and FeedbackForm */ 401 | .emailform-input, 402 | .feedback-textarea { 403 | margin-bottom: 5px; } 404 | 405 | .feedback .btn { 406 | margin-right: 5px; } 407 | 408 | /* EndScreen */ 409 | .endscreen .emailform, 410 | .endscreen .feedback { 411 | margin-top: 30px; } 412 | 413 | .endscreen .no-panel-body { 414 | margin: 5px; } 415 | 416 | /*# sourceMappingURL=nodegame.css.map */ 417 | -------------------------------------------------------------------------------- /public/stylesheets/noscript.css: -------------------------------------------------------------------------------- 1 | /* Styles the noscript tag before nodeGame is even loaded */ 2 | 3 | noscript { 4 | position: fixed; 5 | top: 50%; 6 | left: 50%; 7 | margin-top: -9em; /*set to a negative number 1/2 of your height*/ 8 | margin-left: -15em; /*set to a negative number 1/2 of your width*/ 9 | font-family: Arial, Helvetica, sans-serif; 10 | font-size: 16px; 11 | color: #FF0000; 12 | border: 1px dotted #A60000; 13 | text-align: center; 14 | background: white; 15 | padding: 20px 30px; 16 | } 17 | -------------------------------------------------------------------------------- /public/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | -------------------------------------------------------------------------------- /public/stylesheets/widgets.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stylesheet for nodeGame widgets 3 | Copyright(c) 2016 Stefano Balietti 4 | MIT Licensed 5 | 6 | www.nodegame.org 7 | --- 8 | */ 9 | 10 | /* .ng_widget {} */ 11 | 12 | /* Language Selector */ 13 | .languageselector { 14 | float: center; 15 | width: 160px; 16 | } 17 | 18 | .languageselector > .panel-body > form > label { 19 | font-size: 20px; 20 | font-weight: normal; 21 | } 22 | 23 | .languageselector > .panel-body > form > label.selectedButtonLabel { 24 | font-weight: bold; 25 | } 26 | 27 | /* Visual Timer */ 28 | 29 | .visualtimer { 30 | float: right; 31 | min-width: 100px; 32 | width: auto; 33 | } 34 | 35 | .visualtimer > .panel-body > div { 36 | font-size: 40px; 37 | text-align: center; 38 | } 39 | 40 | .visualtimer > .panel-body > div > div.waitTimerTitle { 41 | font-size: 15px; 42 | } 43 | 44 | .visualtimer > .panel-body > div > div.waitTimerBody { 45 | font-size: 20px; 46 | text-align: center; 47 | } 48 | 49 | /* Visual Round */ 50 | 51 | .visualround { 52 | float: right; 53 | min-width: 100px; 54 | width: auto; 55 | } 56 | .visualround > .panel-body > div > div.rounddiv { 57 | font-size: 40px; 58 | text-align: center; 59 | } 60 | 61 | .visualround > .panel-body > div > div.stagediv { 62 | font-size: 20px; 63 | text-align: center; 64 | } 65 | 66 | .visualround > .panel-body > div > div > .number { 67 | 68 | } 69 | 70 | .visualround > .panel-body > div > div > span.text { 71 | font-size: 40%; 72 | text-align: center; 73 | 74 | } 75 | 76 | .visualround > .panel-body > div > div > div.title { 77 | font-size: 50%; 78 | text-align: center; 79 | } 80 | 81 | /* Visual State */ 82 | 83 | 84 | .visualstage { 85 | float: right; 86 | min-width: 200px; 87 | width: auto; 88 | } 89 | 90 | /* State Display */ 91 | 92 | .statedisplay { 93 | float: right; 94 | width: 300px; 95 | } 96 | 97 | .statedisplay > .panel-body > div { 98 | width: auto; 99 | font-size: 16px; 100 | } 101 | 102 | /* Money Talks */ 103 | 104 | .moneytalks { 105 | float: right; 106 | width: auto; 107 | min-width: 100px; 108 | } 109 | 110 | .moneytalks > .panel-body > span.moneytalksmoney { 111 | font-size: 40px; 112 | text-align: center; 113 | } 114 | 115 | .moneytalks > .panel-body > span.moneytalkscurrency { 116 | font-size: 12px; 117 | text-align: center; 118 | } 119 | 120 | /* Done Button */ 121 | 122 | .donebutton { 123 | float: right; 124 | width: auto; 125 | min-width: 100px; 126 | text-align: center; 127 | background: blue; 128 | } 129 | 130 | .donebutton > .panel-body { 131 | font-size: 20px; 132 | text-align: center; 133 | } 134 | 135 | 136 | /* Chat */ 137 | 138 | 139 | .chat { 140 | overflow-y: scroll; 141 | height: 200px; 142 | width: 450px; 143 | border: 1px solid #CCC; 144 | padding: 5px; 145 | } 146 | 147 | .chat > .panel-body > textarea { 148 | height: 100px; 149 | width: 400px; 150 | } 151 | 152 | .chat_me { 153 | font-weight: bold; 154 | } 155 | 156 | /* Feedback */ 157 | .feedback > .panel-body > textarea { 158 | padding: 5px; 159 | font-size: 16px; 160 | width: 98%; 161 | height: 300px; 162 | border: 1px solid #CCC; 163 | } 164 | 165 | /* Requirements */ 166 | 167 | .requirements { 168 | font-size: 1.2em; 169 | } 170 | 171 | .requirements-success { 172 | font-weight: bold; 173 | color: green; 174 | padding-top: 15px; 175 | padding-bottom: 15px; 176 | } 177 | 178 | .requirements-fail { 179 | font-weight: bold; 180 | color: red; 181 | padding-top: 15px; 182 | padding-bottom: 15px; 183 | } 184 | 185 | 186 | 187 | /* Waiting Room */ 188 | .waitingroom { 189 | font-size: 1.2em; 190 | } 191 | 192 | .waitingroom > .panel-body { 193 | text-align: center; 194 | } 195 | 196 | .waitingroom > .panel-body > div#timer-div { 197 | margin-top: 30px; 198 | } 199 | 200 | .waitingroom > .panel-body > div#player-count-div > p#player-count { 201 | font-size: 40px; 202 | clear: both; 203 | margin-top: 30px; 204 | } 205 | 206 | .waitingroom > .panel-body > div > .visualtimer { 207 | float: none; 208 | display: inline; 209 | font-size: 40px; 210 | margin: 0; 211 | } 212 | 213 | .waitingroom > .panel-body > div > span#span_dots { 214 | font-size: 40px; 215 | } -------------------------------------------------------------------------------- /public/stylesheets/window.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stylesheet for nodeGame window 3 | Copyright(c) 2014 Stefano Balietti 4 | MIT Licensed 5 | 6 | www.nodegame.org 7 | --- 8 | */ 9 | 10 | body { 11 | /* height: 98%; */ 12 | margin: auto; 13 | padding: 0 5px; 14 | font-family: Arial, Helvetica, sans-serif; 15 | font-size: 16px; 16 | color: black; 17 | } 18 | 19 | /* mainframe */ 20 | 21 | iframe#ng_mainframe { 22 | border: 0; 23 | padding: 10px; 24 | } 25 | 26 | .ng_mainframe-header-vertical-r { 27 | height: 100%; 28 | float: left; 29 | width: 86%; 30 | } 31 | 32 | .ng_mainframe-header-vertical-l { 33 | height: 100%; 34 | float: right; 35 | width: 86%; 36 | } 37 | 38 | .ng_mainframe-header-horizontal { 39 | height: 88%; 40 | width: 98%; 41 | clear: both; 42 | } 43 | 44 | /* header */ 45 | 46 | div#ng_header { 47 | padding: 2px 10px; 48 | color: black; 49 | border: 0; 50 | margin: 0; 51 | } 52 | 53 | .ng_header_position-horizontal-t, .ng_header_position-horizontal-b { 54 | width: 98%; 55 | height: 10%; 56 | } 57 | 58 | .ng_header_position-vertical-r { 59 | float: right; 60 | width: 10%; 61 | } 62 | 63 | .ng_header_position-vertical-l { 64 | float: left; 65 | width: 10%; 66 | } 67 | 68 | div#ng_waitScreen { 69 | z-index: 10000; 70 | filter: alpha(opacity=50); /*older IE*/ 71 | filter:progid:DXImageTransform.Microsoft.Alpha(opacity=50); /* IE */ 72 | -moz-opacity: .50; /*older Mozilla*/ 73 | -khtml-opacity: 0.5; /*older Safari*/ 74 | opacity: 0.5; /*supported by current Mozilla, Safari, and Opera*/ 75 | background-color:#000000; 76 | position:fixed; 77 | top:0px; left:0px; width:100%; height:100%; 78 | color:#FFFFFF; 79 | text-align:center; 80 | vertical-align:middle; 81 | font-weight: bold; 82 | font-size: 30px; 83 | padding: 20px; 84 | } 85 | 86 | canvas { 87 | border: 1px #CCC solid; 88 | } 89 | 90 | label { 91 | display: block; 92 | float: left; 93 | min-width: 200px; 94 | } 95 | input[type="range"] { 96 | margin-bottom: 5px; 97 | margin-left: 5px 98 | } 99 | 100 | /* General nodeGame */ 101 | 102 | .strong { 103 | font-weight: bold; 104 | } 105 | 106 | .underline { 107 | text-decoration: underline; 108 | } 109 | 110 | .strike { 111 | text-decoration: line-through; 112 | } 113 | -------------------------------------------------------------------------------- /ssl/README.md: -------------------------------------------------------------------------------- 1 | # SSL Certificate folder. 2 | 3 | ## Mac/Linux 4 | 5 | To create your private key and certificate run the following commands. 6 | 7 | openssl genrsa 1024 > private.key 8 | openssl req -new -key private.key -out cert.csr 9 | openssl x509 -req -in cert.csr -signkey private.key -out certificate.pem 10 | 11 | ### Update 12 | 13 | You can also use this one-liner: 14 | 15 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout private.key -out certificate.pem 16 | 17 | ## Windows 18 | 19 | Please look up on the Internet what is the exact procedure for the exact version of your operating system. 20 | -------------------------------------------------------------------------------- /test/test.ChannelRegistry.js: -------------------------------------------------------------------------------- 1 | // var ChannelRegistry = require('../lib/ChannelRegistry'); 2 | // 3 | // chReg = new ChannelRegistry(); 4 | // 5 | // var pid1 = chReg.addClient({id:'222'}); 6 | // var pid2 = chReg.addClient({id:'225'}); 7 | // var pid3 = chReg.addClient({id:'17'}); 8 | // var pid4 = chReg.addClient({id:'1', admin:true}); 9 | // var pid5 = chReg.addClient({id:'404', disconnected:true}); 10 | // chReg.removeClient(pid3); 11 | // chReg.moveClient(pid1, 'roomA'); 12 | // chReg.moveClient(pid2, 'roomB'); 13 | // chReg.moveClient(pid2, 'roomB1'); 14 | // chReg.moveClient(pid4, 'roomA'); 15 | // chReg.moveClientBack(pid4); 16 | // chReg.registerGameAlias('P1', pid1); 17 | // chReg.registerGameAlias('P2', pid2); 18 | // chReg.registerGameAlias('VeryDeepAlias', 'DeepAlias'); 19 | // chReg.registerGameAlias('DeepAlias', 'P1'); 20 | // chReg.registerGameAlias('GameToRoomAlias', 'HERO'); 21 | // chReg.registerGameAlias('RecursionA', 'RecursionB'); 22 | // chReg.registerGameAlias('RecursionB', 'RecursionA'); 23 | // chReg.registerRoomAlias('roomA', 'HERO', 'P1'); 24 | // chReg.registerRoomAlias('roomB', 'HERO', pid2); 25 | // 26 | // console.log('All IDs: ', chReg.getIds()); 27 | // console.log('All SIDs: ', chReg.getSids()); 28 | // console.log('All players: ', chReg.getPlayers()); 29 | // console.log('All admins: ', chReg.getAdmins()); 30 | // console.log('All connected clients: ', chReg.getConnected()); 31 | // console.log('All disconnected clients: ', chReg.getDisconnected()); 32 | // console.log(chReg.lookupClient('VeryDeepAlias') === pid1); 33 | // console.log(chReg.lookupClient('VeryDeepAlias', 'roomB1') === pid1); 34 | // console.log(chReg.lookupClient('GameToRoomAlias') === null); 35 | // console.log(chReg.lookupClient('GameToRoomAlias', 'roomA') === pid1); 36 | // console.log(chReg.lookupClient('GameToRoomAlias', 'roomB') === pid2); 37 | // console.log(chReg.lookupClient('RecursionA') === null); 38 | // console.log(); 39 | // console.log('IDs in room "roomA": ', chReg.getRoomIds('roomA')); 40 | 41 | /* 42 | console.log(chReg); 43 | console.log(); 44 | 45 | chReg.removeClient(pid1); 46 | chReg.removeClient('P2'); 47 | console.log(chReg); 48 | */ 49 | -------------------------------------------------------------------------------- /test/test.ServerNode.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | should = require('should'), 3 | request = require('request'), 4 | ServerNode = require('./../index.js').ServerNode; 5 | 6 | // Define nodeGame Server options. 7 | var server_options = { 8 | name: 'nodeGame Test Server', 9 | port: 8080, // for socket.io 10 | verbosity: 0, 11 | dumpsys: false, 12 | dumpmsg: false, 13 | mail: false, 14 | io: { 15 | set: { 16 | transports: ['websocket'], 17 | 'log level': -1 18 | } 19 | }, 20 | http: { 21 | } 22 | }; 23 | 24 | // Create a ServerNode instance and start it. 25 | var sn = new ServerNode(server_options); 26 | 27 | describe('nodegame-server: ', function(){ 28 | 29 | var root_url = 'http://127.0.0.1:' + server_options.port + '/'; 30 | 31 | describe('start the web-server', function(){ 32 | 33 | it('should return statusCode 200 for the root', function(done){ 34 | request(root_url, function(err, res, body){ 35 | res.statusCode.should.equal(200); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should return content-type text/plain for the root', function(done){ 41 | request(root_url, function(err, res, body){ 42 | res.headers['content-type'].should.equal('text/html; charset=utf-8'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should return statusCode 200 for player.css', function(done){ 48 | request(root_url + 'stylesheets/player.css', function(err, res, body){ 49 | res.statusCode.should.equal(200); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should return content-type text/css for player.css', function(done){ 55 | request(root_url + 'stylesheets/player.css', function(err, res, body){ 56 | res.headers['content-type'].should.equal('text/css; charset=UTF-8'); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should return statusCode 200 for monitor.css', function(done){ 62 | request(root_url + 'stylesheets/monitor.css', function(err, res, body){ 63 | res.statusCode.should.equal(200); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should return content-type text/css for monitor.css', function(done){ 69 | request(root_url + 'stylesheets/monitor.css', function(err, res, body){ 70 | res.headers['content-type'].should.equal('text/css; charset=UTF-8'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should return statusCode 200 for nodegame.js', function(done){ 76 | request(root_url + 'javascripts/nodegame.js', function(err, res, body){ 77 | res.statusCode.should.equal(200); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should return content-type text/javascript for nodegame.js', function(done){ 83 | request(root_url + 'javascripts/nodegame.js', function(err, res, body){ 84 | res.headers['content-type'].should.equal('application/javascript'); 85 | done(); 86 | }); 87 | }); 88 | 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/test.socketio.js: -------------------------------------------------------------------------------- 1 | // var util = require('util'), 2 | // should = require('should'), 3 | // io = require('socket.io-client'), 4 | // ServerNode = require('./../ServerNode'); 5 | 6 | // // Define nodeGame Server options. 7 | // var server_options = { 8 | // name: 'nodeGame Test Server', 9 | // port: 8080, // for socket.io 10 | // verbosity: 0, 11 | // dumpsys: false, 12 | // dumpmsg: false, 13 | // mail: false, 14 | // io: { 15 | // set: { 16 | // transports: ['websocket'], 17 | // 'log level': -1, 18 | // 'force new connection': true 19 | // } 20 | // }, 21 | // http: { 22 | // } 23 | // }; 24 | 25 | // // Create a ServerNode instance and start it. 26 | // var sn = new ServerNode(server_options); 27 | 28 | // describe('nodegame-server: ', function(){ 29 | 30 | // var socketURL = 'http://localhost:' + server_options.port + '/'; 31 | 32 | // var player1 = {'name': 'Player1'}; 33 | // var player2 = {'name': 'Player2'}; 34 | 35 | // describe('Game Server', function(){ 36 | 37 | // it('Should broadcast a hello-world message', function(done){ 38 | // var io_player1 = io.connect(socketURL, server_options.io); 39 | 40 | 41 | 42 | 43 | // }); 44 | 45 | // }); 46 | 47 | // // describe('start the web-server', function(){ 48 | // // 49 | // // it('should return statusCode 200 for the root', function(done){ 50 | // // request(root_url, function(err, res, body){ 51 | // // res.statusCode.should.equal(200); 52 | // // done(); 53 | // // }); 54 | // // }); 55 | // // 56 | // // it('should return content-type text/plain for the root', function(done){ 57 | // // request(root_url, function(err, res, body){ 58 | // // res.headers['content-type'].should.equal('text/html; charset=utf-8'); 59 | // // done(); 60 | // // }); 61 | // // }); 62 | // // 63 | // // }); 64 | 65 | // }); 66 | -------------------------------------------------------------------------------- /views/error/generic.jade: -------------------------------------------------------------------------------- 1 | h2 Sorry! 2 | #message An error has occurred. Please try again later. 3 | -------------------------------------------------------------------------------- /views/error/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title nodeGame server generic error 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | #main!= body 8 | -------------------------------------------------------------------------------- /views/error/nojs.jade: -------------------------------------------------------------------------------- 1 | h2 Sorry! 2 | #message If you reach this page, you probably do not have Javascript enabled in your browser. Please enable it and try again. 3 | 4 | -------------------------------------------------------------------------------- /views/game_template.jade: -------------------------------------------------------------------------------- 1 | !!! 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | script(type='text/javascript', src='/javascripts/jquery-1.7.2.min.js') 7 | script(type='text/javascript', src='/javascripts/ember-0.9.6.min.js') 8 | 9 | script(type='text/javascript', src='/socket.io/socket.io.js') 10 | script(type='text/javascript', src='/javascripts/nodegame.js') 11 | 12 | script(type='text/x-handlebars', data-template-name='list') 13 | {{#with List.items}} 14 | ul 15 | {{#each items}} 16 | li 17 | {{name}} 18 | {{/each}} 19 | {{/with}} 20 | 21 | body 22 | div(class='container')!= content 23 | -------------------------------------------------------------------------------- /views/homepage.jade: -------------------------------------------------------------------------------- 1 | html5 2 | head 3 | meta(name='description', content='') 4 | meta(name='keywords', content='') 5 | meta(name='viewport', content='width=device-width, initial-scale=1.0') 6 | link(rel='stylesheet', href='//cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css') 7 | script(src='//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js') 8 | script(src='//cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js') 9 | link(rel='stylesheet', href='/stylesheets/homepage.css') 10 | link(rel='stylesheet', href='https://fonts.googleapis.com/icon?family=Material+Icons') 11 | body 12 | div.navbar-fixed 13 | nav.z-depth-4 14 | div.nav-wrapper.brand-logo 15 | if (logo) 16 | img(src="images/nodegame_logo.png", width="70", align="left") 17 | span #{title} 18 | br 19 | br 20 | br 21 | .container 22 | .row 23 | each game in games 24 | span(class='col s12 m6 l4 xl4 center') 25 | div(style='border-radius: 15px', class='center card lighten-2 small z-depth-3 '+ game.color) 26 | div(style='border-radius: 15px; top: 15%; width:80%; height:60%', class='card-panel center waves-effect waves-light hoverable lighten-3 activator z-depth-3 white') 27 | span(class='card-title text-darken-1 activator') 28 | h4(class='activator') #{game.name} 29 | if (game.icon) 30 | i(class='material-icons large') #{game.icon} 31 | .card-reveal 32 | span(class='card-title right') x 33 | p(class='flow-text') #{game.description} 34 | ul 35 | if (game.url) 36 | li 37 | a(href=game.url, class='btn-floating cyan tooltipped z-depth-2', target='_blank', data-delay="0", data-tooltip='Github Project') 38 | i(class='material-icons') code 39 | if (game.publication) 40 | li 41 | a(href=game.publication, class='btn-floating pink tooltipped z-depth-2', target='_blank', data-delay="0", data-tooltip='Research Publication') 42 | i(class='material-icons') assessment 43 | if (game.wiki) 44 | li 45 | a(href=game.wiki, class='btn-floating blue tooltipped z-depth-2', target='_blank', data-delay="0", data-tooltip='Wikipedia') 46 | i(class='material-icons') book 47 | if (game.external) 48 | li 49 | a(href=game.demo, class='btn-floating btn-large green pulse tooltipped z-depth-2', target='_blank', data-delay="0", data-tooltip='Play') 50 | i(class='large material-icons') play_circle_filled 51 | else 52 | li 53 | a(href='/'+game._name, class='btn-floating btn-large green pulse tooltipped z-depth-2', target='_blank', data-delay="0", data-tooltip='Play') 54 | i(class='large material-icons') play_circle_filled 55 | if nodeGameCard 56 | span(class='col s12 m6 l4 xl4 center') 57 | div(style='border-radius: 15px', class='center card lighten-2 small z-depth-3 white') 58 | div(style='border-radius: 15px; top: 15%; width:80%; height:60%', class='card-panel center waves-effect waves-light hoverable lighten-3 activator z-depth-3 white') 59 | span(class='card-title text-darken-1 activator') 60 | h4(class='activator') About 61 | i(class='material-icons medium') info 62 | .card-reveal 63 | span(class='card-title right') x 64 | p(class='flow-text') nodeGame is a framework for fast, scalable, multiplayer, real-time experiments in the browser. 65 | 66 | 67 | footer.footer 68 | if (footerContent) 69 | .container 70 | .row 71 | .col.l6.s12 72 | h5.white-text More about 73 | a(href='https://nodegame.org/', target='_', class='tooltipped', data-delay='0', data-tooltip='Fast, scalable JavaScript for large-scale, online, multiplayer, real-time games and experiments in the browser.') nodeGame 74 | span(class="col 16 s12", style="width: 50%;") 75 | p.grey-text.text-lighten-4 76 | a(href="https://twitter.com/nodegameorg", class="twitter-follow-button social", data-show-count="false", data-size="large", data-show-screen-name="false") Follow @nodegameorg 77 | script(async, src="//platform.twitter.com/widgets.js", charset="utf-8") 78 | a(class="github-button social", href="https://github.com/nodeGame/nodegame", data-icon="octicon-star", data-size="large", data-show-count="true", aria-label="Star nodeGame/nodegame on GitHub") Star 79 | script(async, defer, src="https://buttons.github.io/buttons.js", charset="utf-8") 80 | a(class="social", style="margin-top:0; top:0;", href="https://groups.google.com/forum/?fromgroups#!forum/nodegame", target="_blank") 81 | button(id="forum-button") 82 | img(style="margin-top:0; top:0; height: 20px", src="images/google-group-scaled.png", alt="Forum", align="right") 83 | span Forum 84 | p Balietti (2017) "nodeGame: Real-Time, Synchronous, Online Experiments in the Browser" Behavior Research Methods Volume 49, Issue 5, pp. 1696-1715. 85 | 86 | 87 | .col.l4.offset-l2.s12 88 | h6.white-text Web design by 89 | a(href='https://github.com/ewenw/', target='_') Ewen 90 | -------------------------------------------------------------------------------- /views/index_simple.jade: -------------------------------------------------------------------------------- 1 | h2= title -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title= title 5 | body 6 | div(class='container')!= body 7 | --------------------------------------------------------------------------------