├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── serve ├── conf └── conf.js.dist ├── gulpfile.babel.js ├── package-lock.json ├── package.json ├── public └── favicon.png ├── src ├── bootstrapper.jsx ├── components │ ├── auth │ │ ├── index.jsx │ │ └── index.styl │ ├── characters │ │ └── index.jsx │ ├── game │ │ ├── chat │ │ │ ├── blizzIcon.gif │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── controls.jsx │ │ ├── hud │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── index.jsx │ │ ├── index.styl │ │ ├── portrait │ │ │ ├── images │ │ │ │ ├── icon-portrait.png │ │ │ │ └── portrait.png │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── quests │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ └── stats │ │ │ ├── index.jsx │ │ │ └── index.styl │ ├── kit │ │ └── index.jsx │ ├── realms │ │ └── index.jsx │ └── wowser │ │ ├── images │ │ └── logo.png │ │ ├── index.jsx │ │ ├── index.styl │ │ ├── session.jsx │ │ └── ui │ │ ├── form │ │ └── index.styl │ │ ├── frame │ │ ├── dividers │ │ │ ├── images │ │ │ │ ├── horizontal.png │ │ │ │ └── thick-horizontal.png │ │ │ └── index.styl │ │ ├── images │ │ │ ├── panel-headless.png │ │ │ ├── panel.png │ │ │ ├── thick.png │ │ │ └── thin.png │ │ └── index.styl │ │ ├── index.styl │ │ ├── screen.styl │ │ ├── type.styl │ │ └── widgets │ │ ├── button.styl │ │ └── index.styl ├── index.html ├── lib │ ├── auth │ │ ├── challenge-opcode.js │ │ ├── handler.js │ │ ├── opcode.js │ │ └── packet.js │ ├── characters │ │ ├── character.js │ │ └── handler.js │ ├── config.js │ ├── crypto │ │ ├── big-num.js │ │ ├── crypt.js │ │ ├── hash.js │ │ ├── hash │ │ │ └── sha1.js │ │ └── srp.js │ ├── game │ │ ├── chat │ │ │ ├── chatEnum.js │ │ │ ├── handler.js │ │ │ ├── langEnum.js │ │ │ └── message.js │ │ ├── entity.js │ │ ├── guid.js │ │ ├── handler.js │ │ ├── opcode.js │ │ ├── packet.js │ │ ├── player.js │ │ ├── unit.js │ │ └── world │ │ │ └── handler.js │ ├── index.js │ ├── net │ │ ├── loader.js │ │ ├── packet.js │ │ └── socket.js │ ├── realms │ │ ├── handler.js │ │ └── realm.js │ └── utils │ │ ├── array-util.js │ │ └── object-util.js └── spec │ ├── .eslintrc │ ├── sample-spec.js │ └── spec-helper.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-class-properties", 4 | "transform-export-extensions", 5 | "transform-function-bind", 6 | "transform-es2015-block-scoping", 7 | "add-module-exports", 8 | "transform-es2015-modules-commonjs" 9 | ], 10 | 11 | "presets": [ 12 | "react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - 'lib/*' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Allow vertically aligning values 4 | "no-multi-spaces": [0], 5 | 6 | // See: https://github.com/yannickcr/eslint-plugin-react/issues/128 7 | "react/sort-comp": [0], 8 | 9 | // Disable newer additions originating from Airbnb 10 | "arrow-body-style": [0], 11 | "prefer-arrow-callback": [0], 12 | "space-before-function-paren": [0], 13 | "react/jsx-indent-props": [0], 14 | "react/jsx-closing-bracket-location": [0] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /lib/ 3 | /node_modules/ 4 | /public/scripts/ 5 | /public/styles/ 6 | /public/templates/ 7 | /spec/ 8 | /nbproject/private/ 9 | /npm-debug.log* 10 | /conf/* 11 | !/conf/conf.js.dist 12 | 13 | *~ 14 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: ['public/**', 'src/**', 'bundle.js', 'gulpfile.babel.js'] 3 | include-all-sources: true 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '5' 6 | - '6' 7 | matrix: 8 | fast_finish: true 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | env: 16 | - CXX=g++-4.8 17 | script: npm test --coverage 18 | after_script: codeclimate-test-reporter < coverage/lcov.info 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v0.0.1 - November 13, 2014 4 | 5 | - Initial release. 6 | 7 | ### v0.0.0 - November 1, 2014 8 | 9 | - Placeholder release. 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Licensed under the **MIT** license 4 | 5 | > Copyright (c) 2012-2015 Tim Kurvers <> 6 | > 7 | > Permission is hereby granted, free of charge, to any person obtaining 8 | > a copy of this software and associated documentation files (the 9 | > "Software"), to deal in the Software without restriction, including 10 | > without limitation the rights to use, copy, modify, merge, publish, 11 | > distribute, sublicense, and/or sell copies of the Software, and to 12 | > permit persons to whom the Software is furnished to do so, subject to 13 | > the following conditions: 14 | > 15 | > The above copyright notice and this permission notice shall be 16 | > included in all copies or substantial portions of the Software. 17 | > 18 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WoW Chat, browser game client 2 | 3 | WoW Chat, browser game client based on **WoWser**. 4 | 5 | Licensed under the **MIT** license, see LICENSE for more information. 6 | 7 | ## Background 8 | 9 | Wowser is a proof-of-concept of getting a triple-A game to run in a webbrowser, 10 | attempting to tackle a wide variety of challenges: data retrieval, socket 11 | connections, cryptography, 3d graphics, binary data handling, background workers 12 | and audio, to name a few. 13 | 14 | ## Features 15 | 16 | Wowser is aiming to be both a low-level API as well as a graphical client, 17 | interacting with compatible game servers. 18 | 19 | Wowser is compatible with all server based on Mangos 335 such as AzerothCore, TrinityCore 20 | and Mangos itself. Of course other servers that use the same opcode specifications of projects above, are also supported . 21 | 22 | 23 | At present, this project is capable of: 24 | 25 | - Authenticating by username / password. 26 | - Listing available realms. 27 | - Connecting to a realm. 28 | - Listing characters available on a realm. 29 | - Joining the game world with a character. 30 | - Chat in game on following channels: Guild, Say, Wispers, World (hardcoded custom channel) 31 | - Logging game world packets, such as when a creature moves in the vicinity. 32 | 33 | ## Browser Support 34 | 35 | Wowser is presumed to be working on any browser supporting [JavaScript's typed 36 | arrays] and at the very least a binary version of the WebSocket protocol. 37 | 38 | ## Development 39 | 40 | 1. Clone the repository: 41 | 42 | ```shell 43 | git clone git://github.com/wowserhq/wowser.git 44 | ``` 45 | 46 | 2. Download and install [Node.js] – including `npm` – for your platform. 47 | 48 | 3. Install dependencies: 49 | 50 | ```shell 51 | npm install 52 | ``` 53 | 54 | 4. Install [StormLib] and [BLPConverter], which are used to handle Blizzard's 55 | game files. 56 | 57 | ### Run the client 58 | 59 | Create a copy of **conf/conf.js.dist** file and name it **conf/conf.js** (don't delete the .dist file) 60 | then configure it. 61 | 62 | [Webpack]'s development server monitors source files and builds: 63 | 64 | ```shell 65 | npm run web-dev 66 | ``` 67 | 68 | Wowser will be served on `http://127.0.0.1:8080/webpack-dev-server/`. 69 | 70 | ### Socket proxies 71 | 72 | To utilize raw TCP connections a WebSocket proxy is required for JavaScript 73 | clients. 74 | 75 | [Websockify] can - among other things - act as a proxy for raw TCP sockets. 76 | 77 | For now, you will want to proxy both port 3724 (auth) and 8085 (world). 78 | 79 | #### - Proxy port in localhost 80 | 81 | If you want to connect this web client to a server in the same machine you can change the authserver port from 8085 to 8086 in the **auth realmlist table** using: 82 | ```SQL 83 | UPDATE `realmlist` SET `port`=8086 WHERE `id`=1; 84 | ``` 85 | 86 | Run proxy for localhost: 87 | ```shell 88 | npm run proxy 3725 127.0.0.1:3724 89 | npm run proxy 8086 127.0.0.1:8085 90 | ``` 91 | 92 | While login using the web client, use the port 3725. 93 | 94 | #### - Proxy port for a public server 95 | 96 | Run proxy for a public server: 97 | ```shell 98 | npm run proxy 3724 server.realmlist:3724 99 | npm run proxy 8085 server.realmlist:8085 100 | ``` 101 | replacing *server.realmlist* with the realmlist of the server. 102 | 103 | 104 | ## Contribution 105 | 106 | When contributing, please: 107 | 108 | - Fork the repository 109 | - Open a pull request (preferably on a separate branch) 110 | 111 | [Babel]: https://babeljs.io/ 112 | [ES2015]: https://babeljs.io/docs/learn-es2015/ 113 | [Gulp]: http://gulpjs.com/ 114 | [JavaScript's typed arrays]: http://caniuse.com/#search=typed%20arrays 115 | [Mocha]: http://mochajs.org/ 116 | [Node.js]: http://nodejs.org/#download 117 | [Websockify]: https://github.com/kanaka/websockify/ 118 | [soon™]: http://www.wowwiki.com/Soon 119 | [webpack]: http://webpack.github.io/ 120 | -------------------------------------------------------------------------------- /bin/serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Cluster = require('../lib/server/cluster'); 4 | const ServerConfig = require('../lib/server/config'); 5 | 6 | ServerConfig.verify().then(function() { 7 | const cluster = new Cluster(); 8 | cluster.start(); 9 | }); 10 | -------------------------------------------------------------------------------- /conf/conf.js.dist: -------------------------------------------------------------------------------- 1 | var Conf = { 2 | "welcome" : "Welcome to wowser client", 3 | "slogan" : "Browser JavaScript and WebGL Game Client", 4 | "serverhost" : window.location.hostname, 5 | "authport" : "3724" 6 | } 7 | 8 | module.exports = Conf; -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import Config from 'configstore'; 2 | import babel from 'gulp-babel'; 3 | import cache from 'gulp-cached'; 4 | import del from 'del'; 5 | import gulp from 'gulp'; 6 | import mocha from 'gulp-mocha'; 7 | import pkg from './package.json'; 8 | import plumber from 'gulp-plumber'; 9 | 10 | const config = { 11 | db: new Config(pkg.name), 12 | scripts: 'src/**/*.js', 13 | specs: 'spec/**/*.js' 14 | }; 15 | 16 | gulp.task('reset', function(done) { 17 | config.db.clear(); 18 | process.stdout.write(`\n> Settings deleted from ${config.db.path}\n\n`); 19 | done(); 20 | }); 21 | 22 | gulp.task('clean', function(cb) { 23 | del([ 24 | 'lib/*', 25 | 'spec/*' 26 | ], cb); 27 | }); 28 | 29 | gulp.task('scripts', function() { 30 | return gulp.src(config.scripts) 31 | .pipe(cache('babel')) 32 | .pipe(plumber()) 33 | .pipe(babel()) 34 | .pipe(gulp.dest('.')); 35 | }); 36 | 37 | gulp.task('spec', function() { 38 | return gulp.src(config.specs, { read: false }) 39 | .pipe(plumber()) 40 | .pipe(mocha()); 41 | }); 42 | 43 | gulp.task('rebuild', gulp.series( 44 | 'clean', 'scripts' 45 | )); 46 | 47 | gulp.task('watch', function(done) { 48 | gulp.watch(config.scripts, gulp.series('scripts', 'spec')); 49 | done(); 50 | }); 51 | 52 | gulp.task('default', gulp.series( 53 | 'rebuild', 'spec', 'watch' 54 | )); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wowser", 3 | "version": "0.0.1", 4 | "description": "Browser JavaScript and WebGL Game Client", 5 | "main": "lib/client/index.js", 6 | "files": [ 7 | "bin", 8 | "lib", 9 | "public", 10 | "LICENSE.md", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "gulp": "gulp", 15 | "lint": "eslint src *.js --ext .js --ext .jsx; exit 0", 16 | "proxy": "websockify", 17 | "start": "node bin/serve", 18 | "serve": "node bin/serve", 19 | "serve-dev": "nodemon bin/serve -w lib/server", 20 | "pretest": "gulp rebuild", 21 | "reset": "gulp reset", 22 | "test": "istanbul test ./node_modules/mocha/bin/_mocha -- spec --recursive", 23 | "web-dev": "webpack-dev-server --host 0.0.0.0", 24 | "web-release": "webpack --optimize --progress" 25 | }, 26 | "repository": "wowserhq/wowser", 27 | "author": "Tim Kurvers ", 28 | "license": "MIT", 29 | "keywords": [ 30 | "azerothcore", 31 | "mangos", 32 | "trinitycore", 33 | "web", 34 | "client" 35 | ], 36 | "dependencies": { 37 | "array-find": "^0.1.1", 38 | "bluebird": "^2.10.0", 39 | "byte-buffer": "^1.0.3", 40 | "classnames": "^2.2.0", 41 | "configstore": "^1.2.0", 42 | "deep-equal": "^1.0.0", 43 | "express": "^4.9.3", 44 | "globby": "^5.0.0", 45 | "inquirer": "^0.8.5", 46 | "jsbn": "github:timkurvers/jsbn#wowser", 47 | "keymaster": "^1.6.2", 48 | "morgan": "^1.3.2", 49 | "normalize.css": "^3.0.3", 50 | "pngjs": "^2.3.0", 51 | "react": "^0.14.3", 52 | "react-dom": "^0.14.3", 53 | "react-tabs": "^0.8.2", 54 | "three": "^0.77.0", 55 | "websockify": "^0.7.1", 56 | "ws": "1.1.1" 57 | }, 58 | "devDependencies": { 59 | "babel-core": "^6.10.0", 60 | "babel-eslint": "^6.1.0", 61 | "babel-loader": "^6.2.0", 62 | "babel-plugin-add-module-exports": "^0.2.0", 63 | "babel-plugin-transform-class-properties": "^6.10.0", 64 | "babel-plugin-transform-es2015-block-scoping": "^6.10.0", 65 | "babel-plugin-transform-es2015-modules-commonjs": "^6.8.0", 66 | "babel-plugin-transform-es2015-parameters": "^6.9.0", 67 | "babel-plugin-transform-export-extensions": "^6.8.0", 68 | "babel-plugin-transform-function-bind": "^6.8.0", 69 | "babel-preset-react": "^6.5.0", 70 | "chai": "^3.5.0", 71 | "codeclimate-test-reporter": "^0.3.0", 72 | "css-loader": "^0.23.0", 73 | "del": "^1.2.0", 74 | "eslint": "^2.13.0", 75 | "eslint-config-airbnb": "^6.2.0", 76 | "eslint-loader": "^1.3.0", 77 | "eslint-plugin-react": "^4.3.0", 78 | "file-loader": "^0.9.0", 79 | "glslify-import": "^3.0.0", 80 | "glslify-loader": "github:wowserhq/glslify-loader#query-opts", 81 | "gulp": "^4.0", 82 | "gulp-babel": "^6.1.0", 83 | "gulp-cached": "^1.1.0", 84 | "gulp-mocha": "2.2.0", 85 | "gulp-plumber": "^1.1.0", 86 | "gulp-remember": "^0.3.0", 87 | "gulp-stylus": "^2.5.0", 88 | "html-webpack-plugin": "^2.21.0", 89 | "istanbul": "^0.4.0", 90 | "json-loader": "^0.5.0", 91 | "mocha": "^2.5.0", 92 | "nodemon": "^1.9.0", 93 | "raw-loader": "^0.5.0", 94 | "sinon": "^1.17.0", 95 | "sinon-chai": "^2.8.0", 96 | "style-loader": "^0.13.0", 97 | "stylus-loader": "^1.6.0", 98 | "url-loader": "^0.5.0", 99 | "webpack": "^1.13.0", 100 | "webpack-dev-server": "^1.14.0", 101 | "worker-loader": "^0.7.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/public/favicon.png -------------------------------------------------------------------------------- /src/bootstrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Wowser from './components/wowser'; 5 | 6 | ReactDOM.render(, document.querySelector('app')); 7 | -------------------------------------------------------------------------------- /src/components/auth/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import session from '../wowser/session'; 4 | 5 | class AuthScreen extends React.Component { 6 | 7 | static id = 'auth'; 8 | static title = 'Authentication'; 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | host: session.auth.defhost, 15 | port: session.auth.defport, 16 | username: '', 17 | password: '' 18 | }; 19 | 20 | this._onAuthenticate = ::this._onAuthenticate; 21 | this._onChange = ::this._onChange; 22 | this._onSubmit = ::this._onSubmit; 23 | this._onConnect = ::this._onConnect; 24 | 25 | session.auth.on('connect', this._onConnect); 26 | session.auth.on('reject', session.auth.disconnect); 27 | session.auth.on('authenticate', this._onAuthenticate); 28 | } 29 | 30 | componentWillUnmount() { 31 | session.auth.removeListener('connect', this._onConnect); 32 | session.auth.removeListener('reject', session.auth.disconnect); 33 | session.auth.removeListener('authenticate', this._onAuthenticate); 34 | } 35 | 36 | connect(host, port) { 37 | session.auth.connect(host, port); 38 | } 39 | 40 | authenticate(username, password) { 41 | session.auth.authenticate(username, password); 42 | } 43 | 44 | _onAuthenticate() { 45 | session.screen = 'realms'; 46 | } 47 | 48 | _onChange(event) { 49 | this.setState({ 50 | [event.target.name]: event.target.value 51 | }); 52 | } 53 | 54 | _onConnect() { 55 | this.authenticate(this.state.username, this.state.password); 56 | } 57 | 58 | _onSubmit(event) { 59 | event.preventDefault(); 60 | this.connect(this.state.host, this.state.port); 61 | } 62 | 63 | render() { 64 | return ( 65 | 66 |
67 |

Authentication

68 | 69 |
70 | 71 |

72 | Note: Wowser requires a WebSocket proxy, see the README on GitHub. 73 |

74 | 75 |
76 |
77 | 78 | 80 | 81 | 82 | 84 |
85 | 86 |
87 | 88 | 90 | 91 | 92 | 94 |
95 | 96 |
97 | 98 | 99 |
100 |
101 |
102 | ); 103 | } 104 | 105 | } 106 | 107 | export default AuthScreen; 108 | -------------------------------------------------------------------------------- /src/components/auth/index.styl: -------------------------------------------------------------------------------- 1 | wowser .auth 2 | 3 | .panel 4 | max-width: 300px 5 | -------------------------------------------------------------------------------- /src/components/characters/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import session from '../wowser/session'; 4 | 5 | class CharactersScreen extends React.Component { 6 | 7 | static id = 'characters'; 8 | static title = 'Character Selection'; 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | character: null, 15 | characters: [] 16 | }; 17 | 18 | this._onCharacterSelect = ::this._onCharacterSelect; 19 | this._onJoin = ::this._onJoin; 20 | this._onRefresh = ::this._onRefresh; 21 | this._onSubmit = ::this._onSubmit; 22 | 23 | session.characters.on('refresh', this._onRefresh); 24 | session.game.on('join', this._onJoin); 25 | 26 | this.refresh(); 27 | } 28 | 29 | componentWillUnmount() { 30 | session.characters.removeListener('refresh', this._onRefresh); 31 | session.game.removeListener('join', this._onJoin); 32 | } 33 | 34 | join(character) { 35 | session.game.join(character); 36 | } 37 | 38 | refresh() { 39 | session.characters.refresh(); 40 | } 41 | 42 | _onCharacterSelect(event) { 43 | this.setState({ 44 | index : event.target.value, 45 | character: this.state.characters[event.target.value] 46 | }); 47 | } 48 | 49 | _onJoin() { 50 | session.screen = 'game'; 51 | } 52 | 53 | _onRefresh() { 54 | const characters = session.characters.list; 55 | this.setState({ 56 | character: characters[0], 57 | characters: characters 58 | }); 59 | } 60 | 61 | _onSubmit(event) { 62 | event.preventDefault(); 63 | this.join(this.state.character); 64 | } 65 | 66 | render() { 67 | return ( 68 | 69 |
70 |

Character Selection

71 | 72 |
73 | 74 |

75 | If you want to create a character, please use the Desktop Client 76 |

77 | 78 |
79 |
80 | 90 |
91 | 92 |
93 | 94 | 95 | 96 |
97 |
98 |
99 | ); 100 | } 101 | 102 | } 103 | 104 | export default CharactersScreen; 105 | -------------------------------------------------------------------------------- /src/components/game/chat/blizzIcon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/game/chat/blizzIcon.gif -------------------------------------------------------------------------------- /src/components/game/chat/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from 'classnames'; 3 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 4 | import ChatEnum from '../../../lib/game/chat/chatEnum'; 5 | import './index.styl'; 6 | 7 | import session from '../../wowser/session'; 8 | 9 | class ChatPanel extends React.Component { 10 | 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | selectedIndex : 0, 16 | playerNames : session.chat.playerNames, 17 | sayText: '', 18 | wispText: '', 19 | guildText: '', 20 | worldText: '', 21 | wispDest: null, 22 | sayMessages: session.chat.sayMessages, 23 | wispMessages: session.chat.wispMessages, 24 | guildMessages: session.chat.guildMessages, 25 | worldMessages: session.chat.worldMessages, 26 | logsMessages: session.chat.logsMessages 27 | }; 28 | 29 | this._onChangeSay = ::this._onChangeSay; 30 | this._onMessageSay = ::this._onMessageSay; 31 | this._onSubmitSay = ::this._onSubmitSay; 32 | this._onChangeGuild = ::this._onChangeGuild; 33 | this._onMessageGuild = ::this._onMessageGuild; 34 | this._onSubmitGuild = ::this._onSubmitGuild; 35 | this._onChangeWorld = ::this._onChangeWorld; 36 | this._onMessageWorld = ::this._onMessageWorld; 37 | this._onSubmitWorld = ::this._onSubmitWorld; 38 | this._onChangeWisp = ::this._onChangeWisp; 39 | this._onChangeWispDest = ::this._onChangeWispDest; 40 | this._onMessageWisp = ::this._onMessageWisp; 41 | this._onSubmitWisp = ::this._onSubmitWisp; 42 | this._setWispDest = ::this._setWispDest; 43 | this._onMessageLogs = ::this._onMessageLogs; 44 | this._selTab = ::this._selTab; 45 | this._onMessageDispatch = ::this._onMessageDispatch; 46 | 47 | session.chat.on('message', this._onMessageDispatch); 48 | } 49 | 50 | componentDidUpdate() { 51 | if (document.getElementById("sayMessages")) 52 | document.getElementById("sayMessages").scrollTop = document.getElementById("sayMessages").scrollHeight; 53 | else if (document.getElementById("worldMessages")) 54 | document.getElementById("worldMessages").scrollTop = document.getElementById("worldMessages").scrollHeight; 55 | else if (document.getElementById("guildMessages")) 56 | document.getElementById("guildMessages").scrollTop = document.getElementById("guildMessages").scrollHeight; 57 | else if (document.getElementById("wispMessages")) 58 | document.getElementById("wispMessages").scrollTop = document.getElementById("wispMessages").scrollHeight; 59 | else if (document.getElementById("logsMessages")) 60 | document.getElementById("logsMessages").scrollTop = document.getElementById("logsMessages").scrollHeight; 61 | } 62 | 63 | _selTab(e) { 64 | this.setState({selectedIndex : e }); 65 | session.chat.emit("message",null); // workaround to keep chat box at bottom 66 | } 67 | 68 | _onMessageDispatch(message, type) { 69 | switch(type) { 70 | case ChatEnum.CHAT_MSG_CHANNEL: 71 | this._onMessageWorld(); 72 | document.getElementById("react-tabs-0").setAttribute("aria-selected", "true"); 73 | break; 74 | case ChatEnum.CHAT_MSG_GUILD: 75 | this._onMessageGuild(); 76 | document.getElementById("react-tabs-2").setAttribute("aria-selected", "true"); 77 | break; 78 | case ChatEnum.CHAT_MSG_WHISPER: 79 | case ChatEnum.CHAT_MSG_WHISPER_INFORM: 80 | case ChatEnum.CHAT_MSG_WHISPER_FOREIGN: 81 | this._onMessageWisp(); 82 | document.getElementById("react-tabs-4").setAttribute("aria-selected", "true"); 83 | break; 84 | case ChatEnum.CHAT_MSG_SAY: 85 | case ChatEnum.CHAT_MSG_SYSTEM: 86 | case ChatEnum.CHAT_MSG_EMOTE: 87 | case ChatEnum.CHAT_MSG_YELL: 88 | this._onMessageSay(); 89 | document.getElementById("react-tabs-6").setAttribute("aria-selected", "true"); 90 | break; 91 | default: 92 | this._onMessageLogs(); 93 | break; 94 | } 95 | } 96 | 97 | /* 98 | * SAY 99 | */ 100 | 101 | sendSay(text) { 102 | const message = session.chat.create(); 103 | message.text = text; 104 | session.chat.send(text, ChatEnum.CHAT_MSG_SAY); 105 | } 106 | 107 | _onChangeSay(event) { 108 | this.setState({ sayText: event.target.value }); 109 | } 110 | 111 | _onMessageSay() { 112 | this.setState({ sayMessages: session.chat.sayMessages }); 113 | } 114 | 115 | _onSubmitSay(event) { 116 | event.preventDefault(); 117 | if (this.state.sayText) { 118 | this.sendSay(this.state.sayText); 119 | this.setState({ sayText: '' }); 120 | } 121 | } 122 | 123 | /* 124 | * GUILD 125 | */ 126 | 127 | sendGuild(text) { 128 | const message = session.chat.create(); 129 | message.text = text; 130 | session.chat.send(text,ChatEnum.CHAT_MSG_GUILD); 131 | } 132 | 133 | _onChangeGuild(event) { 134 | this.setState({ guildText: event.target.value }); 135 | } 136 | 137 | _onMessageGuild() { 138 | this.setState({ guildMessages: session.chat.guildMessages }); 139 | } 140 | 141 | _onSubmitGuild(event) { 142 | event.preventDefault(); 143 | if (this.state.guildText) { 144 | this.sendGuild(this.state.guildText); 145 | this.setState({ guildText: '' }); 146 | } 147 | } 148 | 149 | /** 150 | * WORLD 151 | */ 152 | sendWorld(text) { 153 | const message = session.chat.create(); 154 | message.text = text; 155 | session.chat.send(text,ChatEnum.CHAT_MSG_CHANNEL); 156 | } 157 | 158 | _onChangeWorld(event) { 159 | this.setState({ worldText: event.target.value }); 160 | } 161 | 162 | _onMessageWorld() { 163 | this.setState({ worldMessages: session.chat.worldMessages }); 164 | } 165 | 166 | _onSubmitWorld(event) { 167 | event.preventDefault(); 168 | if (this.state.worldText) { 169 | this.sendWorld(this.state.worldText); 170 | this.setState({ worldText: '' }); 171 | } 172 | } 173 | 174 | /** 175 | * WISP 176 | */ 177 | sendWisp(text) { 178 | const message = session.chat.create(); 179 | message.text = text; 180 | session.chat.send(text, ChatEnum.CHAT_MSG_WHISPER, this.state.wispDest); 181 | } 182 | 183 | _onChangeWisp(event) { 184 | this.setState({ wispText: event.target.value }); 185 | } 186 | 187 | _onChangeWispDest(event) { 188 | this.setState({ wispDest: event.target.value }); 189 | } 190 | 191 | _onMessageWisp() { 192 | this.setState({ wispMessages: session.chat.wispMessages }); 193 | } 194 | 195 | _setWispDest(e) { 196 | e.preventDefault(); 197 | var guid=e.target.parentElement.classList[0]; 198 | guid = parseInt(guid); 199 | 200 | if (guid>0) { 201 | var name = this.state.playerNames[guid].name; 202 | var nome = "Gnoma"; 203 | 204 | var equal=name.length == nome.length; 205 | 206 | this.setState({ 207 | wispDest : (' ' + name).slice(1), 208 | selectedIndex : 2 209 | }); 210 | } 211 | } 212 | 213 | _onSubmitWisp(event) { 214 | event.preventDefault(); 215 | if (this.state.wispText) { 216 | this.sendWisp(this.state.wispText); 217 | this.setState({ wispText: '' }); 218 | } 219 | } 220 | 221 | /* 222 | LOGS 223 | */ 224 | 225 | _onMessageLogs() { 226 | this.setState({ logsMessages: session.chat.logsMessages }); 227 | } 228 | 229 | 230 | _getTime(local) { 231 | return local.getHours() + ":" + local.getMinutes() + ":" + local.getSeconds(); 232 | } 233 | 234 | render() { 235 | return ( 236 | 237 | 241 | 242 | World 243 | Guild 244 | Wisp 245 | Say in area 246 | Logs 247 | 248 | 249 |
    250 |
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 251 | { this.state.worldMessages.map((message, index) => { 252 | const className = classes('message', message.kind); 253 | return ( 254 |
  • 255 | [{this._getTime(message.timestamp)}] 256 | [World] 257 | {this.state.playerNames[message.guid1] && this.state.playerNames[message.guid1].isGm ? "[GM]" : ""} 258 | [{this.state.playerNames[message.guid1] ? this.state.playerNames[message.guid1].name : message.guid1}] 259 | : { message.text } 260 |
  • 261 | ); 262 | }) } 263 |
264 | 265 |
266 | 268 |
269 |
270 | 271 |
    272 |
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 273 | { this.state.guildMessages.map((message, index) => { 274 | const className = classes('message', message.kind); 275 | return ( 276 |
  • 277 | [{this._getTime(message.timestamp)}] 278 | [Guild] 279 | {this.state.playerNames[message.guid1] && this.state.playerNames[message.guid1].isGm ? "[GM]" : ""} 280 | [{this.state.playerNames[message.guid1] ? this.state.playerNames[message.guid1].name : message.guid1}] 281 | : { message.text } 282 |
  • 283 | ); 284 | }) } 285 |
286 | 287 |
288 | 290 |
291 |
292 | 293 |
    294 |
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 295 | { this.state.wispMessages.map((message, index) => { 296 | const className = classes('message', message.kind); 297 | return ( 298 |
  • 299 | [{this._getTime(message.timestamp)}] 300 | {this.state.playerNames[message.guid1] && this.state.playerNames[message.guid1].isGm ? "[GM]" : ""} 301 | {message.kind === "whisper incoming" ? "" : "To "} 302 | [{this.state.playerNames[message.guid1] ? this.state.playerNames[message.guid1].name : message.guid1}] 303 | {message.kind === "whisper incoming" ? "whispers" : ""}: { message.text } 304 |
  • 305 | ); 306 | }) } 307 |
308 | 309 |
310 |
311 |
312 | 313 | 314 | 315 |
316 |
317 | 319 |
320 |
321 | 322 |
323 |
324 |
325 |
326 | 327 |
    328 |
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 329 | { this.state.sayMessages.map((message, index) => { 330 | const className = classes('message', message.kind); 331 | return ( 332 |
  • 333 | [{this._getTime(message.timestamp)}] 334 | {this.state.playerNames[message.guid1] && this.state.playerNames[message.guid1].isGm ? "[GM]" : ""} 335 | [{this.state.playerNames[message.guid1] ? this.state.playerNames[message.guid1].name : message.guid1}] 336 | Says: { message.text } 337 |
  • 338 | ); 339 | }) } 340 |
341 | 342 |
343 | 345 |
346 |
347 | 348 |
    349 |
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 350 | { this.state.logsMessages.map((message, index) => { 351 | const className = classes('message', message.kind); 352 | return ( 353 |
  • 354 | [{this._getTime(message.timestamp)}] 355 | [Logs] 356 | {this.state.playerNames[message.guid1] && this.state.playerNames[message.guid1].isGm ? "[GM]" : ""} 357 | [{this.state.playerNames[message.guid1] ? this.state.playerNames[message.guid1].name : message.guid1}] 358 | : { message.text } 359 |
  • 360 | ); 361 | }) } 362 |
363 |
364 |
365 |
366 | ); 367 | } 368 | 369 | } 370 | 371 | export default ChatPanel; 372 | -------------------------------------------------------------------------------- /src/components/game/chat/index.styl: -------------------------------------------------------------------------------- 1 | wowser .chat 2 | position: absolute 3 | bottom: 0 4 | left: 0 5 | max-width: 800px 6 | min-width: 230px 7 | max-height: 100% 8 | min-height: 320px 9 | z-index: 9000 10 | 11 | .chat-box 12 | height: 245px 13 | 14 | ul 15 | max-width: 900px 16 | padding: 0 17 | margin: .4em 18 | list-style: none 19 | overflow: auto 20 | 21 | .message 22 | font-size: 14px 23 | margin-bottom: 5px; 24 | 25 | &.system 26 | color: #FFCC00 27 | 28 | &.me 29 | color: #D2691E 30 | 31 | &.me 32 | color: #D2691E 33 | 34 | &.yell 35 | color: #B22222 36 | 37 | &.info 38 | color: #26C9FF 39 | 40 | &.error 41 | color: #FF0000 42 | 43 | &.channel 44 | color: #FFB872 45 | 46 | &.whisper 47 | color: #FF72FF 48 | 49 | &.guild 50 | color: #2CB200 51 | 52 | form 53 | margin: 2px 5px 54 | 55 | input 56 | width: 100% 57 | 58 | .wisp-form 59 | width: 100% 60 | display: table 61 | 62 | .wisp-to 63 | display: table-cell 64 | width:80px; 65 | 66 | .wisp-input 67 | display: table-cell 68 | 69 | .ReactTabs__TabList 70 | height: auto; 71 | overflow: hidden; 72 | -------------------------------------------------------------------------------- /src/components/game/controls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import THREE from 'three'; 3 | import key from 'keymaster'; 4 | 5 | class Controls extends React.Component { 6 | 7 | static propTypes = { 8 | camera: React.PropTypes.object.isRequired, 9 | for: React.PropTypes.object.isRequired 10 | }; 11 | 12 | constructor(props) { 13 | super(); 14 | 15 | this.element = document.body; 16 | this.unit = props.for; 17 | this.camera = props.camera; 18 | 19 | // Based on THREE's OrbitControls 20 | // See: http://threejs.org/examples/js/controls/OrbitControls.js 21 | this.clock = new THREE.Clock(); 22 | 23 | this.rotateStart = new THREE.Vector2(); 24 | this.rotateEnd = new THREE.Vector2(); 25 | this.rotateDelta = new THREE.Vector2(); 26 | 27 | this.rotating = false; 28 | this.rotateSpeed = 1.0; 29 | 30 | this.offset = new THREE.Vector3(-10, 0, 10); 31 | this.target = new THREE.Vector3(); 32 | 33 | this.phi = this.phiDelta = 0; 34 | this.theta = this.thetaDelta = 0; 35 | 36 | this.scale = 1; 37 | this.zoomSpeed = 1.0; 38 | this.zoomScale = Math.pow(0.95, this.zoomSpeed); 39 | 40 | // Zoom distance limits 41 | this.minDistance = 6; 42 | this.maxDistance = 500; 43 | 44 | // Vertical orbit limits 45 | this.minPhi = 0; 46 | this.maxPhi = Math.PI * 0.45; 47 | 48 | this.quat = new THREE.Quaternion().setFromUnitVectors( 49 | this.camera.up, new THREE.Vector3(0, 1, 0) 50 | ); 51 | this.quatInverse = this.quat.clone().inverse(); 52 | 53 | this.EPS = 0.000001; 54 | 55 | this._onMouseDown = ::this._onMouseDown; 56 | this._onMouseUp = ::this._onMouseUp; 57 | this._onMouseMove = ::this._onMouseMove; 58 | this._onMouseWheel = ::this._onMouseWheel; 59 | 60 | this.element.addEventListener('mousedown', this._onMouseDown); 61 | this.element.addEventListener('mouseup', this._onMouseUp); 62 | this.element.addEventListener('mousemove', this._onMouseMove); 63 | this.element.addEventListener('mousewheel', this._onMouseWheel); 64 | 65 | // Firefox scroll-wheel support 66 | this.element.addEventListener('DOMMouseScroll', this._onMouseWheel); 67 | 68 | this.update(); 69 | } 70 | 71 | componentWillUnmount() { 72 | this.element.removeEventListener('mousedown', this._onMouseDown); 73 | this.element.removeEventListener('mouseup', this._onMouseUp); 74 | this.element.removeEventListener('mousemove', this._onMouseMove); 75 | this.element.removeEventListener('mousewheel', this._onMouseWheel); 76 | this.element.removeEventListener('DOMMouseScroll', this._onMouseWheel); 77 | } 78 | 79 | update() { 80 | const unit = this.unit; 81 | 82 | // TODO: Get rid of this delta retrieval call 83 | const delta = this.clock.getDelta(); 84 | 85 | if (this.unit) { 86 | if (key.isPressed('up') || key.isPressed('w')) { 87 | unit.moveForward(delta); 88 | } 89 | 90 | if (key.isPressed('down') || key.isPressed('s')) { 91 | unit.moveBackward(delta); 92 | } 93 | 94 | if (key.isPressed('q')) { 95 | unit.strafeLeft(delta); 96 | } 97 | 98 | if (key.isPressed('e')) { 99 | unit.strafeRight(delta); 100 | } 101 | 102 | if (key.isPressed('space')) { 103 | unit.ascend(delta); 104 | } 105 | 106 | if (key.isPressed('x')) { 107 | unit.descend(delta); 108 | } 109 | 110 | if (key.isPressed('left') || key.isPressed('a')) { 111 | unit.rotateLeft(delta); 112 | } 113 | 114 | if (key.isPressed('right') || key.isPressed('d')) { 115 | unit.rotateRight(delta); 116 | } 117 | 118 | this.target = this.unit.position; 119 | } 120 | 121 | const position = this.camera.position; 122 | 123 | // Rotate offset to "y-axis-is-up" space 124 | this.offset.applyQuaternion(this.quat); 125 | 126 | // Angle from z-axis around y-axis 127 | let theta = Math.atan2(this.offset.x, this.offset.z); 128 | 129 | // Angle from y-axis 130 | let phi = Math.atan2( 131 | Math.sqrt(this.offset.x * this.offset.x + this.offset.z * this.offset.z), 132 | this.offset.y 133 | ); 134 | 135 | theta += this.thetaDelta; 136 | phi += this.phiDelta; 137 | 138 | // Limit vertical orbit 139 | phi = Math.max(this.minPhi, Math.min(this.maxPhi, phi)); 140 | phi = Math.max(this.EPS, Math.min(Math.PI - this.EPS, phi)); 141 | 142 | let radius = this.offset.length() * this.scale; 143 | 144 | // Limit zoom distance 145 | radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius)); 146 | 147 | this.offset.x = radius * Math.sin(phi) * Math.sin(theta); 148 | this.offset.y = radius * Math.cos(phi); 149 | this.offset.z = radius * Math.sin(phi) * Math.cos(theta); 150 | 151 | // Rotate offset back to 'camera-up-vector-is-up' space 152 | this.offset.applyQuaternion(this.quatInverse); 153 | 154 | position.copy(this.target).add(this.offset); 155 | 156 | this.camera.lookAt(this.target); 157 | 158 | this.thetaDelta = 0; 159 | this.phiDelta = 0; 160 | this.scale = 1; 161 | } 162 | 163 | rotateHorizontally(angle) { 164 | this.thetaDelta -= angle; 165 | } 166 | 167 | rotateVertically(angle) { 168 | this.phiDelta -= angle; 169 | } 170 | 171 | zoomOut() { 172 | this.scale /= this.zoomScale; 173 | } 174 | 175 | zoomIn() { 176 | this.scale *= this.zoomScale; 177 | } 178 | 179 | _onMouseDown(event) { 180 | this.rotating = true; 181 | this.rotateStart.set(event.clientX, event.clientY); 182 | } 183 | 184 | _onMouseUp() { 185 | this.rotating = false; 186 | } 187 | 188 | _onMouseMove(event) { 189 | if (this.rotating) { 190 | event.preventDefault(); 191 | 192 | this.rotateEnd.set(event.clientX, event.clientY); 193 | this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); 194 | 195 | this.rotateHorizontally( 196 | 2 * Math.PI * this.rotateDelta.x / this.element.clientWidth * this.rotateSpeed 197 | ); 198 | 199 | this.rotateVertically( 200 | 2 * Math.PI * this.rotateDelta.y / this.element.clientHeight * this.rotateSpeed 201 | ); 202 | 203 | this.rotateStart.copy(this.rotateEnd); 204 | 205 | this.update(); 206 | } 207 | } 208 | 209 | _onMouseWheel(event) { 210 | event.preventDefault(); 211 | event.stopPropagation(); 212 | 213 | const delta = event.wheelDelta || -event.detail; 214 | if (delta > 0) { 215 | this.zoomIn(); 216 | } else if (delta < 0) { 217 | this.zoomOut(); 218 | } 219 | 220 | this.update(); 221 | } 222 | 223 | render() { 224 | return null; 225 | } 226 | 227 | } 228 | 229 | export default Controls; 230 | -------------------------------------------------------------------------------- /src/components/game/hud/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.styl'; 4 | 5 | import Chat from '../chat'; 6 | import Portrait from '../portrait'; 7 | // TODO: import Quests from '../quests'; 8 | import session from '../../wowser/session'; 9 | 10 | class HUD extends React.Component { 11 | 12 | render() { 13 | const player = session.player; 14 | return ( 15 | 16 | 17 | { player.target && } 18 | 19 | 20 | ); 21 | } 22 | 23 | } 24 | 25 | export default HUD; 26 | -------------------------------------------------------------------------------- /src/components/game/hud/index.styl: -------------------------------------------------------------------------------- 1 | wowser .game .hud 2 | z-index: 2 3 | display: flex 4 | align-items: center 5 | justify-content: center 6 | -------------------------------------------------------------------------------- /src/components/game/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import THREE from 'three'; 3 | 4 | import './index.styl'; 5 | 6 | import Controls from './controls'; 7 | import HUD from './hud'; 8 | import Stats from './stats'; 9 | import session from '../wowser/session'; 10 | 11 | class GameScreen extends React.Component { 12 | 13 | static id = 'game'; 14 | static title = 'Game'; 15 | 16 | constructor() { 17 | super(); 18 | 19 | this.animate = ::this.animate; 20 | this.resize = ::this.resize; 21 | 22 | this.camera = new THREE.PerspectiveCamera(60, this.aspectRatio, 1, 1000); 23 | this.camera.up.set(0, 0, 1); 24 | this.camera.position.set(15, 0, 7); 25 | 26 | this.prevCameraRotation = null; 27 | this.prevCameraPosition = null; 28 | 29 | this.renderer = null; 30 | this.requestID = null; 31 | 32 | // For some reason, we can't use the clock from controls here. 33 | this.clock = new THREE.Clock(); 34 | } 35 | 36 | componentDidMount() { 37 | this.renderer = new THREE.WebGLRenderer({ 38 | alpha: true, 39 | canvas: this.refs.canvas 40 | }); 41 | 42 | this.forceUpdate(); 43 | this.resize(); 44 | this.animate(); 45 | 46 | window.addEventListener('resize', this.resize); 47 | } 48 | 49 | componentWillUnmount() { 50 | if (this.renderer) { 51 | this.renderer.dispose(); 52 | this.renderer = null; 53 | } 54 | 55 | if (this.requestID) { 56 | this.requestID = null; 57 | cancelAnimationFrame(this.requestID); 58 | } 59 | 60 | window.removeEventListener('resize', this.resize); 61 | } 62 | 63 | get aspectRatio() { 64 | return window.innerWidth / window.innerHeight; 65 | } 66 | 67 | resize() { 68 | this.renderer.setSize(window.innerWidth, window.innerHeight); 69 | this.camera.aspect = this.aspectRatio; 70 | this.camera.updateProjectionMatrix(); 71 | } 72 | 73 | animate() { 74 | if (!this.renderer) { 75 | return; 76 | } 77 | 78 | this.refs.controls.update(); 79 | this.refs.stats.forceUpdate(); 80 | 81 | const cameraMoved = 82 | this.prevCameraRotation === null || 83 | this.prevCameraPosition === null || 84 | !this.prevCameraRotation.equals(this.camera.quaternion) || 85 | !this.prevCameraPosition.equals(this.camera.position); 86 | 87 | session.world.animate(this.clock.getDelta(), this.camera, cameraMoved); 88 | 89 | this.renderer.render(session.world.scene, this.camera); 90 | this.requestID = requestAnimationFrame(this.animate); 91 | 92 | this.prevCameraRotation = this.camera.quaternion.clone(); 93 | this.prevCameraPosition = this.camera.position.clone(); 94 | } 95 | 96 | render() { 97 | return ( 98 | 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | } 106 | 107 | } 108 | 109 | export default GameScreen; 110 | -------------------------------------------------------------------------------- /src/components/game/index.styl: -------------------------------------------------------------------------------- 1 | wowser .game 2 | 3 | canvas 4 | position: absolute 5 | top: 0 6 | left: 0 7 | z-index: 1 8 | width: 100% 9 | height: 100% 10 | -------------------------------------------------------------------------------- /src/components/game/portrait/images/icon-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/game/portrait/images/icon-portrait.png -------------------------------------------------------------------------------- /src/components/game/portrait/images/portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/game/portrait/images/portrait.png -------------------------------------------------------------------------------- /src/components/game/portrait/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from 'classnames'; 3 | 4 | import './index.styl'; 5 | 6 | class Portrait extends React.Component { 7 | 8 | static propTypes = { 9 | self: React.PropTypes.bool, 10 | unit: React.PropTypes.object.isRequired, 11 | target: React.PropTypes.bool 12 | }; 13 | 14 | render() { 15 | const unit = this.props.unit; 16 | 17 | const className = classes('portrait', { 18 | self: this.props.self, 19 | target: this.props.target 20 | }); 21 | return ( 22 | 23 |
24 | 25 |
{ unit.name }
26 | 27 | 28 |
29 | 30 |
{ unit.hp } / { unit.maxHp }
31 |
{ unit.mp } / { unit.maxMp }
32 |
33 | ); 34 | } 35 | 36 | } 37 | 38 | export default Portrait; 39 | -------------------------------------------------------------------------------- /src/components/game/portrait/index.styl: -------------------------------------------------------------------------------- 1 | wowser .portrait 2 | display: block 3 | position: relative 4 | width: 178px 5 | height: 60px 6 | background: url('./images/portrait.png') no-repeat 7 | 8 | .icon 9 | position: absolute 10 | top: -4px 11 | left: -4px 12 | z-index: 1 13 | 14 | .name 15 | position: absolute 16 | top: 4px 17 | left: 64px 18 | color: #FFCC00 19 | font-size: 15px 20 | 21 | .level 22 | position: absolute 23 | top: 6px 24 | right: 12px 25 | font-size: 11px 26 | 27 | .divider 28 | position: absolute 29 | top: 20px 30 | right: 3px 31 | width: 113px 32 | 33 | .health, .mana 34 | width: 115px 35 | border-radius: 4px 36 | font-size: 12px 37 | line-height: 11px 38 | text-shadow: 1px 1px 0px #000000 39 | text-align: center 40 | 41 | .health 42 | position: absolute 43 | bottom: 23px 44 | right: 6px 45 | background-image: linear-gradient(180deg, #330000 0%, #990000 100%) 46 | box-shadow: -1px -1px 0px #660000, 1px 1px 0px #E50202 47 | 48 | .mana 49 | position: absolute 50 | bottom: 7px 51 | right: 20px 52 | background-image: linear-gradient(180deg, #021D39 0%, #0B4C93 100%) 53 | box-shadow: -1px -1px 0px #063467, 1px 1px 0px #146CD0 54 | 55 | &.self 56 | position: absolute 57 | top: 12px 58 | left: 12px 59 | 60 | &.target 61 | position: absolute 62 | top: 12px 63 | left: 210px 64 | 65 | wowser .icon.portrait 66 | width: 66px 67 | height: 66px 68 | background: url('./images/icon-portrait.png') no-repeat 69 | -------------------------------------------------------------------------------- /src/components/game/quests/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.styl'; 4 | 5 | class QuestsPanel extends React.Component { 6 | 7 | render() { 8 | return ( 9 | 10 |
11 | 12 |

Quest Log

13 | 14 |
15 | 16 |

17 | Soon™ 18 |

19 |
20 | ); 21 | } 22 | 23 | } 24 | 25 | export default QuestsPanel; 26 | -------------------------------------------------------------------------------- /src/components/game/quests/index.styl: -------------------------------------------------------------------------------- 1 | wowser .quests 2 | position: absolute 3 | bottom: 0 4 | right: 0 5 | height: 30% 6 | width: 300px 7 | -------------------------------------------------------------------------------- /src/components/game/stats/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.styl'; 4 | 5 | class Stats extends React.Component { 6 | 7 | static propTypes = { 8 | renderer: React.PropTypes.object, 9 | map: React.PropTypes.object 10 | }; 11 | 12 | mapStats() { 13 | const map = this.props.map; 14 | 15 | return ( 16 |
17 |
18 | 19 |

Map Chunks

20 |
21 |

22 | Loaded: { map ? map.chunks.size : 0 } 23 |

24 | 25 |
26 | ); 27 | } 28 | 29 | render() { 30 | const renderer = this.props.renderer; 31 | if (!renderer) { 32 | return null; 33 | } 34 | 35 | const map = this.props.map; 36 | 37 | const { memory, programs, render } = renderer.info; 38 | return ( 39 | 40 |

Memory

41 |
42 |

43 | Geometries: { memory.geometries } 44 |

45 |

46 | Textures: { memory.textures } 47 |

48 |

49 | Programs: { programs.length } 50 |

51 | 52 |
53 | 54 |

Render

55 |
56 |

57 | Calls: { render.calls } 58 |

59 |

60 | Faces: { render.faces } 61 |

62 |

63 | Points: { render.points } 64 |

65 |

66 | Vertices: { render.vertices } 67 |

68 | 69 | { map && this.mapStats() } 70 |
71 | ); 72 | } 73 | 74 | } 75 | 76 | export default Stats; 77 | -------------------------------------------------------------------------------- /src/components/game/stats/index.styl: -------------------------------------------------------------------------------- 1 | wowser .stats 2 | position: absolute 3 | bottom: 0 4 | right: 0 5 | z-index: 3 6 | width: 160px 7 | -------------------------------------------------------------------------------- /src/components/kit/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class KitScreen extends React.Component { 4 | 5 | static id = 'kit'; 6 | static title = 'UI Kit'; 7 | 8 | render() { 9 | return ( 10 | 11 |
12 |

Thin frame

13 |
14 |

15 | Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. 16 |

17 |
18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |

Thick frame

26 |
27 |

28 | Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. 29 |

30 |
31 | 32 |
33 |
34 |

Regular panel

35 |
36 |

37 | Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. 38 |

39 |
40 | 41 |
42 |
43 |

Headless panel

44 |
45 |

46 | Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. 47 |

48 |
49 |
50 | ); 51 | } 52 | 53 | } 54 | 55 | export default KitScreen; 56 | -------------------------------------------------------------------------------- /src/components/realms/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import session from '../wowser/session'; 4 | 5 | class RealmsScreen extends React.Component { 6 | 7 | static id = 'realms'; 8 | static title = 'Realms Selection'; 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | realm: null, 15 | realms: [] 16 | }; 17 | 18 | this._onAuthenticate = ::this._onAuthenticate; 19 | this._onRealmSelect = ::this._onRealmSelect; 20 | this._onRefresh = ::this._onRefresh; 21 | this._onSubmit = ::this._onSubmit; 22 | 23 | session.realms.on('refresh', this._onRefresh); 24 | session.game.on('authenticate', this._onAuthenticate); 25 | 26 | this.refresh(); 27 | } 28 | 29 | componentWillUnmount() { 30 | session.realms.removeListener('refresh', this._onRefresh); 31 | session.game.removeListener('authenticate', this._onAuthenticate); 32 | } 33 | 34 | connect(realm) { 35 | session.game.connect(session.auth.host, realm); 36 | } 37 | 38 | refresh() { 39 | session.realms.refresh(); 40 | } 41 | 42 | _onAuthenticate() { 43 | session.screen = 'characters'; 44 | } 45 | 46 | _onRealmSelect(event) { 47 | const realms = session.realms.list; 48 | this.setState({ 49 | realm: realms[event.target.selectedIndex] 50 | }); 51 | } 52 | 53 | _onRefresh() { 54 | const realms = session.realms.list; 55 | this.setState({ 56 | realm: realms[0], 57 | realms: realms 58 | }); 59 | } 60 | 61 | _onSubmit(event) { 62 | event.preventDefault(); 63 | this.connect(this.state.realm); 64 | } 65 | 66 | render() { 67 | return ( 68 | 69 |
70 |

Realm Selection

71 | 72 |
73 | 74 |
75 |
76 | 86 |
87 | 88 |
89 | 90 | 91 | 92 |
93 |
94 |
95 | ); 96 | } 97 | 98 | } 99 | 100 | export default RealmsScreen; 101 | -------------------------------------------------------------------------------- /src/components/wowser/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/images/logo.png -------------------------------------------------------------------------------- /src/components/wowser/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.styl'; 4 | 5 | import AuthScreen from '../auth'; 6 | import CharactersScreen from '../characters'; 7 | import GameScreen from '../game'; 8 | import RealmsScreen from '../realms'; 9 | import Kit from '../kit'; 10 | import session from './session'; 11 | 12 | class Wowser extends React.Component { 13 | 14 | static SCREENS = [ 15 | AuthScreen, 16 | RealmsScreen, 17 | CharactersScreen, 18 | GameScreen, 19 | Kit 20 | ]; 21 | 22 | constructor() { 23 | super(); 24 | 25 | this.state = { 26 | screen: session.screen 27 | }; 28 | 29 | this._onScreenChange = ::this._onScreenChange; 30 | this._onScreenSelect = ::this._onScreenSelect; 31 | 32 | session.on('screen:change', this._onScreenChange); 33 | } 34 | 35 | get currentScreen() { 36 | const Screen = this.constructor.SCREENS.find((screen) => { 37 | return screen.id === this.state.screen; 38 | }); 39 | return ; 40 | } 41 | 42 | _onScreenChange(_from, to) { 43 | this.setState({ screen: to }); 44 | } 45 | 46 | _onScreenSelect(event) { 47 | session.screen = event.target.value; 48 | } 49 | 50 | render() { 51 | const screens = this.constructor.SCREENS; 52 | return ( 53 | 54 |
55 |
Wowser
56 |
57 |
{ session.config.slogan }
58 |
59 | 60 | 71 | 72 | { this.currentScreen } 73 |
74 | ); 75 | } 76 | 77 | } 78 | 79 | export default Wowser; 80 | -------------------------------------------------------------------------------- /src/components/wowser/index.styl: -------------------------------------------------------------------------------- 1 | @import '~normalize.css' 2 | 3 | @import './ui'; 4 | 5 | html, body 6 | width: 100% 7 | height: 100% 8 | overflow: hidden 9 | 10 | * 11 | box-sizing: border-box 12 | 13 | wowser 14 | display: flex 15 | align-items: center 16 | justify-content: center 17 | width: 100% 18 | height: 100% 19 | background: #222222 20 | background-position: center; 21 | background-size: cover; 22 | font-family: Galdeano 23 | font-size: 13px 24 | color: #FFFFFF 25 | -webkit-font-smoothing: antialiased 26 | 27 | &:active 28 | cursor: none 29 | 30 | .branding 31 | position: absolute 32 | top: 10px 33 | right: 10px 34 | z-index: 1 35 | 36 | header 37 | width: 204px 38 | height: 47px 39 | background: url('./images/logo.png') no-repeat 40 | text-indent: -99999px 41 | 42 | .divider 43 | margin: 5px 0 44 | 45 | .slogan 46 | color: #FFCC00 47 | letter-spacing: .075em 48 | 49 | select.screen-selector 50 | position: absolute 51 | top: 100px 52 | right: 10px 53 | z-index: 4 54 | color: #000000 55 | -------------------------------------------------------------------------------- /src/components/wowser/session.jsx: -------------------------------------------------------------------------------- 1 | import Client from '../../lib'; 2 | 3 | class Session extends Client { 4 | 5 | constructor() { 6 | super(); 7 | 8 | this._screen = 'auth'; 9 | } 10 | 11 | get screen() { 12 | return this._screen; 13 | } 14 | 15 | set screen(screen) { 16 | if (this._screen !== screen) { 17 | this.emit('screen:change', this._screen, screen); 18 | this._screen = screen; 19 | } 20 | } 21 | 22 | } 23 | 24 | export default new Session(); 25 | -------------------------------------------------------------------------------- /src/components/wowser/ui/form/index.styl: -------------------------------------------------------------------------------- 1 | wowser form 2 | 3 | fieldset 4 | display: block 5 | border: 0 6 | padding: 0 7 | margin: .5em 8 | border-top: 1px solid transparent 9 | 10 | label 11 | display: block 12 | color: #999999 13 | font-size: 14px 14 | margin: .7em 0 .1em 15 | 16 | input, select, textarea 17 | padding: .2em .3em 18 | background-color: #111111 19 | border-width: 1px 20 | border-style: solid 21 | border-color: #333333 #666666 #666666 #333333 22 | border-radius: 6px 23 | color: #FFFFFF 24 | font-size: 14px 25 | -webkit-font-smoothing: antialiased 26 | outline: none 27 | margin-bottom: .3em 28 | -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/dividers/images/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/dividers/images/horizontal.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/dividers/images/thick-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/dividers/images/thick-horizontal.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/dividers/index.styl: -------------------------------------------------------------------------------- 1 | wowser .divider 2 | border-style: solid 3 | 4 | &, &.horizontal 5 | border-width: 3px 5px 0 5px 6 | border-image: url('./images/horizontal.png') 3 5 0 5 repeat 7 | 8 | &.thick 9 | border-width: 11px 5px 0 5px 10 | border-image: url('./images/thick-horizontal.png') 11 5 0 5 repeat 11 | -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/images/panel-headless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/images/panel-headless.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/images/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/images/panel.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/images/thick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/images/thick.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/images/thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azerothcore/acore-client/661d5a8594320e3a612fb1aa7282529caf45379b/src/components/wowser/ui/frame/images/thin.png -------------------------------------------------------------------------------- /src/components/wowser/ui/frame/index.styl: -------------------------------------------------------------------------------- 1 | @import './dividers'; 2 | 3 | wowser 4 | 5 | .frame, .panel 6 | display: block 7 | position: relative 8 | margin: 10px 9 | 10 | &:before 11 | content: ' ' 12 | display: block 13 | position: absolute 14 | top: -3px 15 | bottom: -3px 16 | left: -3px 17 | right: -3px 18 | z-index: -1 19 | background: rgba(0, 0, 0, .8) 20 | 21 | .frame 22 | border-width: 5px 23 | border-image: url('./images/thin.png') 5 5 5 5 repeat 24 | border-style: solid 25 | 26 | .divider 27 | 28 | &, &.horizontal 29 | margin-left: -3px 30 | margin-right: -3px 31 | 32 | &.thick 33 | border-width: 11px 34 | border-image: url('./images/thick.png') 11 11 11 11 repeat 35 | 36 | .divider 37 | 38 | &, &.horizontal 39 | margin-left: -5px 40 | margin-right: -5px 41 | 42 | .panel 43 | border-width: 23px 44 | border-image: url('./images/panel.png') 23 23 23 23 repeat 45 | border-style: solid 46 | 47 | .icon.portrait 48 | position: absolute 49 | top: -30px 50 | left: -46px 51 | 52 | & + h1, & + h2, & + h3 53 | margin-left: 1.5em 54 | 55 | .divider 56 | 57 | &, &.horizontal 58 | margin-left: -6px 59 | margin-right: -6px 60 | 61 | &.headless 62 | border-width: 11px 23px 23px 23px 63 | border-image: url('./images/panel-headless.png') 11 23 23 23 repeat 64 | 65 | .icon.portrait 66 | top: -20px 67 | -------------------------------------------------------------------------------- /src/components/wowser/ui/index.styl: -------------------------------------------------------------------------------- 1 | @import './form'; 2 | @import './frame'; 3 | @import './screen'; 4 | @import './type'; 5 | @import './widgets'; 6 | -------------------------------------------------------------------------------- /src/components/wowser/ui/screen.styl: -------------------------------------------------------------------------------- 1 | wowser .screen 2 | position: absolute 3 | top: 0 4 | left: 0 5 | z-index: 3 6 | width: 100% 7 | height: 100% 8 | display: flex 9 | align-items: center 10 | justify-content: center 11 | -------------------------------------------------------------------------------- /src/components/wowser/ui/type.styl: -------------------------------------------------------------------------------- 1 | wowser 2 | 3 | h1, h2, h3, h4 4 | margin: .3em 5 | color: #FFCC00 6 | font-weight: normal 7 | 8 | h1 9 | font-size: 17px 10 | 11 | h2 12 | font-size: 15px 13 | 14 | p 15 | margin: .5em 16 | -------------------------------------------------------------------------------- /src/components/wowser/ui/widgets/button.styl: -------------------------------------------------------------------------------- 1 | wowser 2 | 3 | input[type='submit'], input[type='button'], button 4 | margin: .4em 0 .3em .5em 5 | padding: .2em 1em 6 | border: none 7 | border-radius: 4px 8 | background-image: linear-gradient(180deg, #990000 0%, #660000 60%, #660000 100%) 9 | border-width: 1px 10 | border-style: solid 11 | border-color: #E50202 #990000 #770000 #E50202 12 | color: #FFFFFF 13 | text-shadow: 1px 1px 0px #330000 14 | font-size: 13px 15 | -webkit-font-smoothing: antialiased 16 | outline: none 17 | 18 | &:enabled:active 19 | background-image: linear-gradient(180deg, #660000 0%, #660000 26%, #990000 100%) 20 | border-color: #330000 21 | transform: scale(.97) 22 | text-shadow: none 23 | box-shadow: -1px -1px 1px rgba(#000000, .2), 1px 1px 1px rgba(#000000, .2) 24 | 25 | &:enabled:hover 26 | color: #FFCC00 27 | 28 | &:disabled 29 | background-image: linear-gradient(180deg, #4A0000 0%, #2A0000 100%) 30 | border-color: #8A0000 #400000 #400000 #8A0000 31 | color: #990000 32 | -------------------------------------------------------------------------------- /src/components/wowser/ui/widgets/index.styl: -------------------------------------------------------------------------------- 1 | @import './button'; 2 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wowser 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/auth/challenge-opcode.js: -------------------------------------------------------------------------------- 1 | class ChallengeOpcode { 2 | 3 | static SUCCESS = 0x00; 4 | static UNKNOWN0 = 0x01; 5 | static UNKNOWN1 = 0x02; 6 | static ACCOUNT_BANNED = 0x03; 7 | static ACCOUNT_INVALID = 0x04; 8 | static PASSWORD_INVALID = 0x05; 9 | static ALREADY_ONLINE = 0x06; 10 | static OUT_OF_CREDIT = 0x07; 11 | static BUSY = 0x08; 12 | static BUILD_INVALID = 0x09; 13 | static BUILD_UPDATE = 0x0A; 14 | static INVALID_SERVER = 0x0B; 15 | static ACCOUNT_SUSPENDED = 0x0C; 16 | static ACCESS_DENIED = 0x0D; 17 | static SURVEY = 0x0E; 18 | static PARENTAL_CONTROL = 0x0F; 19 | static LOCK_ENFORCED = 0x10; 20 | static TRIAL_EXPIRED = 0x11; 21 | static BATTLE_NET = 0x12; 22 | 23 | } 24 | 25 | export default ChallengeOpcode; 26 | -------------------------------------------------------------------------------- /src/lib/auth/handler.js: -------------------------------------------------------------------------------- 1 | import AuthChallengeOpcode from './challenge-opcode'; 2 | import AuthOpcode from './opcode'; 3 | import AuthPacket from './packet'; 4 | import Socket from '../net/socket'; 5 | import SRP from '../crypto/srp'; 6 | 7 | class AuthHandler extends Socket { 8 | 9 | // Creates a new authentication handler 10 | constructor(session) { 11 | super(); 12 | 13 | // Holds session 14 | this.session = session; 15 | 16 | this.defport = this.session.config.authport; 17 | this.defhost = this.session.config.serverhost; 18 | 19 | // Holds credentials for this session (if any) 20 | this.account = null; 21 | this.password = null; 22 | 23 | // Holds Secure Remote Password implementation 24 | this.srp = null; 25 | 26 | // Listen for incoming data 27 | this.on('data:receive', this.dataReceived); 28 | 29 | // Delegate packets 30 | this.on('packet:receive:LOGON_CHALLENGE', this.handleLogonChallenge); 31 | this.on('packet:receive:LOGON_PROOF', this.handleLogonProof); 32 | } 33 | 34 | // Retrieves the session key (if any) 35 | get key() { 36 | return this.srp && this.srp.K; 37 | } 38 | 39 | // Connects to given host through given port 40 | connect(host, port = NaN) { 41 | if (!this.connected) { 42 | super.connect(host || this.defhost, port || this.defport); 43 | console.info('connecting to auth-server @', this.host, ':', this.port); 44 | } 45 | return this; 46 | } 47 | 48 | // Sends authentication request to connected host 49 | authenticate(account, password) { 50 | if (!this.connected) { 51 | return false; 52 | } 53 | 54 | this.account = account.toUpperCase(); 55 | this.password = password.toUpperCase(); 56 | 57 | console.info('authenticating', this.account); 58 | 59 | // Extract configuration data 60 | const { 61 | build, 62 | majorVersion, 63 | minorVersion, 64 | patchVersion, 65 | game, 66 | raw: { 67 | os, locale, platform 68 | }, 69 | timezone 70 | } = this.session.config; 71 | 72 | const ap = new AuthPacket(AuthOpcode.LOGON_CHALLENGE, 4 + 29 + 1 + this.account.length); 73 | ap.writeByte(0x00); 74 | ap.writeShort(30 + this.account.length); 75 | 76 | ap.writeString(game); // game string 77 | ap.writeByte(majorVersion); // v1 (major) 78 | ap.writeByte(minorVersion); // v2 (minor) 79 | ap.writeByte(patchVersion); // v3 (patch) 80 | ap.writeShort(build); // build 81 | ap.writeString(platform); // platform 82 | ap.writeString(os); // os 83 | ap.writeString(locale); // locale 84 | ap.writeUnsignedInt(timezone); // timezone 85 | ap.writeUnsignedInt(0); // ip 86 | ap.writeByte(this.account.length); // account length 87 | ap.writeString(this.account); // account 88 | 89 | this.send(ap); 90 | } 91 | 92 | // Data received handler 93 | dataReceived() { 94 | while (true) { 95 | if (!this.connected || this.buffer.available < AuthPacket.HEADER_SIZE) { 96 | return; 97 | } 98 | 99 | const ap = new AuthPacket(this.buffer.readByte(), this.buffer.seek(-AuthPacket.HEADER_SIZE).read(), false); 100 | 101 | console.log('⟹', ap.toString()); 102 | // console.debug ap.toHex() 103 | // console.debug ap.toASCII() 104 | 105 | this.emit('packet:receive', ap); 106 | if (ap.opcodeName) { 107 | this.emit(`packet:receive:${ap.opcodeName}`, ap); 108 | } 109 | } 110 | } 111 | 112 | // Logon challenge handler (LOGON_CHALLENGE) 113 | handleLogonChallenge(ap) { 114 | ap.readUnsignedByte(); 115 | const status = ap.readUnsignedByte(); 116 | 117 | switch (status) { 118 | case AuthChallengeOpcode.SUCCESS: 119 | console.info('received logon challenge'); 120 | 121 | const B = ap.read(32); // B 122 | 123 | const glen = ap.readUnsignedByte(); // g-length 124 | const g = ap.read(glen); // g 125 | 126 | const Nlen = ap.readUnsignedByte(); // n-length 127 | const N = ap.read(Nlen); // N 128 | 129 | const salt = ap.read(32); // salt 130 | 131 | ap.read(16); // unknown 132 | ap.readUnsignedByte(); // security flags 133 | 134 | this.srp = new SRP(N, g); 135 | this.srp.feed(salt, B, this.account, this.password); 136 | 137 | const lpp = new AuthPacket(AuthOpcode.LOGON_PROOF, 1 + 32 + 20 + 20 + 2); 138 | lpp.write(this.srp.A.toArray()); 139 | lpp.write(this.srp.M1.digest); 140 | lpp.write(new Array(20)); // CRC hash 141 | lpp.writeByte(0x00); // number of keys 142 | lpp.writeByte(0x00); // security flags 143 | 144 | this.send(lpp); 145 | break; 146 | case AuthChallengeOpcode.ACCOUNT_INVALID: 147 | console.warn('account invalid'); 148 | alert("Invalid Account!"); 149 | this.emit('reject'); 150 | break; 151 | case AuthChallengeOpcode.BUILD_INVALID: 152 | console.warn('build invalid'); 153 | this.emit('reject'); 154 | break; 155 | default: 156 | break; 157 | } 158 | } 159 | 160 | // Logon proof handler (LOGON_PROOF) 161 | handleLogonProof(ap) { 162 | ap.readByte(); 163 | 164 | console.info('received proof response'); 165 | 166 | var M2; 167 | 168 | try { 169 | M2 = ap.read(20); 170 | } catch (e) { 171 | // reject 172 | } 173 | 174 | if (M2 && this.srp.validate(M2)) { 175 | this.emit('authenticate'); 176 | } else { 177 | alert("Invalid account!"); 178 | this.emit('reject'); 179 | } 180 | } 181 | 182 | } 183 | 184 | export default AuthHandler; 185 | -------------------------------------------------------------------------------- /src/lib/auth/opcode.js: -------------------------------------------------------------------------------- 1 | class Opcode { 2 | 3 | static LOGON_CHALLENGE = 0x00; 4 | static LOGON_PROOF = 0x01; 5 | static RECONNECT_CHALLENGE = 0x02; 6 | static RECONNECT_PROOF = 0x03; 7 | static REALM_LIST = 0x10; 8 | 9 | } 10 | 11 | export default Opcode; 12 | -------------------------------------------------------------------------------- /src/lib/auth/packet.js: -------------------------------------------------------------------------------- 1 | import AuthOpcode from './opcode'; 2 | import BasePacket from '../net/packet'; 3 | import ObjectUtil from '../utils/object-util'; 4 | 5 | class AuthPacket extends BasePacket { 6 | 7 | // Header size in bytes for both incoming and outgoing packets 8 | static HEADER_SIZE = 1; 9 | 10 | constructor(opcode, source, outgoing = true) { 11 | super(opcode, source || AuthPacket.HEADER_SIZE, outgoing); 12 | } 13 | 14 | // Retrieves the name of the opcode for this packet (if available) 15 | get opcodeName() { 16 | return ObjectUtil.keyByValue(AuthOpcode, this.opcode); 17 | } 18 | 19 | // Finalizes this packet 20 | finalize() { 21 | this.index = 0; 22 | this.writeByte(this.opcode); 23 | } 24 | 25 | } 26 | 27 | export default AuthPacket; 28 | -------------------------------------------------------------------------------- /src/lib/characters/character.js: -------------------------------------------------------------------------------- 1 | class Character { 2 | 3 | // Short string representation of this character 4 | toString() { 5 | return `[Character; GUID: ${this.guid}]`; 6 | } 7 | 8 | } 9 | 10 | export default Character; 11 | -------------------------------------------------------------------------------- /src/lib/characters/handler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import Character from './character'; 4 | import GamePacket from '../game/packet'; 5 | import GameOpcode from '../game/opcode'; 6 | 7 | class CharacterHandler extends EventEmitter { 8 | 9 | // Creates a new character handler 10 | constructor(session) { 11 | super(); 12 | 13 | // Holds session 14 | this.session = session; 15 | 16 | // Initially empty list of characters 17 | this.list = []; 18 | 19 | // Listen for character list 20 | this.session.game.on('packet:receive:SMSG_CHAR_ENUM', ::this.handleCharacterList); 21 | } 22 | 23 | // Requests a fresh list of characters 24 | refresh() { 25 | console.info('refreshing character list'); 26 | 27 | const gp = new GamePacket(GameOpcode.CMSG_CHAR_ENUM); 28 | 29 | return this.session.game.send(gp); 30 | } 31 | 32 | // Character list refresh handler (SMSG_CHAR_ENUM) 33 | handleCharacterList(gp) { 34 | const count = gp.readByte(); // number of characters 35 | 36 | this.list.length = 0; 37 | 38 | for (let i = 0; i < count; ++i) { 39 | const character = new Character(); 40 | 41 | character.guid = gp.readGUID(); 42 | character.name = gp.readCString(); 43 | character.race = gp.readUnsignedByte(); 44 | character.class = gp.readUnsignedByte(); 45 | character.gender = gp.readUnsignedByte(); 46 | character.bytes = gp.readUnsignedInt(); 47 | character.facial = gp.readUnsignedByte(); 48 | character.level = gp.readUnsignedByte(); 49 | character.zone = gp.readUnsignedInt(); 50 | character.map = gp.readUnsignedInt(); 51 | character.x = gp.readFloat(); 52 | character.y = gp.readFloat(); 53 | character.z = gp.readFloat(); 54 | character.guild = gp.readUnsignedInt(); 55 | character.flags = gp.readUnsignedInt(); 56 | 57 | gp.readUnsignedInt(); // character customization 58 | gp.readUnsignedByte(); // (?) 59 | 60 | const pet = { 61 | model: gp.readUnsignedInt(), 62 | level: gp.readUnsignedInt(), 63 | family: gp.readUnsignedInt() 64 | }; 65 | if (pet.model) { 66 | character.pet = pet; 67 | } 68 | 69 | character.equipment = []; 70 | for (let j = 0; j < 23; ++j) { 71 | const item = { 72 | model: gp.readUnsignedInt(), 73 | type: gp.readUnsignedByte(), 74 | enchantment: gp.readUnsignedInt() 75 | }; 76 | character.equipment.push(item); 77 | } 78 | 79 | this.list.push(character); 80 | } 81 | 82 | this.emit('refresh'); 83 | } 84 | 85 | } 86 | 87 | export default CharacterHandler; 88 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | class Raw { 2 | constructor(config) { 3 | this.config = config; 4 | } 5 | 6 | raw(value) { 7 | return (value.split('').reverse().join('')+'\u0000' ).slice(0,4); 8 | } 9 | 10 | get locale() { 11 | return this.raw(this.config.locale); 12 | } 13 | 14 | get os() { 15 | return this.raw(this.config.os); 16 | } 17 | 18 | get platform() { 19 | return this.raw(this.config.platform); 20 | } 21 | 22 | } 23 | 24 | class Config { 25 | 26 | constructor() { 27 | this.game = 'Wow '; 28 | this.build = 12340; 29 | this.version = '3.3.5'; 30 | this.timezone = 0; 31 | 32 | this.locale = 'enUS'; 33 | this.os = 'Win'; 34 | this.platform = 'x86'; 35 | 36 | var CustomDef=require("../../conf/conf.js.dist"); 37 | Object.assign(this,CustomDef); 38 | var Custom=require("../../conf/conf.js"); 39 | Object.assign(this,Custom); 40 | 41 | this.raw = new Raw(this); 42 | } 43 | 44 | set version(version) { 45 | [ 46 | this.majorVersion, 47 | this.minorVersion, 48 | this.patchVersion 49 | ] = version.split('.').map(function(bit) { 50 | return parseInt(bit, 10); 51 | }); 52 | } 53 | 54 | } 55 | 56 | export default Config; 57 | -------------------------------------------------------------------------------- /src/lib/crypto/big-num.js: -------------------------------------------------------------------------------- 1 | import BigInteger from 'jsbn/lib/big-integer'; 2 | 3 | // C-like BigNum decorator for JSBN's BigInteger 4 | class BigNum { 5 | 6 | // Convenience BigInteger.ZERO decorator 7 | static ZERO = new BigNum(BigInteger.ZERO); 8 | 9 | // Creates a new BigNum 10 | constructor(value, radix) { 11 | if (typeof value === 'number') { 12 | this._bi = BigInteger.fromInt(value); 13 | } else if (value.constructor === BigInteger) { 14 | this._bi = value; 15 | } else if (value.constructor === BigNum) { 16 | this._bi = value.bi; 17 | } else { 18 | this._bi = new BigInteger(value, radix); 19 | } 20 | } 21 | 22 | // Short string description of this BigNum 23 | toString() { 24 | return `[BigNum; Value: ${this._bi}; Hex: ${this._bi.toString(16).toUpperCase()}]`; 25 | } 26 | 27 | // Retrieves BigInteger instance being decorated 28 | get bi() { 29 | return this._bi; 30 | } 31 | 32 | // Performs a modulus operation 33 | mod(m) { 34 | return new BigNum(this._bi.mod(m.bi)); 35 | } 36 | 37 | // Performs an exponential+modulus operation 38 | modPow(e, m) { 39 | return new BigNum(this._bi.modPow(e.bi, m.bi)); 40 | } 41 | 42 | // Performs an addition 43 | add(o) { 44 | return new BigNum(this._bi.add(o.bi)); 45 | } 46 | 47 | // Performs a subtraction 48 | subtract(o) { 49 | return new BigNum(this._bi.subtract(o.bi)); 50 | } 51 | 52 | // Performs a multiplication 53 | multiply(o) { 54 | return new BigNum(this._bi.multiply(o.bi)); 55 | } 56 | 57 | // Performs a division 58 | divide(o) { 59 | return new BigNum(this._bi.divide(o.bi)); 60 | } 61 | 62 | // Whether the given BigNum is equal to this one 63 | equals(o) { 64 | return this._bi.equals(o.bi); 65 | } 66 | 67 | // Generates a byte-array from this BigNum (defaults to little-endian) 68 | toArray(littleEndian = true, unsigned = true) { 69 | const ba = this._bi.toByteArray(); 70 | 71 | if (unsigned && this._bi.s === 0 && ba[0] === 0) { 72 | ba.shift(); 73 | } 74 | 75 | if (littleEndian) { 76 | return ba.reverse(); 77 | } 78 | 79 | return ba; 80 | } 81 | 82 | // Creates a new BigNum from given byte-array 83 | static fromArray(bytes, littleEndian = true, unsigned = true) { 84 | if (typeof bytes.toArray !== 'undefined') { 85 | bytes = bytes.toArray(); 86 | } else { 87 | bytes = bytes.slice(0); 88 | } 89 | 90 | if (littleEndian) { 91 | bytes = bytes.reverse(); 92 | } 93 | 94 | if (unsigned && bytes[0] & 0x80) { 95 | bytes.unshift(0); 96 | } 97 | 98 | return new BigNum(bytes); 99 | } 100 | 101 | // Creates a new random BigNum of the given number of bytes 102 | static fromRand(length) { 103 | // TODO: This should use a properly seeded, secure RNG 104 | const bytes = []; 105 | for (let i = 0; i < length; ++i) { 106 | bytes.push(Math.floor(Math.random() * 128)); 107 | } 108 | return new BigNum(bytes); 109 | } 110 | 111 | } 112 | 113 | export default BigNum; 114 | -------------------------------------------------------------------------------- /src/lib/crypto/crypt.js: -------------------------------------------------------------------------------- 1 | import { HMAC } from 'jsbn/lib/sha1'; 2 | import RC4 from 'jsbn/lib/rc4'; 3 | 4 | import ArrayUtil from '../utils/array-util'; 5 | 6 | class Crypt { 7 | 8 | // Creates crypt 9 | constructor() { 10 | 11 | // RC4's for encryption and decryption 12 | this._encrypt = null; 13 | this._decrypt = null; 14 | 15 | } 16 | 17 | // Encrypts given data through RC4 18 | encrypt(data) { 19 | if (this._encrypt) { 20 | this._encrypt.encrypt(data); 21 | } 22 | return this; 23 | } 24 | 25 | // Decrypts given data through RC4 26 | decrypt(data) { 27 | if (this._decrypt) { 28 | this._decrypt.decrypt(data); 29 | } 30 | return this; 31 | } 32 | 33 | // Sets session key and initializes this crypt 34 | set key(key) { 35 | console.info('initializing crypt'); 36 | 37 | // Fresh RC4's 38 | this._encrypt = new RC4(); 39 | this._decrypt = new RC4(); 40 | 41 | // Calculate the encryption hash (through the server decryption key) 42 | const enckey = ArrayUtil.fromHex('C2B3723CC6AED9B5343C53EE2F4367CE'); 43 | const enchash = HMAC.fromArrays(enckey, key); 44 | 45 | // Calculate the decryption hash (through the client decryption key) 46 | const deckey = ArrayUtil.fromHex('CC98AE04E897EACA12DDC09342915357'); 47 | const dechash = HMAC.fromArrays(deckey, key); 48 | 49 | // Seed RC4's with the computed hashes 50 | this._encrypt.init(enchash); 51 | this._decrypt.init(dechash); 52 | 53 | // Ensure the buffer is synchronized 54 | for (let i = 0; i < 1024; ++i) { 55 | this._encrypt.next(); 56 | this._decrypt.next(); 57 | } 58 | } 59 | 60 | } 61 | 62 | export default Crypt; 63 | -------------------------------------------------------------------------------- /src/lib/crypto/hash.js: -------------------------------------------------------------------------------- 1 | import ByteBuffer from 'byte-buffer'; 2 | 3 | // Feedable hash implementation 4 | class Hash { 5 | 6 | // Creates a new hash 7 | constructor() { 8 | 9 | // Data fed to this hash 10 | this._data = null; 11 | 12 | // Resulting digest 13 | this._digest = null; 14 | 15 | this.reset(); 16 | } 17 | 18 | // Retrieves digest (finalizes this hash if needed) 19 | get digest() { 20 | if (!this._digest) { 21 | this.finalize(); 22 | } 23 | return this._digest; 24 | } 25 | 26 | // Resets this hash, voiding the digest and allowing new feeds 27 | reset() { 28 | this._data = new ByteBuffer(0, ByteBuffer.BIG_ENDIAN, true); 29 | this._digest = null; 30 | return this; 31 | } 32 | 33 | // Feeds hash given value 34 | feed(value) { 35 | if (this._digest) { 36 | return this; 37 | } 38 | 39 | if (value.constructor === String) { 40 | this._data.writeString(value); 41 | } else { 42 | this._data.write(value); 43 | } 44 | 45 | return this; 46 | } 47 | 48 | // Finalizes this hash, calculates the digest and blocks additional feeds 49 | finalize() { 50 | return this; 51 | } 52 | 53 | } 54 | 55 | export default Hash; 56 | -------------------------------------------------------------------------------- /src/lib/crypto/hash/sha1.js: -------------------------------------------------------------------------------- 1 | import SHA1Base from 'jsbn/lib/sha1'; 2 | 3 | import Hash from '../hash'; 4 | 5 | // SHA-1 implementation 6 | class SHA1 extends Hash { 7 | 8 | // Finalizes this SHA-1 hash 9 | finalize() { 10 | this._digest = SHA1Base.fromArray(this._data.toArray()); 11 | } 12 | 13 | } 14 | 15 | export default SHA1; 16 | -------------------------------------------------------------------------------- /src/lib/crypto/srp.js: -------------------------------------------------------------------------------- 1 | import equal from 'deep-equal'; 2 | 3 | import BigNum from './big-num'; 4 | import SHA1 from './hash/sha1'; 5 | 6 | // Secure Remote Password 7 | // http://tools.ietf.org/html/rfc2945 8 | class SRP { 9 | 10 | // Creates new SRP instance with given constant prime and generator 11 | constructor(N, g) { 12 | 13 | // Constant prime (N) 14 | this._N = BigNum.fromArray(N); 15 | 16 | // Generator (g) 17 | this._g = BigNum.fromArray(g); 18 | 19 | // Client salt (provided by server) 20 | this._s = null; 21 | 22 | // Salted authentication hash 23 | this._x = null; 24 | 25 | // Random scrambling parameter 26 | this._u = null; 27 | 28 | // Derived key 29 | this._k = new BigNum(3); 30 | 31 | // Server's public ephemeral value (provided by server) 32 | this._B = null; 33 | 34 | // Password verifier 35 | this._v = null; 36 | 37 | // Client-side session key 38 | this._S = null; 39 | 40 | // Shared session key 41 | this._K = null; 42 | 43 | // Client proof hash 44 | this._M1 = null; 45 | 46 | // Expected server proof hash 47 | this._M2 = null; 48 | 49 | while (true) { 50 | 51 | // Client's private ephemeral value (random) 52 | this._a = BigNum.fromRand(19); 53 | 54 | // Client's public ephemeral value based on the above 55 | // A = g ^ a mod N 56 | this._A = this._g.modPow(this._a, this._N); 57 | 58 | if (!this._A.mod(this._N).equals(BigNum.ZERO)) { 59 | break; 60 | } 61 | } 62 | } 63 | 64 | // Retrieves client's public ephemeral value 65 | get A() { 66 | return this._A; 67 | } 68 | 69 | // Retrieves the session key 70 | get K() { 71 | return this._K; 72 | } 73 | 74 | // Retrieves the client proof hash 75 | get M1() { 76 | return this._M1; 77 | } 78 | 79 | // Feeds salt, server's public ephemeral value, account and password strings 80 | feed(s, B, I, P) { 81 | 82 | // Generated salt (s) and server's public ephemeral value (B) 83 | this._s = BigNum.fromArray(s); 84 | this._B = BigNum.fromArray(B); 85 | 86 | // Authentication hash consisting of user's account (I), a colon and user's password (P) 87 | // auth = H(I : P) 88 | const auth = new SHA1(); 89 | auth.feed(I); 90 | auth.feed(':'); 91 | auth.feed(P).finalize(); 92 | 93 | // Salted authentication hash consisting of the salt and the authentication hash 94 | // x = H(s | auth) 95 | const x = new SHA1(); 96 | x.feed(this._s.toArray()); 97 | x.feed(auth.digest); 98 | this._x = BigNum.fromArray(x.digest); 99 | 100 | // Password verifier 101 | // v = g ^ x mod N 102 | this._v = this._g.modPow(this._x, this._N); 103 | 104 | // Random scrambling parameter consisting of the public ephemeral values 105 | // u = H(A | B) 106 | const u = new SHA1(); 107 | u.feed(this._A.toArray()); 108 | u.feed(this._B.toArray()); 109 | this._u = BigNum.fromArray(u.digest); 110 | 111 | // Client-side session key 112 | // S = (B - (kg^x)) ^ (a + ux) 113 | const kgx = this._k.multiply(this._g.modPow(this._x, this._N)); 114 | const aux = this._a.add(this._u.multiply(this._x)); 115 | this._S = this._B.subtract(kgx).modPow(aux, this._N); 116 | 117 | // Store odd and even bytes in separate byte-arrays 118 | const S = this._S.toArray(); 119 | const S1 = []; 120 | const S2 = []; 121 | for (let i = 0; i < 16; ++i) { 122 | S1[i] = S[i * 2]; 123 | S2[i] = S[i * 2 + 1]; 124 | } 125 | 126 | // Hash these byte-arrays 127 | const S1h = new SHA1(); 128 | const S2h = new SHA1(); 129 | S1h.feed(S1).finalize(); 130 | S2h.feed(S2).finalize(); 131 | 132 | // Shared session key generation by interleaving the previously generated hashes 133 | this._K = []; 134 | for (let i = 0; i < 20; ++i) { 135 | this._K[i * 2] = S1h.digest[i]; 136 | this._K[i * 2 + 1] = S2h.digest[i]; 137 | } 138 | 139 | // Generate username hash 140 | const userh = new SHA1(); 141 | userh.feed(I).finalize(); 142 | 143 | // Hash both prime and generator 144 | const Nh = new SHA1(); 145 | const gh = new SHA1(); 146 | Nh.feed(this._N.toArray()).finalize(); 147 | gh.feed(this._g.toArray()).finalize(); 148 | 149 | // XOR N-prime and generator 150 | const Ngh = []; 151 | for (let i = 0; i < 20; ++i) { 152 | Ngh[i] = Nh.digest[i] ^ gh.digest[i]; 153 | } 154 | 155 | // Calculate M1 (client proof) 156 | // M1 = H( (H(N) ^ H(G)) | H(I) | s | A | B | K ) 157 | this._M1 = new SHA1(); 158 | this._M1.feed(Ngh); 159 | this._M1.feed(userh.digest); 160 | this._M1.feed(this._s.toArray()); 161 | this._M1.feed(this._A.toArray()); 162 | this._M1.feed(this._B.toArray()); 163 | this._M1.feed(this._K); 164 | this._M1.finalize(); 165 | 166 | // Pre-calculate M2 (expected server proof) 167 | // M2 = H( A | M1 | K ) 168 | this._M2 = new SHA1(); 169 | this._M2.feed(this._A.toArray()); 170 | this._M2.feed(this._M1.digest); 171 | this._M2.feed(this._K); 172 | this._M2.finalize(); 173 | } 174 | 175 | // Validates given M2 with expected M2 176 | validate(M2) { 177 | if (!this._M2) { 178 | return false; 179 | } 180 | return equal(M2.toArray(), this._M2.digest); 181 | } 182 | 183 | } 184 | 185 | export default SRP; 186 | -------------------------------------------------------------------------------- /src/lib/game/chat/chatEnum.js: -------------------------------------------------------------------------------- 1 | class ChatEnum { 2 | static channel = "world"; // hacky workaround 3 | static CHAT_MSG_ADDON = 0xFFFFFFFF; 4 | static CHAT_MSG_SYSTEM = 0x00; 5 | static CHAT_MSG_SAY = 0x01; 6 | static CHAT_MSG_PARTY = 0x02; 7 | static CHAT_MSG_RAID = 0x03; 8 | static CHAT_MSG_GUILD = 0x04; 9 | static CHAT_MSG_OFFICER = 0x05; 10 | static CHAT_MSG_YELL = 0x06; 11 | static CHAT_MSG_WHISPER = 0x07; 12 | static CHAT_MSG_WHISPER_FOREIGN = 0x08; 13 | static CHAT_MSG_WHISPER_INFORM = 0x09; 14 | static CHAT_MSG_EMOTE = 0x0A; 15 | static CHAT_MSG_TEXT_EMOTE = 0x0B; 16 | static CHAT_MSG_MONSTER_SAY = 0x0C; 17 | static CHAT_MSG_MONSTER_PARTY = 0x0D; 18 | static CHAT_MSG_MONSTER_YELL = 0x0E; 19 | static CHAT_MSG_MONSTER_WHISPER = 0x0F; 20 | static CHAT_MSG_MONSTER_EMOTE = 0x10; 21 | static CHAT_MSG_CHANNEL = 0x11; 22 | static CHAT_MSG_CHANNEL_JOIN = 0x12; 23 | static CHAT_MSG_CHANNEL_LEAVE = 0x13; 24 | static CHAT_MSG_CHANNEL_LIST = 0x14; 25 | static CHAT_MSG_CHANNEL_NOTICE = 0x15; 26 | static CHAT_MSG_CHANNEL_NOTICE_USER = 0x16; 27 | static CHAT_MSG_AFK = 0x17; 28 | static CHAT_MSG_DND = 0x18; 29 | static CHAT_MSG_IGNORED = 0x19; 30 | static CHAT_MSG_SKILL = 0x1A; 31 | static CHAT_MSG_LOOT = 0x1B; 32 | static CHAT_MSG_MONEY = 0x1C; 33 | static CHAT_MSG_OPENING = 0x1D; 34 | static CHAT_MSG_TRADESKILLS = 0x1E; 35 | static CHAT_MSG_PET_INFO = 0x1F; 36 | static CHAT_MSG_COMBAT_MISC_INFO = 0x20; 37 | static CHAT_MSG_COMBAT_XP_GAIN = 0x21; 38 | static CHAT_MSG_COMBAT_HONOR_GAIN = 0x22; 39 | static CHAT_MSG_COMBAT_FACTION_CHANGE = 0x23; 40 | static CHAT_MSG_BG_SYSTEM_NEUTRAL = 0x24; 41 | static CHAT_MSG_BG_SYSTEM_ALLIANCE = 0x25; 42 | static CHAT_MSG_BG_SYSTEM_HORDE = 0x26; 43 | static CHAT_MSG_RAID_LEADER = 0x27; 44 | static CHAT_MSG_RAID_WARNING = 0x28; 45 | static CHAT_MSG_RAID_BOSS_EMOTE = 0x29; 46 | static CHAT_MSG_RAID_BOSS_WHISPER = 0x2A; 47 | static CHAT_MSG_FILTERED = 0x2B; 48 | static CHAT_MSG_BATTLEGROUND = 0x2C; 49 | static CHAT_MSG_BATTLEGROUND_LEADER = 0x2D; 50 | static CHAT_MSG_RESTRICTED = 0x2E; 51 | static CHAT_MSG_BATTLENET = 0x2F; 52 | static CHAT_MSG_ACHIEVEMENT = 0x30; 53 | static CHAT_MSG_GUILD_ACHIEVEMENT = 0x31; 54 | static CHAT_MSG_ARENA_POINTS = 0x32; 55 | static CHAT_MSG_PARTY_LEADER = 0x33; 56 | } 57 | 58 | export default ChatEnum; -------------------------------------------------------------------------------- /src/lib/game/chat/handler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import Message from './message'; 4 | import GamePacket from '../packet'; 5 | import GameOpcode from '../opcode'; 6 | import ChatEnum from './chatEnum'; 7 | import Language from './langEnum'; 8 | 9 | class ChatHandler extends EventEmitter { 10 | 11 | 12 | // Creates a new chat handler 13 | constructor(session) { 14 | super(); 15 | 16 | // Holds session 17 | this.session = session; 18 | 19 | this.playerNames = this.session.game.playerNames; 20 | 21 | var welcome = new Message('system', this.session.config.title,0); 22 | 23 | // Holds messages 24 | this.sayMessages = [ 25 | welcome, 26 | new Message('info', 'This is an info message',0), 27 | new Message('error', 'This is an error message',0), 28 | new Message('area', 'Player: This is a message emitted nearby',0), 29 | ]; 30 | 31 | this.guildMessages = [ 32 | welcome, 33 | new Message('guild', '[Guild] Someone: This is your guild channel (if you have a guild)',0) 34 | ]; 35 | 36 | this.worldMessages = [ 37 | welcome, 38 | new Message('channel', '[World]: This is the official world channel',0), 39 | ] 40 | 41 | this.wispMessages = [ 42 | welcome, 43 | new Message('whisper outgoing', 'To [Someone]: This is an outgoing whisper',0), 44 | new Message('whisper incoming', 'wispers: This is an incoming whisper',0), 45 | ] 46 | 47 | this.logsMessages = [ 48 | welcome, 49 | new Message('info', '[Logs]: This is a log window',0), 50 | ] 51 | 52 | // Listen for messages 53 | this.session.game.on('packet:receive:SMSG_GM_MESSAGECHAT', ::this.handleGmMessage); 54 | this.session.game.on('packet:receive:SMSG_MESSAGE_CHAT', ::this.handleMessage); 55 | this.session.game.on('packet:receive:SMSG_CHANNEL_NOTIFY', ::this.handleNotify); 56 | } 57 | 58 | // Creates chat message 59 | create() { 60 | return new Message(); 61 | } 62 | 63 | // Sends given message 64 | send(_message,type, dest) { 65 | var size=64+_message.length; 66 | 67 | var channel = ChatEnum.channel+"\0"; 68 | 69 | 70 | if (type==ChatEnum.CHAT_MSG_CHANNEL) { 71 | size += channel.length; 72 | } 73 | 74 | const app = new GamePacket(GameOpcode.CMSG_MESSAGE_CHAT, size); 75 | app.writeUnsignedInt(type); // type 76 | 77 | app.writeUnsignedInt(Language.LANG_COMMON); // lang , 7: common [TODO: use race specific ] 78 | 79 | switch(type) { 80 | case ChatEnum.CHAT_MSG_SAY: 81 | case ChatEnum.CHAT_MSG_GUILD: 82 | app.writeString(_message); 83 | break; 84 | case ChatEnum.CHAT_MSG_CHANNEL: 85 | app.writeString(channel); 86 | app.writeString(_message); 87 | break; 88 | case ChatEnum.CHAT_MSG_WHISPER: 89 | app.writeString(dest+"\0"); 90 | app.writeString(_message); 91 | break; 92 | } 93 | 94 | this.session.game.send(app); 95 | return true; 96 | } 97 | 98 | handleNotify(gp) { 99 | console.log(gp); 100 | } 101 | 102 | handleGmMessage(gp) { 103 | this.handleMessage(gp,true); 104 | } 105 | 106 | 107 | // Message handler (SMSG_MESSAGE_CHAT) 108 | handleMessage(gp,isGm) { 109 | var guid2 = 0; 110 | 111 | var type = gp.readUnsignedByte(); // type 112 | const lang = gp.readUnsignedInt(); // language 113 | const guid1 = gp.readGUID(); 114 | const unk1 = gp.readUnsignedInt(); 115 | 116 | var isAddon = lang == Language.LANG_ADDON; 117 | 118 | if (isGm === true) 119 | { 120 | var nameLen = gp.readUnsignedInt(); 121 | var senderName = gp.readString(nameLen); 122 | 123 | this.playerNames[guid1.low] = { 124 | name : senderName, 125 | isGm : true 126 | }; 127 | 128 | } else { 129 | if (!this.playerNames[guid1.low]) { 130 | this.playerNames[guid1.low]= { name: guid1.low }; 131 | this.session.game.askName(guid1); 132 | } 133 | } 134 | 135 | var channelName=""; 136 | 137 | var len = 0; 138 | var text = ""; 139 | var flags = 0; 140 | var senderName = ""; 141 | var recvGuid = ""; 142 | 143 | switch(type) { 144 | case ChatEnum.CHAT_MSG_CHANNEL: 145 | // hardcoded channel 146 | channelName = gp.readString(5); 147 | if (channelName !== ChatEnum.channel) 148 | return; 149 | 150 | var _unk=gp.readUnsignedInt(); 151 | len = gp.length - gp.index - 2; // channel buffer min size 152 | text = gp.readString(len); 153 | break; 154 | case ChatEnum.CHAT_MSG_WHISPER_FOREIGN: 155 | len = gp.readUnsignedInt(); 156 | senderName = gp.readString(len); 157 | 158 | recvGuid = gp.readGUID(); 159 | 160 | if (!this.playerNames[recvGuid.low]) { 161 | this.playerNames[recvGuid.low]= { name: recvGuid.low }; 162 | this.session.game.askName(recvGuid); 163 | } 164 | break; 165 | default: 166 | guid2 = gp.readGUID(); // guid2 167 | 168 | if (!this.playerNames[guid2.low]) { 169 | this.playerNames[guid2.low]= { name: guid2.low }; 170 | this.session.game.askName(guid2); 171 | } 172 | 173 | len = gp.readUnsignedInt(); 174 | 175 | text = gp.readString(len); 176 | flags = gp.readUnsignedByte(); // flags 177 | break; 178 | } 179 | 180 | const message = null; 181 | 182 | var chatLimit=300; 183 | 184 | if (isAddon) { 185 | type = ChatEnum.CHAT_MSG_ADDON; 186 | } 187 | 188 | switch(type) { 189 | case ChatEnum.CHAT_MSG_SAY: 190 | message = new Message("area", text, guid1.low); 191 | this.sayMessages.push(message); 192 | this.sayMessages.length > chatLimit && this.sayMessages.shift(); 193 | break; 194 | case ChatEnum.CHAT_MSG_SYSTEM: 195 | message = new Message("system", text, 0); // hardcoded guid 196 | this.sayMessages.push(message); 197 | this.sayMessages.length > chatLimit && this.sayMessages.shift(); 198 | break; 199 | case ChatEnum.CHAT_MSG_EMOTE: 200 | message = new Message("me", text, guid1.low); 201 | this.sayMessages.push(message); 202 | this.sayMessages.length > chatLimit && this.sayMessages.shift(); 203 | break; 204 | case ChatEnum.CHAT_MSG_YELL: 205 | message = new Message("yell", text, guid1.low); 206 | this.sayMessages.push(message); 207 | this.sayMessages.length > chatLimit && this.sayMessages.shift(); 208 | break; 209 | case ChatEnum.CHAT_MSG_GUILD: 210 | message = new Message("guild", text, guid1.low); 211 | this.guildMessages.push(message); 212 | this.guildMessages.length > chatLimit && this.guildMessages.shift(); 213 | break; 214 | case ChatEnum.CHAT_MSG_CHANNEL: 215 | message = new Message("channel", text, guid1.low); 216 | this.worldMessages.push(message); 217 | this.worldMessages.length > chatLimit && this.worldMessages.shift(); 218 | break; 219 | case ChatEnum.CHAT_MSG_WHISPER: 220 | message = new Message("whisper incoming", text, guid1.low, guid2.low); 221 | this.wispMessages.push(message); 222 | this.wispMessages.length > chatLimit && this.wispMessages.shift(); 223 | break; 224 | case ChatEnum.CHAT_MSG_WHISPER_INFORM: 225 | message = new Message("whisper outgoing", text, guid1.low, guid2.low); 226 | this.wispMessages.push(message); 227 | this.wispMessages.length > chatLimit && this.wispMessages.shift(); 228 | break; 229 | case ChatEnum.CHAT_MSG_WHISPER_FOREIGN: 230 | message = new Message("whisper incoming", text, senderName, recvGuid.low); 231 | this.wispMessages.push(message); 232 | this.wispMessages.length > chatLimit && this.wispMessages.shift(); 233 | break; 234 | default: 235 | message = new Message("info", text, guid1.low); 236 | this.logsMessages.push(message); 237 | this.logsMessages.length > chatLimit && this.logsMessages.shift(); 238 | break; 239 | } 240 | 241 | this.emit('message', message, type); 242 | } 243 | 244 | } 245 | 246 | export default ChatHandler; 247 | -------------------------------------------------------------------------------- /src/lib/game/chat/langEnum.js: -------------------------------------------------------------------------------- 1 | class Language 2 | { 3 | static LANG_UNIVERSAL = 0; 4 | static LANG_ORCISH = 1; 5 | static LANG_DARNASSIAN = 2; 6 | static LANG_TAURAHE = 3; 7 | static LANG_DWARVISH = 6; 8 | static LANG_COMMON = 7; 9 | static LANG_DEMONIC = 8; 10 | static LANG_TITAN = 9; 11 | static LANG_THALASSIAN = 10; 12 | static LANG_DRACONIC = 11; 13 | static LANG_KALIMAG = 12; 14 | static LANG_GNOMISH = 13; 15 | static LANG_TROLL = 14; 16 | static LANG_GUTTERSPEAK = 33; 17 | static LANG_DRAENEI = 35; 18 | static LANG_ZOMBIE = 36; 19 | static LANG_GNOMISH_BINARY = 37; 20 | static LANG_GOBLIN_BINARY = 38; 21 | static LANG_ADDON = 0xFFFFFFFF; // used by addons, in 2.4.0 not exist, replaced by messagetype? 22 | }; 23 | 24 | export default Language; -------------------------------------------------------------------------------- /src/lib/game/chat/message.js: -------------------------------------------------------------------------------- 1 | class ChatMessage { 2 | 3 | // Creates a new message 4 | constructor(kind, text, guid1, guid2) { 5 | this.kind = kind; 6 | this.text = text; 7 | this.guid1 = guid1; 8 | this.guid2 = guid2; 9 | this.timestamp = new Date(); 10 | } 11 | 12 | // Short string representation of this message 13 | toString() { 14 | return `[Message; Text: ${this.text}; GUID: ${this.guid}]`; 15 | } 16 | 17 | } 18 | 19 | export default ChatMessage; 20 | -------------------------------------------------------------------------------- /src/lib/game/entity.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | class Entity extends EventEmitter { 4 | 5 | constructor() { 6 | super(); 7 | this.guid = Math.random() * 1000000 | 0; 8 | } 9 | 10 | } 11 | 12 | export default Entity; 13 | -------------------------------------------------------------------------------- /src/lib/game/guid.js: -------------------------------------------------------------------------------- 1 | class GUID { 2 | 3 | // GUID byte-length (64-bit) 4 | static LENGTH = 8; 5 | 6 | // Creates a new GUID 7 | constructor(buffer) { 8 | 9 | // Holds raw byte representation 10 | this.raw = buffer; 11 | 12 | // Holds low-part 13 | this.low = buffer.readUnsignedInt(); 14 | 15 | // Holds high-part 16 | this.high = buffer.readUnsignedInt(); 17 | 18 | } 19 | 20 | // Short string representation of this GUID 21 | toString() { 22 | const high = ('00000000' + this.high.toString(16)).slice(-8); 23 | const low = ('00000000' + this.low.toString(16)).slice(-8); 24 | return `[GUID; Hex: 0x${high}${low}]`; 25 | } 26 | 27 | } 28 | 29 | export default GUID; 30 | -------------------------------------------------------------------------------- /src/lib/game/handler.js: -------------------------------------------------------------------------------- 1 | import ByteBuffer from 'byte-buffer'; 2 | 3 | import BigNum from '../crypto/big-num'; 4 | import Crypt from '../crypto/crypt'; 5 | import GameOpcode from './opcode'; 6 | import GamePacket from './packet'; 7 | import GUID from '../game/guid'; 8 | import SHA1 from '../crypto/hash/sha1'; 9 | import Socket from '../net/socket'; 10 | import ChatEnum from '../game/chat/chatEnum'; 11 | import Player from './player'; 12 | 13 | class GameHandler extends Socket { 14 | 15 | static pingRecv = true; 16 | 17 | // Creates a new game handler 18 | constructor(session) { 19 | super(); 20 | 21 | // Holds session 22 | this.session = session; 23 | 24 | this.session.player = new Player("Player",-1); 25 | 26 | // [guid] = name 27 | this.playerNames = []; 28 | 29 | this.playerNames[0] = { name : "SYSTEM" }; 30 | 31 | // Listen for incoming data 32 | this.on('data:receive', ::this.dataReceived); 33 | 34 | // Delegate packets 35 | this.on('packet:receive:SMSG_PONG', ::this.handlePong); 36 | this.on('packet:receive:SMSG_AUTH_CHALLENGE', ::this.handleAuthChallenge); 37 | this.on('packet:receive:SMSG_AUTH_RESPONSE', ::this.handleAuthResponse); 38 | this.on('packet:receive:SMSG_LOGIN_VERIFY_WORLD', ::this.handleWorldLogin); 39 | this.on('packet:receive:SMSG_NAME_QUERY_RESPONSE', ::this.handleName); 40 | } 41 | 42 | // Connects to given host through given realm information 43 | connect(host, realm) { 44 | // this.realm = _realm; 45 | 46 | if (!this.connected) { 47 | super.connect(host, realm.port); 48 | console.info('connecting to game-server @', this.host, ':', this.port); 49 | } 50 | return this; 51 | } 52 | 53 | // Finalizes and sends given packet 54 | send(packet) { 55 | const size = packet.bodySize + GamePacket.OPCODE_SIZE_OUTGOING; 56 | 57 | packet.front(); 58 | packet.writeShort(size, ByteBuffer.BIG_ENDIAN); 59 | packet.writeUnsignedInt(packet.opcode); 60 | 61 | // Encrypt header if needed 62 | if (this._crypt) { 63 | this._crypt.encrypt(new Uint8Array(packet.buffer, 0, GamePacket.HEADER_SIZE_OUTGOING)); 64 | } 65 | 66 | return super.send(packet); 67 | } 68 | 69 | // Attempts to join game with given character 70 | join(character) { 71 | var name = character.toString(); 72 | 73 | this.session.player.name = character.name; 74 | this.session.player.guid = character.guid; 75 | 76 | this.playerNames[character.guid.low] = { 77 | name : character.name 78 | } 79 | 80 | if (character) { 81 | console.info('joining game with', character.toString()); 82 | 83 | const gp = new GamePacket(GameOpcode.CMSG_PLAYER_LOGIN, GamePacket.HEADER_SIZE_OUTGOING + GUID.LENGTH); 84 | gp.writeGUID(character.guid); 85 | return this.send(gp); 86 | } 87 | 88 | return false; 89 | } 90 | 91 | // Data received handler 92 | dataReceived(_socket) { 93 | while (true) { 94 | if (!this.connected) { 95 | return; 96 | } 97 | 98 | var isLarge = false; 99 | 100 | if (this.remaining === false) { 101 | 102 | if (this.buffer.available < GamePacket.HEADER_SIZE_INCOMING) { 103 | return; 104 | } 105 | 106 | // Decrypt header if needed 107 | if (this._crypt) { 108 | this._crypt.decrypt(new Uint8Array(this.buffer.buffer, this.buffer.index, GamePacket.HEADER_SIZE_INCOMING)); 109 | } 110 | 111 | var firstByte=this.buffer.raw[this.buffer.index]; 112 | isLarge = firstByte & GamePacket.LARGE_PACKET_FLAG; 113 | 114 | if (isLarge) { 115 | this._crypt.decrypt(new Uint8Array(this.buffer.buffer, this.buffer.index + GamePacket.HEADER_SIZE_INCOMING, 1)); 116 | this.remaining = this.buffer.readUnsignedByte(ByteBuffer.BIG_ENDIAN) | this.buffer.readUnsignedShort(ByteBuffer.BIG_ENDIAN); 117 | } else { 118 | this.remaining = this.buffer.readUnsignedShort(ByteBuffer.BIG_ENDIAN); 119 | } 120 | } 121 | 122 | if (this.remaining > 0 && this.buffer.available >= this.remaining) { 123 | const size = GamePacket.OPCODE_SIZE_INCOMING + this.remaining; 124 | const gp = new GamePacket(this.buffer.readUnsignedShort(), this.buffer.seek(-GamePacket.HEADER_SIZE_INCOMING).read(size), false , isLarge); 125 | 126 | this.remaining = false; 127 | 128 | console.log('⟹', gp.toString()); 129 | // console.debug gp.toHex() 130 | // console.debug gp.toASCII() 131 | 132 | this.emit('packet:receive', gp); 133 | if (gp.opcodeName) { 134 | this.emit(`packet:receive:${gp.opcodeName}`, gp); 135 | } 136 | 137 | } else if (this.remaining !== 0) { 138 | return; 139 | } 140 | } 141 | } 142 | 143 | handleName(gp) { 144 | const guid = gp.readPackedGUID(); 145 | const name_known = gp.readUnsignedByte(); 146 | const name = gp.readCString(); 147 | const realm = gp.readCString(); // only for crossrealm 148 | 149 | const race = gp.readUnsignedByte(); 150 | const gender = gp.readUnsignedByte(); // guid2 151 | const playerClass = gp.readUnsignedByte(); 152 | const declined = gp.readUnsignedByte(); 153 | 154 | this.session.player.name=name; 155 | 156 | this.playerNames[guid] = { 157 | name : name 158 | //race : race, 159 | //gender : gender, 160 | //playerClass : playerClass 161 | }; 162 | 163 | this.session.chat.emit("message",null); // to refresh 164 | } 165 | 166 | askName(guid) { 167 | const app = new GamePacket(GameOpcode.CMSG_NAME_QUERY, 64); 168 | 169 | app.writeGUID(guid); 170 | 171 | this.session.game.send(app); 172 | return true; 173 | } 174 | 175 | // Pong handler (SMSG_PONG) 176 | handlePong(gp) { 177 | console.log("pong"); 178 | this.pingRecv = true; 179 | var ping=gp.readUnsignedInt(); // (0x01) 180 | } 181 | 182 | ping() { 183 | console.log("ping"); 184 | if (this.pingRecv === false) { 185 | this.disconnect(); 186 | } 187 | 188 | const app = new GamePacket(GameOpcode.CMSG_PING, GamePacket.OPCODE_SIZE_INCOMING + 64); 189 | app.writeUnsignedInt(1); // ping ( unknown value) 190 | app.writeUnsignedInt(10); // latency, 10ms for now 191 | 192 | this.pingRecv = false; 193 | 194 | this.send(app); 195 | } 196 | 197 | // Auth challenge handler (SMSG_AUTH_CHALLENGE) 198 | handleAuthChallenge(gp) { 199 | console.info('handling auth challenge'); 200 | 201 | gp.readUnsignedInt(); // (0x01) 202 | 203 | const salt = gp.read(4); 204 | 205 | const seed = BigNum.fromRand(4); 206 | 207 | const hash = new SHA1(); 208 | hash.feed(this.session.auth.account); 209 | hash.feed([0, 0, 0, 0]); 210 | hash.feed(seed.toArray()); 211 | hash.feed(salt); 212 | hash.feed(this.session.auth.key); 213 | 214 | const build = this.session.config.build; 215 | const account = this.session.auth.account; 216 | 217 | const size = GamePacket.HEADER_SIZE_OUTGOING + 8 + this.session.auth.account.length + 1 + 4 + 4 + 20 + 20 + 4; 218 | 219 | const app = new GamePacket(GameOpcode.CMSG_AUTH_PROOF, size); 220 | app.writeUnsignedInt(build); // build 221 | app.writeUnsignedInt(0); // (?) 222 | app.writeCString(account); // account 223 | app.writeUnsignedInt(0); // (?) 224 | app.write(seed.toArray()); // client-seed 225 | app.writeUnsignedInt(0); // (?) 226 | app.writeUnsignedInt(0); // (?) 227 | 228 | // app.writeUnsignedInt(this.realm.id); // realmid 229 | app.writeUnsignedInt(1); // realmid 230 | 231 | app.writeUnsignedInt(0); // (?) 232 | app.writeUnsignedInt(0); // (?) 233 | app.write(hash.digest); // digest 234 | app.writeUnsignedInt(0); // addon-data 235 | 236 | this.send(app); 237 | 238 | this._crypt = new Crypt(); 239 | this._crypt.key = this.session.auth.key; 240 | } 241 | 242 | // Auth response handler (SMSG_AUTH_RESPONSE) 243 | handleAuthResponse(gp) { 244 | console.info('handling auth response'); 245 | 246 | // Handle result byte 247 | const result = gp.readUnsignedByte(); 248 | if (result === 0x0D) { 249 | console.warn('server-side auth/realm failure; try again'); 250 | this.emit('reject'); 251 | return; 252 | } 253 | 254 | if (result === 0x15) { 255 | console.warn('account in use/invalid; aborting'); 256 | this.emit('reject'); 257 | return; 258 | } 259 | 260 | // TODO: Ensure the account is flagged as WotLK (expansion //2) 261 | 262 | this.emit('authenticate'); 263 | } 264 | 265 | // World login handler (SMSG_LOGIN_VERIFY_WORLD) 266 | handleWorldLogin(_gp) { 267 | var that=this; 268 | setInterval(function() { 269 | that.ping() 270 | },50000) 271 | 272 | this.joinWorldChannel(); 273 | 274 | 275 | this.emit('join'); 276 | } 277 | 278 | 279 | joinWorldChannel() { 280 | console.log("join world"); 281 | 282 | var channel=ChatEnum.channel; 283 | var pass=""; 284 | 285 | var size=1 + 16 + 4 + 4 + channel.length + pass.length; 286 | const app = new GamePacket(GameOpcode.CMSG_JOIN_CHANNEL, size); 287 | app.writeUnsignedInt(0); 288 | app.writeByte(0); 289 | app.writeByte(0); 290 | app.writeString(channel); 291 | app.writeString(pass); 292 | 293 | this.session.game.send(app); 294 | return true; 295 | } 296 | 297 | } 298 | 299 | export default GameHandler; -------------------------------------------------------------------------------- /src/lib/game/opcode.js: -------------------------------------------------------------------------------- 1 | class GameOpcode { 2 | 3 | static CMSG_CHAR_ENUM = 0x0037; 4 | 5 | static SMSG_CHAR_ENUM = 0x003B; 6 | 7 | static CMSG_PLAYER_LOGIN = 0x003D; 8 | 9 | static SMSG_CHARACTER_LOGIN_FAILED = 0x0041; 10 | static SMSG_LOGIN_SETTIMESPEED = 0x0042; 11 | 12 | static CMSG_NAME_QUERY = 0x0050; 13 | static SMSG_NAME_QUERY_RESPONSE = 0x0051; 14 | 15 | static SMSG_CONTACT_LIST = 0x0067; 16 | 17 | static CMSG_MESSAGE_CHAT = 0x0095; 18 | static SMSG_MESSAGE_CHAT = 0x0096; 19 | 20 | static CMSG_JOIN_CHANNEL = 0x0097; 21 | 22 | static SMSG_CHANNEL_NOTIFY = 0x0099; 23 | 24 | static SMSG_UPDATE_OBJECT = 0x00A9; 25 | 26 | static SMSG_MONSTER_MOVE = 0x00DD; 27 | 28 | static SMSG_TUTORIAL_FLAGS = 0x00FD; 29 | 30 | static SMSG_INITIALIZE_FACTIONS = 0x0122; 31 | 32 | static SMSG_SET_PROFICIENCY = 0x0127; 33 | 34 | static SMSG_ACTION_BUTTONS = 0x0129; 35 | static SMSG_INITIAL_SPELLS = 0x012A; 36 | 37 | static SMSG_SPELL_START = 0x0131; 38 | static SMSG_SPELL_GO = 0x0132; 39 | 40 | static SMSG_BINDPOINT_UPDATE = 0x0155; 41 | 42 | static CMSG_PING = 0x01DC; 43 | static SMSG_PONG = 0x01DD; 44 | 45 | static SMSG_ITEM_TIME_UPDATE = 0x01EA; 46 | 47 | static SMSG_AUTH_CHALLENGE = 0x01EC; 48 | static CMSG_AUTH_PROOF = 0x01ED; 49 | static SMSG_AUTH_RESPONSE = 0x01EE; 50 | 51 | static SMSG_COMPRESSED_UPDATE_OBJECT = 0x01F6; 52 | 53 | static SMSG_ACCOUNT_DATA_TIMES = 0x0209; 54 | 55 | static SMSG_LOGIN_VERIFY_WORLD = 0x0236; 56 | 57 | static SMSG_SPELL_NON_MELEE_DAMAGE_LOG = 0x0250; 58 | 59 | static SMSG_INIT_WORLD_STATES = 0x02C2; 60 | static SMSG_UPDATE_WORLD_STATE = 0x02C3; 61 | 62 | static SMSG_WEATHER = 0x02F4; 63 | 64 | static MSG_SET_DUNGEON_DIFFICULTY = 0x0329; 65 | 66 | static SMSG_UPDATE_INSTANCE_OWNERSHIP = 0x032B; 67 | 68 | static SMSG_INSTANCE_DIFFICULTY = 0x033B; 69 | 70 | static SMSG_MOTD = 0x033D; 71 | 72 | static SMSG_TIME_SYNC_REQ = 0x0390; 73 | 74 | static SMSG_GM_MESSAGECHAT = 0x03B3; 75 | 76 | static SMSG_FEATURE_SYSTEM_STATUS = 0x03C9; 77 | 78 | static SMSG_SERVER_BUCK_DATA = 0x041D; 79 | static SMSG_SEND_UNLEARN_SPELLS = 0x041E; 80 | 81 | static SMSG_LEARNED_DANCE_MOVES = 0x0455; 82 | 83 | static SMSG_ALL_ACHIEVEMENT_DATA = 0x047D; 84 | 85 | static SMSG_POWER_UPDATE = 0x0480; 86 | 87 | static SMSG_AURA_UPDATE_ALL = 0x0495; 88 | static SMSG_AURA_UPDATE = 0x0496; 89 | 90 | static SMSG_EQUIPMENT_SET_LIST = 0x04BC; 91 | 92 | static SMSG_TALENTS_INFO = 0x04C0; 93 | 94 | static MSG_SET_RAID_DIFFICULTY = 0x04EB; 95 | 96 | } 97 | 98 | export default GameOpcode; 99 | -------------------------------------------------------------------------------- /src/lib/game/packet.js: -------------------------------------------------------------------------------- 1 | import BasePacket from '../net/packet'; 2 | import GameOpcode from './opcode'; 3 | import GUID from './guid'; 4 | import ObjectUtil from '../utils/object-util'; 5 | 6 | class GamePacket extends BasePacket { 7 | 8 | // Header sizes in bytes for both incoming and outgoing packets 9 | static HEADER_LARGE_SIZE_INCOMING = 5; 10 | static HEADER_SIZE_INCOMING = 4; 11 | static HEADER_SIZE_OUTGOING = 6; 12 | 13 | // Opcode sizes in bytes for both incoming and outgoing packets 14 | static OPCODE_SIZE_INCOMING = 2; 15 | static OPCODE_SIZE_OUTGOING = 4; 16 | 17 | static LARGE_PACKET_FLAG = 0x80; 18 | 19 | constructor(opcode, source, outgoing = true, isLarge = false) { 20 | if (!source) { 21 | if (outgoing === true) { 22 | source = GamePacket.HEADER_SIZE_OUTGOING 23 | } else { 24 | source = (isLarge === true ? GamePacket.HEADER_LARGE_SIZE_INCOMING : GamePacket.HEADER_SIZE_INCOMING); 25 | } 26 | } 27 | 28 | super(opcode, source, outgoing); 29 | 30 | // is it a large package ? 31 | this.isLarge = isLarge; 32 | } 33 | 34 | // Retrieves the name of the opcode for this packet (if available) 35 | get opcodeName() { 36 | return ObjectUtil.keyByValue(GameOpcode, this.opcode); 37 | } 38 | 39 | // Header size in bytes (dependent on packet origin) 40 | get headerSize() { 41 | if (this.outgoing) { 42 | return this.constructor.HEADER_SIZE_OUTGOING; 43 | } 44 | 45 | return this.isLarge === true ? GamePacket.HEADER_LARGE_SIZE_INCOMING : GamePacket.HEADER_SIZE_INCOMING; 46 | } 47 | 48 | // Reads GUID from this packet 49 | readGUID() { 50 | return new GUID(this.read(GUID.LENGTH)); 51 | } 52 | 53 | // Writes given GUID to this packet 54 | writeGUID(guid) { 55 | this.write(guid.raw); 56 | return this; 57 | } 58 | 59 | // // Reads packed GUID from this packet 60 | // // TODO: Implementation 61 | // readPackedGUID: -> 62 | // return null 63 | 64 | readPackedGUID() { 65 | var guidMark = this.readUnsignedByte(); 66 | 67 | var guid = 0; 68 | 69 | var i; 70 | for (i = 0; i < 8; ++i) 71 | { 72 | if(guidMark & (1 << i)) 73 | { 74 | if(this.index + 1 > this.length) 75 | throw "Buffer exception "+this.index+" >= "+this.lenght; 76 | 77 | var bit = this.readUnsignedByte(); 78 | guid |= (bit << (i * 8)); 79 | } 80 | } 81 | 82 | return guid; 83 | } 84 | 85 | // // Writes given GUID to this packet in packed form 86 | // // TODO: Implementation 87 | // writePackedGUID: (guid) -> 88 | // return this 89 | 90 | } 91 | 92 | export default GamePacket; 93 | -------------------------------------------------------------------------------- /src/lib/game/player.js: -------------------------------------------------------------------------------- 1 | import Unit from './unit'; 2 | 3 | class Player extends Unit { 4 | 5 | constructor(name, guid) { 6 | super(guid); 7 | 8 | this.name = name; 9 | this.hp = this.hp; 10 | this.mp = this.mp; 11 | 12 | this.target = null; 13 | 14 | this.displayID = 24978; 15 | this.mapID = null; 16 | } 17 | 18 | worldport(mapID, x, y, z) { 19 | if (!this.mapID || this.mapID !== mapID) { 20 | this.mapID = mapID; 21 | this.emit('map:change', mapID); 22 | } 23 | 24 | this.position.set(x, y, z); 25 | this.emit('position:change', this); 26 | } 27 | 28 | } 29 | 30 | export default Player; 31 | -------------------------------------------------------------------------------- /src/lib/game/unit.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import Entity from './entity'; 3 | 4 | class Unit extends Entity { 5 | 6 | constructor(guid) { 7 | super(); 8 | 9 | this.guid = guid; 10 | 11 | this.name = ''; 12 | this.level = '?'; 13 | this.target = null; 14 | 15 | this.maxHp = 0; 16 | this.hp = 0; 17 | 18 | this.maxMp = 0; 19 | this.mp = 0; 20 | 21 | this.rotateSpeed = 2; 22 | this.moveSpeed = 40; 23 | 24 | this._view = new THREE.Group(); 25 | 26 | this._displayID = 0; 27 | this._model = null; 28 | } 29 | 30 | get position() { 31 | return this._view.position; 32 | } 33 | 34 | get displayID() { 35 | return this._displayID; 36 | } 37 | 38 | set displayID(displayID) { 39 | if (!displayID) { 40 | return; 41 | } 42 | } 43 | 44 | get view() { 45 | return this._view; 46 | } 47 | 48 | get model() { 49 | return this._model; 50 | } 51 | 52 | set model(m2) { 53 | // TODO: Should this support multiple models? Mounts? 54 | if (this._model) { 55 | this.view.remove(this._model); 56 | } 57 | 58 | // TODO: Figure out whether this 180 degree rotation is correct 59 | m2.rotation.z = Math.PI; 60 | m2.updateMatrix(); 61 | 62 | this.view.add(m2); 63 | 64 | // Auto-play animation index 0 in unit model, if present 65 | // TODO: Properly manage unit animations 66 | if (m2.animated && m2.animations.length > 0) { 67 | m2.animations.playAnimation(0); 68 | m2.animations.playAllSequences(); 69 | } 70 | 71 | this.emit('model:change', this, this._model, m2); 72 | this._model = m2; 73 | } 74 | 75 | ascend(delta) { 76 | this.view.translateZ(this.moveSpeed * delta); 77 | this.emit('position:change', this); 78 | } 79 | 80 | descend(delta) { 81 | this.view.translateZ(-this.moveSpeed * delta); 82 | this.emit('position:change', this); 83 | } 84 | 85 | moveForward(delta) { 86 | this.view.translateX(this.moveSpeed * delta); 87 | this.emit('position:change', this); 88 | } 89 | 90 | moveBackward(delta) { 91 | this.view.translateX(-this.moveSpeed * delta); 92 | this.emit('position:change', this); 93 | } 94 | 95 | rotateLeft(delta) { 96 | this.view.rotateZ(this.rotateSpeed * delta); 97 | this.emit('position:change', this); 98 | } 99 | 100 | rotateRight(delta) { 101 | this.view.rotateZ(-this.rotateSpeed * delta); 102 | this.emit('position:change', this); 103 | } 104 | 105 | strafeLeft(delta) { 106 | this.view.translateY(this.moveSpeed * delta); 107 | this.emit('position:change', this); 108 | } 109 | 110 | strafeRight(delta) { 111 | this.view.translateY(-this.moveSpeed * delta); 112 | this.emit('position:change', this); 113 | } 114 | 115 | } 116 | 117 | export default Unit; 118 | -------------------------------------------------------------------------------- /src/lib/game/world/handler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import THREE from 'three'; 3 | 4 | class WorldHandler extends EventEmitter { 5 | 6 | constructor(session) { 7 | super(); 8 | this.session = session; 9 | this.player = this.session.player; 10 | 11 | this.scene = new THREE.Scene(); 12 | this.scene.matrixAutoUpdate = false; 13 | 14 | this.map = null; 15 | 16 | this.changeMap = ::this.changeMap; 17 | this.changeModel = ::this.changeModel; 18 | this.changePosition = ::this.changePosition; 19 | 20 | this.entities = new Set(); 21 | this.add(this.player); 22 | 23 | this.player.on('map:change', this.changeMap); 24 | this.player.on('position:change', this.changePosition); 25 | 26 | // Darkshire (Eastern Kingdoms) 27 | this.player.worldport(0, -10559, -1189, 28); 28 | 29 | // Booty Bay (Eastern Kingdoms) 30 | // this.player.worldport(0, -14354, 518, 22); 31 | 32 | // Stonewrought Dam (Eastern Kingdoms) 33 | // this.player.worldport(0, -4651, -3316, 296); 34 | 35 | // Ironforge (Eastern Kingdoms) 36 | // this.player.worldport(0, -4981.25, -881.542, 502.66); 37 | 38 | // Darnassus (Kalimdor) 39 | // this.player.worldport(1, 9947, 2557, 1316); 40 | 41 | // Astranaar (Kalimdor) 42 | // this.player.worldport(1, 2752, -348, 107); 43 | 44 | // Moonglade (Kalimdor) 45 | // this.player.worldport(1, 7827, -2425, 489); 46 | 47 | // Un'Goro Crater (Kalimdor) 48 | // this.player.worldport(1, -7183, -1394, -183); 49 | 50 | // Everlook (Kalimdor) 51 | // this.player.worldport(1, 6721.44, -4659.09, 721.893); 52 | 53 | // Stonetalon Mountains (Kalimdor) 54 | // this.player.worldport(1, 2506.3, 1470.14, 263.722); 55 | 56 | // Mulgore (Kalimdor) 57 | // this.player.worldport(1, -1828.913, -426.307, 6.299); 58 | 59 | // Thunderbluff (Kalimdor) 60 | // this.player.worldport(1, -1315.901, 138.6357, 302.008); 61 | 62 | // Auberdine (Kalimdor) 63 | // this.player.worldport(1, 6355.151, 508.831, 15.859); 64 | 65 | // The Exodar (Expansion 01) 66 | // this.player.worldport(530, -4013, -11894, -2); 67 | 68 | // Nagrand (Expansion 01) 69 | // this.player.worldport(530, -743.149, 8385.114, 33.435); 70 | 71 | // Eversong Woods (Expansion 01) 72 | // this.player.worldport(530, 9152.441, -7442.229, 68.144); 73 | 74 | // Daggercap Bay (Northrend) 75 | // this.player.worldport(571, 1031, -5192, 180); 76 | 77 | // Dalaran (Northrend) 78 | // this.player.worldport(571, 5797, 629, 647); 79 | } 80 | 81 | add(entity) { 82 | this.entities.add(entity); 83 | if (entity.view) { 84 | this.scene.add(entity.view); 85 | entity.on('model:change', this.changeModel); 86 | } 87 | } 88 | 89 | remove(entity) { 90 | this.entity.delete(entity); 91 | if (entity.view) { 92 | this.scene.remove(entity.view); 93 | entity.removeListener('model:change', this.changeModel); 94 | } 95 | } 96 | 97 | renderAtCoords(x, y) { 98 | if (!this.map) { 99 | return; 100 | } 101 | this.map.render(x, y); 102 | } 103 | 104 | changeMap(mapID) { 105 | // WorldMap.load(mapID).then((map) => { 106 | // if (this.map) { 107 | // this.scene.remove(this.map); 108 | // } 109 | // this.map = map; 110 | // this.scene.add(this.map); 111 | // this.renderAtCoords(this.player.position.x, this.player.position.y); 112 | // }); 113 | } 114 | 115 | changeModel(_unit, _oldModel, _newModel) { 116 | } 117 | 118 | changePosition(player) { 119 | this.renderAtCoords(player.position.x, player.position.y); 120 | } 121 | 122 | animate(delta, camera, cameraMoved) { 123 | this.animateEntities(delta, camera, cameraMoved); 124 | 125 | if (this.map !== null) { 126 | this.map.animate(delta, camera, cameraMoved); 127 | } 128 | } 129 | 130 | animateEntities(delta, camera, cameraMoved) { 131 | this.entities.forEach((entity) => { 132 | const { model } = entity; 133 | 134 | if (model === null || !model.animated) { 135 | return; 136 | } 137 | 138 | if (model.receivesAnimationUpdates && model.animations.length > 0) { 139 | model.animations.update(delta); 140 | } 141 | 142 | if (cameraMoved && model.billboards.length > 0) { 143 | model.applyBillboards(camera); 144 | } 145 | 146 | if (model.skeletonHelper) { 147 | model.skeletonHelper.update(); 148 | } 149 | }); 150 | } 151 | 152 | } 153 | 154 | export default WorldHandler; 155 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import AuthHandler from './auth/handler'; 4 | import CharactersHandler from './characters/handler'; 5 | import ChatHandler from './game/chat/handler'; 6 | import Config from './config'; 7 | import GameHandler from './game/handler'; 8 | import RealmsHandler from './realms/handler'; 9 | import WorldHandler from './game/world/handler'; 10 | 11 | class Client extends EventEmitter { 12 | 13 | constructor(config) { 14 | super(); 15 | 16 | this.config = config || new Config(); 17 | this.auth = new AuthHandler(this); 18 | this.realms = new RealmsHandler(this); 19 | this.game = new GameHandler(this); 20 | this.characters = new CharactersHandler(this); 21 | this.chat = new ChatHandler(this); 22 | this.world = new WorldHandler(this); 23 | } 24 | 25 | } 26 | 27 | export default Client; 28 | -------------------------------------------------------------------------------- /src/lib/net/loader.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | 3 | class Loader { 4 | 5 | constructor() { 6 | this.prefix = this.prefix || '/pipeline/'; 7 | this.responseType = this.responseType || 'arraybuffer'; 8 | } 9 | 10 | load(path) { 11 | return new Promise((resolve, _reject) => { 12 | const uri = `${this.prefix}${path}`; 13 | 14 | const xhr = new XMLHttpRequest(); 15 | xhr.open('GET', encodeURI(uri), true); 16 | 17 | xhr.onload = function(_event) { 18 | // TODO: Handle failure 19 | if (this.status >= 200 && this.status < 400) { 20 | resolve(this.response); 21 | } 22 | }; 23 | 24 | xhr.responseType = this.responseType; 25 | xhr.send(); 26 | }); 27 | } 28 | 29 | } 30 | 31 | export default Loader; 32 | -------------------------------------------------------------------------------- /src/lib/net/packet.js: -------------------------------------------------------------------------------- 1 | import ByteBuffer from 'byte-buffer'; 2 | 3 | class Packet extends ByteBuffer { 4 | 5 | // Creates a new packet with given opcode from given source or length 6 | constructor(opcode, source, outgoing = true) { 7 | super(source, ByteBuffer.LITTLE_ENDIAN); 8 | 9 | // Holds the opcode for this packet 10 | this.opcode = opcode; 11 | 12 | // Whether this packet is outgoing or incoming 13 | this.outgoing = outgoing; 14 | 15 | // Seek past opcode to reserve space for it when finalizing 16 | this.index = this.headerSize; 17 | } 18 | 19 | // Header size in bytes 20 | get headerSize() { 21 | return this.constructor.HEADER_SIZE; 22 | } 23 | 24 | // Body size in bytes 25 | get bodySize() { 26 | return this.length - this.headerSize; 27 | } 28 | 29 | // Retrieves the name of the opcode for this packet (if available) 30 | get opcodeName() { 31 | return null; 32 | } 33 | 34 | // Short string representation of this packet 35 | toString() { 36 | const opcode = ('0000' + this.opcode.toString(16).toUpperCase()).slice(-4); 37 | return `[${this.constructor.name}; Opcode: ${this.opcodeName || 'UNKNOWN'} (0x${opcode}); Length: ${this.length}; Body: ${this.bodySize}; Index: ${this._index}]`; 38 | } 39 | 40 | // Finalizes this packet 41 | finalize() { 42 | return this; 43 | } 44 | 45 | } 46 | 47 | export default Packet; 48 | -------------------------------------------------------------------------------- /src/lib/net/socket.js: -------------------------------------------------------------------------------- 1 | import ByteBuffer from 'byte-buffer'; 2 | import EventEmitter from 'events'; 3 | 4 | // Base-class for any socket including signals and host/port management 5 | class Socket extends EventEmitter { 6 | 7 | // Maximum buffer capacity 8 | // TODO: Arbitrarily chosen, determine this cap properly 9 | static BUFFER_CAP = 2048; 10 | 11 | // Creates a new socket 12 | constructor() { 13 | super(); 14 | 15 | // Holds the host, port and uri currently connected to (if any) 16 | this.host = null; 17 | this.port = NaN; 18 | this.uri = null; 19 | 20 | // Holds the actual socket 21 | this.socket = null; 22 | 23 | // Holds buffered data 24 | this.buffer = null; 25 | 26 | // Holds incoming packet's remaining size in bytes (false if no packet is being handled) 27 | this.remaining = false; 28 | } 29 | 30 | // Whether this socket is currently connected 31 | get connected() { 32 | return this.socket && this.socket.readyState === WebSocket.OPEN; 33 | } 34 | 35 | // Connects to given host through given port (if any; default port is implementation specific) 36 | connect(host, port = NaN) { 37 | if (!this.connected) { 38 | var that = this; 39 | 40 | this.host = host; 41 | this.port = port; 42 | this.uri = 'ws://' + this.host + ':' + this.port; 43 | 44 | this.buffer = new ByteBuffer(0, ByteBuffer.LITTLE_ENDIAN); 45 | this.remaining = false; 46 | 47 | this.socket = new WebSocket(this.uri, 'binary'); 48 | this.socket.binaryType = 'arraybuffer'; 49 | 50 | this.socket.onopen = (e) => { 51 | this.emit('connect', e); 52 | }; 53 | 54 | this.socket.onclose = (e) => { 55 | that.disconnect(); 56 | }; 57 | 58 | this.socket.onmessage = (e) => { 59 | const index = this.buffer.index; 60 | this.buffer.end().append(e.data.byteLength).write(e.data); 61 | this.buffer.index = index; 62 | 63 | this.emit('data:receive', this); 64 | 65 | if (this.buffer.available === 0 && this.buffer.length > this.constructor.BUFFER_CAP) { 66 | this.buffer.clip(); 67 | } 68 | }; 69 | 70 | this.socket.onerror = function(e) { 71 | console.error(e); 72 | }; 73 | } 74 | 75 | return this; 76 | } 77 | 78 | // Attempts to reconnect to cached host and port 79 | reconnect() { 80 | if (!this.connected && this.host && this.port) { 81 | this.connect(this.host, this.port); 82 | } 83 | return this; 84 | } 85 | 86 | // Disconnects this socket 87 | disconnect() { 88 | if (this.connected) { 89 | this.socket.close(); 90 | } 91 | 92 | alert("You have been disconnected"); 93 | location.reload(); 94 | 95 | return this; 96 | } 97 | 98 | // Finalizes and sends given packet 99 | send(packet) { 100 | if (this.connected) { 101 | 102 | packet.finalize(); 103 | 104 | console.log('⟸', packet.toString()); 105 | // console.debug packet.toHex() 106 | // console.debug packet.toASCII() 107 | 108 | this.socket.send(packet.buffer); 109 | 110 | this.emit('packet:send', packet); 111 | 112 | return true; 113 | } 114 | 115 | return false; 116 | } 117 | 118 | } 119 | 120 | export default Socket; 121 | -------------------------------------------------------------------------------- /src/lib/realms/handler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | import AuthOpcode from '../auth/opcode'; 4 | import AuthPacket from '../auth/packet'; 5 | import Realm from './realm'; 6 | 7 | class RealmsHandler extends EventEmitter { 8 | 9 | // Creates a new realm handler 10 | constructor(session) { 11 | super(); 12 | 13 | // Holds session 14 | this.session = session; 15 | 16 | // Initially empty list of realms 17 | this.list = []; 18 | 19 | // Listen for realm list 20 | this.session.auth.on('packet:receive:REALM_LIST', ::this.handleRealmList); 21 | } 22 | 23 | // Requests a fresh list of realms 24 | refresh() { 25 | console.info('refreshing realmlist'); 26 | 27 | const ap = new AuthPacket(AuthOpcode.REALM_LIST, 1 + 4); 28 | 29 | // Per WoWDev, the opcode is followed by an unknown uint32 30 | ap.writeUnsignedInt(0x00); 31 | 32 | return this.session.auth.send(ap); 33 | } 34 | 35 | // Realm list refresh handler (REALM_LIST) 36 | handleRealmList(ap) { 37 | ap.readShort(); // packet-size 38 | ap.readUnsignedInt(); // (?) 39 | 40 | const count = ap.readShort(); // number of realms 41 | 42 | this.list.length = 0; 43 | 44 | for (let i = 0; i < count; ++i) { 45 | const realm = new Realm(); 46 | 47 | realm.icon = ap.readUnsignedByte(); 48 | realm.lock = ap.readUnsignedByte(); 49 | realm.flags = ap.readUnsignedByte(); 50 | realm.name = ap.readCString(); 51 | realm.address = ap.readCString(); 52 | realm.population = ap.readFloat(); 53 | realm.characters = ap.readUnsignedByte(); 54 | realm.timezone = ap.readUnsignedByte(); 55 | realm.id = ap.readUnsignedByte(); 56 | realm.ord = i; 57 | 58 | // TODO: Introduce magic constants such as REALM_FLAG_SPECIFYBUILD 59 | if (realm.flags & 0x04) { 60 | realm.majorVersion = ap.readUnsignedByte(); 61 | realm.minorVersion = ap.readUnsignedByte(); 62 | realm.patchVersion = ap.readUnsignedByte(); 63 | realm.build = ap.readUnsignedShort(); 64 | } 65 | 66 | this.list.push(realm); 67 | } 68 | 69 | this.emit('refresh'); 70 | } 71 | 72 | } 73 | 74 | export default RealmsHandler; 75 | -------------------------------------------------------------------------------- /src/lib/realms/realm.js: -------------------------------------------------------------------------------- 1 | class Realm { 2 | 3 | // Creates a new realm 4 | constructor() { 5 | 6 | // Holds host, port and address 7 | this._host = null; 8 | this._port = NaN; 9 | this._address = null; 10 | 11 | // Holds realm attributes 12 | this.name = null; 13 | this.id = null; 14 | this.icon = null; 15 | this.flags = null; 16 | this.timezone = null; 17 | this.population = 0.0; 18 | this.characters = 0; 19 | 20 | this.majorVersion = null; 21 | this.minorVersion = null; 22 | this.patchVersion = null; 23 | this.build = null; 24 | } 25 | 26 | // Short string representation of this realm 27 | toString() { 28 | return `[Realm; Name: ${this.name}; Address: ${this._address}; Characters: ${this.characters}]`; 29 | } 30 | 31 | // Retrieves host for this realm 32 | get host() { 33 | return this._host; 34 | } 35 | 36 | // Retrieves port for this realm 37 | get port() { 38 | return this._port; 39 | } 40 | 41 | // Retrieves address for this realm 42 | get address() { 43 | return this._address; 44 | } 45 | 46 | // Sets address for this realm 47 | set address(address) { 48 | this._address = address; 49 | const parts = this._address.split(':'); 50 | this._host = parts[0] || null; 51 | this._port = parts[1] || NaN; 52 | } 53 | 54 | } 55 | 56 | export default Realm; 57 | -------------------------------------------------------------------------------- /src/lib/utils/array-util.js: -------------------------------------------------------------------------------- 1 | class ArrayUtil { 2 | 3 | // Generates array from given hex string 4 | static fromHex(hex) { 5 | const array = []; 6 | for (let i = 0; i < hex.length; i += 2) { 7 | array.push(parseInt(hex.slice(i, i + 2), 16)); 8 | } 9 | return array; 10 | } 11 | 12 | } 13 | 14 | export default ArrayUtil; 15 | -------------------------------------------------------------------------------- /src/lib/utils/object-util.js: -------------------------------------------------------------------------------- 1 | class ObjectUtil { 2 | 3 | // Retrieves key for given value (if any) in object 4 | static keyByValue(object, target) { 5 | if (!('lookup' in object)) { 6 | const lookup = {}; 7 | for (const key in object) { 8 | if (object.hasOwnProperty(key)) { 9 | const value = object[key]; 10 | lookup[value] = key; 11 | } 12 | } 13 | object.lookup = lookup; 14 | } 15 | 16 | return object.lookup[target]; 17 | } 18 | 19 | } 20 | 21 | export default ObjectUtil; 22 | -------------------------------------------------------------------------------- /src/spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/spec/sample-spec.js: -------------------------------------------------------------------------------- 1 | import {} from './spec-helper'; 2 | 3 | describe('Wowser', function() { 4 | 5 | xit('will have specs (hopefully)'); 6 | 7 | }); 8 | -------------------------------------------------------------------------------- /src/spec/spec-helper.js: -------------------------------------------------------------------------------- 1 | import bridge from 'sinon-chai'; 2 | import chai from 'chai'; 3 | import sinon from 'sinon'; 4 | 5 | chai.use(bridge); 6 | 7 | beforeEach(function() { 8 | this.sandbox = sinon.sandbox.create(); 9 | }); 10 | 11 | afterEach(function() { 12 | this.sandbox.restore(); 13 | }); 14 | 15 | export const expect = chai.expect; 16 | export sinon from 'sinon'; 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | context: path.join(__dirname, 'src'), 6 | entry: './bootstrapper', 7 | output: { 8 | path: path.join(__dirname, 'public'), 9 | filename: 'wowser-[hash].js' 10 | }, 11 | resolve: { 12 | extensions: ['', '.js', '.jsx'] 13 | }, 14 | resolveLoader: { 15 | root: path.join(__dirname, 'node_modules') 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.json$/, 21 | loader: 'json-loader' 22 | }, 23 | { 24 | test: /\.(png|jpg)$/, 25 | loader: 'url-loader?limit=100000' 26 | }, 27 | { 28 | test: /\.styl$/, 29 | loader: 'style-loader!css-loader!stylus-loader?resolve url', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.(frag|vert|glsl)$/, 34 | loader: 'raw-loader!glslify-loader?transform[]=glslify-import', 35 | exclude: /node_modules/ 36 | }, 37 | { 38 | test: /\.jsx?$/, 39 | loader: 'babel-loader', 40 | exclude: /node_modules|blizzardry/ 41 | }, 42 | // { 43 | // test: /\.jsx?$/, 44 | // loader: 'eslint-loader', 45 | // exclude: /node_modules|blizzardry/ 46 | // } 47 | ] 48 | }, 49 | plugins: [ 50 | new HtmlWebpackPlugin({ 51 | hash: true, 52 | inject: true, 53 | template: 'index.html' 54 | }) 55 | ], 56 | devServer: { 57 | contentBase: path.join(__dirname, 'public'), 58 | // proxy: { 59 | // '/pipeline/*': { 60 | // target: 'http://localhost:3000', 61 | // secure: false 62 | // } 63 | // } 64 | } 65 | }; 66 | --------------------------------------------------------------------------------