Showing preview only (236K chars total). Download the full file or copy to clipboard to get everything.
Repository: wowserhq/wowser
Branch: master
Commit: 5fcd3e607db7
Files: 126
Total size: 207.4 KB
Directory structure:
gitextract_5ysnh6kx/
├── .babelrc
├── .codeclimate.yml
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .istanbul.yml
├── .travis.yml
├── AUTHORS
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│ └── serve
├── gulpfile.babel.js
├── package.json
├── src/
│ ├── bootstrapper.jsx
│ ├── components/
│ │ ├── auth/
│ │ │ ├── index.jsx
│ │ │ └── index.styl
│ │ ├── characters/
│ │ │ └── index.jsx
│ │ ├── game/
│ │ │ ├── chat/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.styl
│ │ │ ├── controls.jsx
│ │ │ ├── hud/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.styl
│ │ │ ├── index.jsx
│ │ │ ├── index.styl
│ │ │ ├── portrait/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.styl
│ │ │ ├── quests/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.styl
│ │ │ └── stats/
│ │ │ ├── index.jsx
│ │ │ └── index.styl
│ │ ├── kit/
│ │ │ └── index.jsx
│ │ ├── realms/
│ │ │ └── index.jsx
│ │ └── wowser/
│ │ ├── index.jsx
│ │ ├── index.styl
│ │ ├── session.jsx
│ │ └── ui/
│ │ ├── form/
│ │ │ └── index.styl
│ │ ├── frame/
│ │ │ ├── dividers/
│ │ │ │ └── index.styl
│ │ │ └── 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/
│ │ │ │ └── sha1.js
│ │ │ ├── hash.js
│ │ │ └── srp.js
│ │ ├── game/
│ │ │ ├── chat/
│ │ │ │ ├── handler.js
│ │ │ │ └── message.js
│ │ │ ├── entity.js
│ │ │ ├── guid.js
│ │ │ ├── handler.js
│ │ │ ├── opcode.js
│ │ │ ├── packet.js
│ │ │ ├── player.js
│ │ │ ├── unit.js
│ │ │ └── world/
│ │ │ ├── content-queue.js
│ │ │ ├── doodad-manager.js
│ │ │ ├── handler.js
│ │ │ ├── map.js
│ │ │ ├── terrain-manager.js
│ │ │ └── wmo-manager/
│ │ │ ├── index.js
│ │ │ └── wmo-handler.js
│ │ ├── index.js
│ │ ├── net/
│ │ │ ├── loader.js
│ │ │ ├── packet.js
│ │ │ └── socket.js
│ │ ├── pipeline/
│ │ │ ├── adt/
│ │ │ │ ├── chunk/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── material.js
│ │ │ │ │ ├── shader.frag
│ │ │ │ │ └── shader.vert
│ │ │ │ ├── index.js
│ │ │ │ └── loader.js
│ │ │ ├── dbc/
│ │ │ │ ├── index.js
│ │ │ │ └── loader.js
│ │ │ ├── m2/
│ │ │ │ ├── animation-manager.js
│ │ │ │ ├── batch-manager.js
│ │ │ │ ├── blueprint.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader.js
│ │ │ │ ├── material/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── shader.frag
│ │ │ │ │ └── shader.vert
│ │ │ │ └── submesh.js
│ │ │ ├── material.js
│ │ │ ├── texture-loader.js
│ │ │ ├── wdt/
│ │ │ │ ├── index.js
│ │ │ │ └── loader.js
│ │ │ ├── wmo/
│ │ │ │ ├── blueprint.js
│ │ │ │ ├── group/
│ │ │ │ │ ├── blueprint.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── loader.js
│ │ │ │ ├── index.js
│ │ │ │ ├── loader.js
│ │ │ │ └── material/
│ │ │ │ ├── index.js
│ │ │ │ ├── shader.frag
│ │ │ │ └── shader.vert
│ │ │ └── worker/
│ │ │ ├── index.js
│ │ │ ├── pool.js
│ │ │ ├── task.js
│ │ │ └── thread.js
│ │ ├── realms/
│ │ │ ├── handler.js
│ │ │ └── realm.js
│ │ ├── server/
│ │ │ ├── .babelrc
│ │ │ ├── cluster.js
│ │ │ ├── config/
│ │ │ │ ├── index.js
│ │ │ │ └── setup-prompts.js
│ │ │ ├── index.js
│ │ │ └── pipeline/
│ │ │ ├── archive.js
│ │ │ └── index.js
│ │ └── utils/
│ │ ├── array-util.js
│ │ └── object-util.js
│ └── spec/
│ ├── .eslintrc
│ ├── sample-spec.js
│ └── spec-helper.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"plugins": [
"transform-class-properties",
"transform-export-extensions",
"transform-function-bind",
"transform-es2015-block-scoping",
"add-module-exports",
"transform-es2015-modules-commonjs"
],
"presets": [
"react"
]
}
================================================
FILE: .codeclimate.yml
================================================
languages:
JavaScript: true
exclude_paths:
- 'lib/*'
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintrc
================================================
{
"extends": "timkurvers/react",
"rules": {
// Allow vertically aligning values
"no-multi-spaces": [0],
// See: https://github.com/yannickcr/eslint-plugin-react/issues/128
"react/sort-comp": [0],
// Disable newer additions originating from Airbnb
"arrow-body-style": [0],
"prefer-arrow-callback": [0],
"space-before-function-paren": [0],
"react/jsx-indent-props": [0],
"react/jsx-closing-bracket-location": [0]
}
}
================================================
FILE: .gitignore
================================================
/coverage/
/lib/
/node_modules/
/public/scripts/
/public/styles/
/public/templates/
/spec/
================================================
FILE: .istanbul.yml
================================================
instrumentation:
excludes: ['public/**', 'src/**', 'bundle.js', 'gulpfile.babel.js']
include-all-sources: true
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
node_js:
- '4'
- '5'
- '6'
matrix:
fast_finish: true
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
env:
- CXX=g++-4.8
script: npm test --coverage
after_script:
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
================================================
FILE: AUTHORS
================================================
fallenoak (https://github.com/fallenoak)
timkurvers (https://github.com/timkurvers)
================================================
FILE: CHANGELOG.md
================================================
# Changelog
### v0.0.2 - March 20, 2020
- Ensure package on `npm` contains current project status.
### v0.0.1 - November 13, 2014
- Initial release.
### v0.0.0 - November 1, 2014
- Placeholder release.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2012-2018 Wowser Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Wowser
[](https://www.npmjs.org/package/wowser)
[](https://discord.gg/DeVVKVg)
[](https://travis-ci.org/wowserhq/wowser)
[](https://snyk.io/test/github/wowserhq/wowser)
[](https://codeclimate.com/github/wowserhq/wowser/maintainability)
[](https://codeclimate.com/github/wowserhq/wowser/test_coverage)
World of Warcraft in the browser using JavaScript and WebGL.
Licensed under the [**MIT** license](LICENSE).
[](https://www.youtube.com/watch?v=BrnbANSwC4I)
## Status
Wowser is in the process of being split up into (at minimum) the following parts:
- [Client](https://github.com/wowserhq/client/) (user interface loaded from XML/LUA)
- [Pipeline](https://github.com/wowserhq/pipeline) server (serves up resources from the official client)
This repository will in the future become an umbrella package.
## Background
Wowser is a proof-of-concept of getting a triple-A game to run in a webbrowser,
attempting to tackle a wide variety of challenges: data retrieval, socket
connections, cryptography, 3d graphics, binary data handling, background workers
and audio, to name a few.
## Features
Wowser is aiming to be both a low-level API as well as a graphical client,
interacting with a World of Warcraft server like an official client would.
**Note:** Only Wrath of the Lich King (3.3.5a) is currently supported. A copy of
the official client is required.
**Warning:** Do not attempt to use this client on official/retail servers as
your account may get banned.
At present, Wowser is capable of:
- Authenticating by username / password.
- Listing available realms.
- Connecting to a realm.
- Listing characters available on a realm.
- Joining the game world with a character.
- Logging game world packets, such as when a creature moves in the vicinity.
In addition, there's good progress on getting terrain and models rendered.
## Browser Support
Wowser is presumed to be working on any browser supporting [JavaScript's typed
arrays] and at the very least a binary version of the WebSocket protocol.
## Development
Wowser is written in [ES2015], developed with [webpack] and [Gulp], compiled by
[Babel] and [soon™] to be tested through [Mocha].
1. Clone the repository:
```shell
git clone git://github.com/wowserhq/wowser.git
```
2. Download and install [Node.js] – including `npm` – for your platform.
3. Install dependencies:
```shell
npm install
```
4. Install [StormLib] and [BLPConverter], which are used to handle Blizzard's
game files.
### Client
[Webpack]'s development server monitors source files and builds:
```shell
npm run web-dev
```
Wowser will be served on `http://localhost:8080`.
### Pipeline server
To deliver game resources to its client, Wowser ships with a pipeline.
Build the pipeline:
```shell
npm run gulp
```
Keep this process running to monitor source files and automatically rebuild.
After building, serve the pipeline as follows in a separate process:
```shell
npm run serve
```
On first run you will be prompted to specify the following:
- Path to client data folder (e.g. `C:/Program Files (x86)/World of Warcraft/Data`)
- Server port (default is `3000`)
- Number of cluster workers (default depends on amount of CPUs)
Clear these settings by running `npm run reset`
**Disclaimer:** Wowser serves up resources to the browser over HTTP. Depending
on your network configuration these may be available to others. Respect laws and
do not distribute game data you do not own.
### Socket proxies
To utilize raw TCP connections a WebSocket proxy is required for JavaScript
clients.
[Websockify] can - among other things - act as a proxy for raw TCP sockets.
For now, you will want to proxy both port 3724 (auth) and 8129 (world). Use a
different set of ports if the game server is on the same machine as your client.
```shell
npm run proxy 3724 host:3724
npm run proxy 8129 host:8129
```
## Contribution
When contributing, please:
- Fork the repository
- Open a pull request (preferably on a separate branch)
[Babel]: https://babeljs.io/
[BLPConverter]: https://github.com/wowserhq/blizzardry#blp
[ES2015]: https://babeljs.io/docs/learn-es2015/
[Gulp]: http://gulpjs.com/
[JavaScript's typed arrays]: http://caniuse.com/#search=typed%20arrays
[Mocha]: http://mochajs.org/
[Node.js]: http://nodejs.org/#download
[StormLib]: https://github.com/wowserhq/blizzardry#mpq
[Websockify]: https://github.com/kanaka/websockify/
[soon™]: http://www.wowwiki.com/Soon
[webpack]: http://webpack.github.io/
================================================
FILE: bin/serve
================================================
#!/usr/bin/env node
const Cluster = require('../lib/server/cluster');
const ServerConfig = require('../lib/server/config');
ServerConfig.verify().then(function() {
const cluster = new Cluster();
cluster.start();
});
================================================
FILE: gulpfile.babel.js
================================================
import Config from 'configstore';
import babel from 'gulp-babel';
import cache from 'gulp-cached';
import del from 'del';
import gulp from 'gulp';
import mocha from 'gulp-mocha';
import pkg from './package.json';
import plumber from 'gulp-plumber';
const config = {
db: new Config(pkg.name),
scripts: 'src/**/*.js',
specs: 'spec/**/*.js'
};
gulp.task('reset', function(done) {
config.db.clear();
process.stdout.write(`\n> Settings deleted from ${config.db.path}\n\n`);
done();
});
gulp.task('clean', function(cb) {
del([
'lib/*',
'spec/*'
], cb);
});
gulp.task('scripts', function() {
return gulp.src(config.scripts)
.pipe(cache('babel'))
.pipe(plumber())
.pipe(babel())
.pipe(gulp.dest('.'));
});
gulp.task('spec', function() {
return gulp.src(config.specs, { read: false })
.pipe(plumber())
.pipe(mocha());
});
gulp.task('rebuild', gulp.series(
'clean', 'scripts'
));
gulp.task('watch', function(done) {
gulp.watch(config.scripts, gulp.series('scripts', 'spec'));
done();
});
gulp.task('default', gulp.series(
'rebuild', 'spec', 'watch'
));
================================================
FILE: package.json
================================================
{
"name": "wowser",
"version": "0.0.2",
"description": "World of Warcraft in the browser using JavaScript and WebGL",
"author": "Wowser Contributors",
"repository": "wowserhq/wowser",
"license": "MIT",
"main": "lib/client/index.js",
"files": [
"AUTHORS",
"CHANGELOG.md",
"LICENSE",
"README.md"
],
"scripts": {
"gulp": "gulp",
"lint": "eslint src *.js --ext .js --ext .jsx; exit 0",
"proxy": "websockify",
"start": "node bin/serve",
"serve": "node bin/serve",
"serve-dev": "nodemon bin/serve -w lib/server",
"pretest": "gulp rebuild",
"reset": "gulp reset",
"test": "istanbul test ./node_modules/mocha/bin/_mocha -- spec --recursive",
"web-dev": "webpack-dev-server",
"web-release": "webpack --optimize --progress"
},
"keywords": [
"world of warcraft",
"warcraft",
"blizzard",
"wow"
],
"dependencies": {
"array-find": "^0.1.1",
"blizzardry": "^0.4.0",
"bluebird": "^2.10.0",
"byte-buffer": "^1.0.3",
"classnames": "^2.2.0",
"configstore": "^1.2.0",
"deep-equal": "^1.0.0",
"express": "^4.9.3",
"globby": "^5.0.0",
"inquirer": "^0.8.5",
"jsbn": "timkurvers/jsbn.git#wowser",
"keymaster": "^1.6.2",
"morgan": "^1.3.2",
"normalize.css": "^3.0.3",
"pngjs": "^2.3.0",
"react": "^0.14.3",
"react-dom": "^0.14.3",
"three": "^0.77.0",
"websockify": "^0.7.1"
},
"devDependencies": {
"babel-core": "^6.10.0",
"babel-eslint": "^6.1.0",
"babel-loader": "^6.2.0",
"babel-plugin-transform-class-properties": "^6.10.0",
"babel-plugin-transform-export-extensions": "^6.8.0",
"babel-plugin-transform-function-bind": "^6.8.0",
"babel-plugin-transform-es2015-block-scoping": "^6.10.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.8.0",
"babel-plugin-transform-es2015-parameters": "^6.9.0",
"babel-plugin-add-module-exports": "^0.2.0",
"babel-preset-react": "^6.5.0",
"chai": "^3.5.0",
"css-loader": "^0.23.0",
"del": "^1.2.0",
"eslint": "^2.13.0",
"eslint-config-airbnb": "^6.2.0",
"eslint-config-timkurvers": "^0.2.3",
"eslint-loader": "^1.3.0",
"eslint-plugin-react": "^4.3.0",
"file-loader": "^0.9.0",
"glslify-loader": "wowserhq/glslify-loader#query-opts",
"glslify-import": "^3.0.0",
"gulp": "gulpjs/gulp.git#4.0",
"gulp-babel": "^6.1.0",
"gulp-cached": "^1.1.0",
"gulp-mocha": "2.2.0",
"gulp-plumber": "^1.1.0",
"gulp-remember": "^0.3.0",
"gulp-stylus": "^2.5.0",
"html-webpack-plugin": "^2.21.0",
"istanbul": "^0.4.0",
"json-loader": "^0.5.0",
"mocha": "^2.5.0",
"nodemon": "^1.9.0",
"raw-loader": "^0.5.0",
"sinon": "^1.17.0",
"sinon-chai": "^2.8.0",
"style-loader": "^0.13.0",
"stylus-loader": "^1.6.0",
"url-loader": "^0.5.0",
"webpack": "^1.13.0",
"webpack-dev-server": "^1.14.0",
"worker-loader": "^0.7.0"
}
}
================================================
FILE: src/bootstrapper.jsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import Wowser from './components/wowser';
ReactDOM.render(<Wowser />, document.querySelector('app'));
================================================
FILE: src/components/auth/index.jsx
================================================
import React from 'react';
import session from '../wowser/session';
class AuthScreen extends React.Component {
static id = 'auth';
static title = 'Authentication';
constructor() {
super();
this.state = {
host: window.location.hostname,
port: session.auth.constructor.PORT,
username: '',
password: ''
};
this._onAuthenticate = ::this._onAuthenticate;
this._onChange = ::this._onChange;
this._onSubmit = ::this._onSubmit;
this._onConnect = ::this._onConnect;
session.auth.on('connect', this._onConnect);
session.auth.on('reject', session.auth.disconnect);
session.auth.on('authenticate', this._onAuthenticate);
}
componentWillUnmount() {
session.auth.removeListener('connect', this._onConnect);
session.auth.removeListener('reject', session.auth.disconnect);
session.auth.removeListener('authenticate', this._onAuthenticate);
}
connect(host, port) {
session.auth.connect(host, port);
}
authenticate(username, password) {
session.auth.authenticate(username, password);
}
_onAuthenticate() {
session.screen = 'realms';
}
_onChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
_onConnect() {
this.authenticate(this.state.username, this.state.password);
}
_onSubmit(event) {
event.preventDefault();
this.connect(this.state.host, this.state.port);
}
render() {
return (
<auth className="auth screen">
<div className="panel">
<h1>Authentication</h1>
<div className="divider"></div>
<p>
<strong>Note:</strong> Wowser requires a WebSocket proxy, see the README on GitHub.
</p>
<form onSubmit={ this._onSubmit }>
<fieldset>
<label>Host</label>
<input type="text" onChange={ this._onChange }
name="host" value={ this.state.host } />
<label>Port</label>
<input type="text" onChange={ this._onChange }
name="port" value={ this.state.port } />
</fieldset>
<fieldset>
<label>Username</label>
<input type="text" onChange={ this._onChange }
name="username" value={ this.state.username } autoFocus />
<label>Password</label>
<input type="password" onChange={ this._onChange }
name="password" value={ this.state.password } />
</fieldset>
<div className="divider"></div>
<input type="submit" defaultValue="Connect" />
</form>
</div>
</auth>
);
}
}
export default AuthScreen;
================================================
FILE: src/components/auth/index.styl
================================================
wowser .auth
.panel
max-width: 300px
================================================
FILE: src/components/characters/index.jsx
================================================
import React from 'react';
import session from '../wowser/session';
class CharactersScreen extends React.Component {
static id = 'characters';
static title = 'Character Selection';
constructor() {
super();
this.state = {
character: null,
characters: []
};
this._onCharacterSelect = ::this._onCharacterSelect;
this._onJoin = ::this._onJoin;
this._onRefresh = ::this._onRefresh;
this._onSubmit = ::this._onSubmit;
session.characters.on('refresh', this._onRefresh);
session.game.on('join', this._onJoin);
this.refresh();
}
componentWillUnmount() {
session.characters.removeListener('refresh', this._onRefresh);
session.game.removeListener('join', this._onJoin);
}
join(character) {
session.game.join(character);
}
refresh() {
session.characters.refresh();
}
_onCharacterSelect(event) {
this.setState({ character: event.target.value });
}
_onJoin() {
session.screen = 'game';
}
_onRefresh() {
const characters = session.characters.list;
this.setState({
character: characters[0],
characters: characters
});
}
_onSubmit(event) {
event.preventDefault();
this.join(this.state.character);
}
render() {
return (
<characters className="characters screen">
<div className="panel">
<h1>Character Selection</h1>
<div className="divider"></div>
<p>
At some point this screen will allow managing characters. Soon™
</p>
<form onSubmit={ this._onSubmit }>
<fieldset>
<select value={ this.state.character }
onChange={ this._onCharacterSelect }>
{ this.state.characters.map((character) => {
return (
<option key={ character.guid } value={ character }>
{ character.name }
</option>
);
}) }
</select>
</fieldset>
<div className="divider"></div>
<input type="submit" value="Join world" autoFocus />
<input type="button" value="Refresh" onClick={ this.refresh } />
</form>
</div>
</characters>
);
}
}
export default CharactersScreen;
================================================
FILE: src/components/game/chat/index.jsx
================================================
import React from 'react';
import classes from 'classnames';
import './index.styl';
import session from '../../wowser/session';
class ChatPanel extends React.Component {
constructor() {
super();
this.state = {
text: '',
messages: session.chat.messages
};
this._onChange = ::this._onChange;
this._onMessage = ::this._onMessage;
this._onSubmit = ::this._onSubmit;
session.chat.on('message', this._onMessage);
}
componentDidUpdate() {
this.refs.messages.scrollTop = this.refs.messages.scrollHeight;
}
send(text) {
const message = session.chat.create();
message.text = text;
session.chat.messages.push(message);
}
_onChange(event) {
this.setState({ text: event.target.value });
}
_onMessage() {
this.setState({ messages: session.chat.messages });
}
_onSubmit(event) {
event.preventDefault();
if (this.state.text) {
this.send(this.state.text);
this.setState({ text: '' });
}
}
render() {
return (
<chat className="chat frame">
<ul ref="messages">
{ this.state.messages.map((message, index) => {
const className = classes('message', message.kind);
return (
<li className={ className } key={ index }>
{ message.text }
</li>
);
}) }
</ul>
<form onSubmit={ this._onSubmit }>
<input type="text" onChange={ this._onChange }
name="text" value={ this.state.text } />
</form>
</chat>
);
}
}
export default ChatPanel;
================================================
FILE: src/components/game/chat/index.styl
================================================
wowser .chat
position: absolute
bottom: 0
left: 0
width: 400px
ul
height: 182px
padding: 0
margin: .4em
list-style: none
overflow: auto
.message
font-size: 14px
&.system
color: #FFCC00
&.info
color: #26C9FF
&.error
color: #FF0000
&.channel
color: #FFB872
&.whisper
color: #FF72FF
&.guild
color: #2CB200
form
margin: 2px 5px
input
width: 100%
================================================
FILE: src/components/game/controls.jsx
================================================
import React from 'react';
import THREE from 'three';
import key from 'keymaster';
class Controls extends React.Component {
static propTypes = {
camera: React.PropTypes.object.isRequired,
for: React.PropTypes.object.isRequired
};
constructor(props) {
super();
this.element = document.body;
this.unit = props.for;
this.camera = props.camera;
// Based on THREE's OrbitControls
// See: http://threejs.org/examples/js/controls/OrbitControls.js
this.clock = new THREE.Clock();
this.rotateStart = new THREE.Vector2();
this.rotateEnd = new THREE.Vector2();
this.rotateDelta = new THREE.Vector2();
this.rotating = false;
this.rotateSpeed = 1.0;
this.offset = new THREE.Vector3(-10, 0, 10);
this.target = new THREE.Vector3();
this.phi = this.phiDelta = 0;
this.theta = this.thetaDelta = 0;
this.scale = 1;
this.zoomSpeed = 1.0;
this.zoomScale = Math.pow(0.95, this.zoomSpeed);
// Zoom distance limits
this.minDistance = 6;
this.maxDistance = 500;
// Vertical orbit limits
this.minPhi = 0;
this.maxPhi = Math.PI * 0.45;
this.quat = new THREE.Quaternion().setFromUnitVectors(
this.camera.up, new THREE.Vector3(0, 1, 0)
);
this.quatInverse = this.quat.clone().inverse();
this.EPS = 0.000001;
this._onMouseDown = ::this._onMouseDown;
this._onMouseUp = ::this._onMouseUp;
this._onMouseMove = ::this._onMouseMove;
this._onMouseWheel = ::this._onMouseWheel;
this.element.addEventListener('mousedown', this._onMouseDown);
this.element.addEventListener('mouseup', this._onMouseUp);
this.element.addEventListener('mousemove', this._onMouseMove);
this.element.addEventListener('mousewheel', this._onMouseWheel);
// Firefox scroll-wheel support
this.element.addEventListener('DOMMouseScroll', this._onMouseWheel);
this.update();
}
componentWillUnmount() {
this.element.removeEventListener('mousedown', this._onMouseDown);
this.element.removeEventListener('mouseup', this._onMouseUp);
this.element.removeEventListener('mousemove', this._onMouseMove);
this.element.removeEventListener('mousewheel', this._onMouseWheel);
this.element.removeEventListener('DOMMouseScroll', this._onMouseWheel);
}
update() {
const unit = this.unit;
// TODO: Get rid of this delta retrieval call
const delta = this.clock.getDelta();
if (this.unit) {
if (key.isPressed('up') || key.isPressed('w')) {
unit.moveForward(delta);
}
if (key.isPressed('down') || key.isPressed('s')) {
unit.moveBackward(delta);
}
if (key.isPressed('q')) {
unit.strafeLeft(delta);
}
if (key.isPressed('e')) {
unit.strafeRight(delta);
}
if (key.isPressed('space')) {
unit.ascend(delta);
}
if (key.isPressed('x')) {
unit.descend(delta);
}
if (key.isPressed('left') || key.isPressed('a')) {
unit.rotateLeft(delta);
}
if (key.isPressed('right') || key.isPressed('d')) {
unit.rotateRight(delta);
}
this.target = this.unit.position;
}
const position = this.camera.position;
// Rotate offset to "y-axis-is-up" space
this.offset.applyQuaternion(this.quat);
// Angle from z-axis around y-axis
let theta = Math.atan2(this.offset.x, this.offset.z);
// Angle from y-axis
let phi = Math.atan2(
Math.sqrt(this.offset.x * this.offset.x + this.offset.z * this.offset.z),
this.offset.y
);
theta += this.thetaDelta;
phi += this.phiDelta;
// Limit vertical orbit
phi = Math.max(this.minPhi, Math.min(this.maxPhi, phi));
phi = Math.max(this.EPS, Math.min(Math.PI - this.EPS, phi));
let radius = this.offset.length() * this.scale;
// Limit zoom distance
radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius));
this.offset.x = radius * Math.sin(phi) * Math.sin(theta);
this.offset.y = radius * Math.cos(phi);
this.offset.z = radius * Math.sin(phi) * Math.cos(theta);
// Rotate offset back to 'camera-up-vector-is-up' space
this.offset.applyQuaternion(this.quatInverse);
position.copy(this.target).add(this.offset);
this.camera.lookAt(this.target);
this.thetaDelta = 0;
this.phiDelta = 0;
this.scale = 1;
}
rotateHorizontally(angle) {
this.thetaDelta -= angle;
}
rotateVertically(angle) {
this.phiDelta -= angle;
}
zoomOut() {
this.scale /= this.zoomScale;
}
zoomIn() {
this.scale *= this.zoomScale;
}
_onMouseDown(event) {
this.rotating = true;
this.rotateStart.set(event.clientX, event.clientY);
}
_onMouseUp() {
this.rotating = false;
}
_onMouseMove(event) {
if (this.rotating) {
event.preventDefault();
this.rotateEnd.set(event.clientX, event.clientY);
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart);
this.rotateHorizontally(
2 * Math.PI * this.rotateDelta.x / this.element.clientWidth * this.rotateSpeed
);
this.rotateVertically(
2 * Math.PI * this.rotateDelta.y / this.element.clientHeight * this.rotateSpeed
);
this.rotateStart.copy(this.rotateEnd);
this.update();
}
}
_onMouseWheel(event) {
event.preventDefault();
event.stopPropagation();
const delta = event.wheelDelta || -event.detail;
if (delta > 0) {
this.zoomIn();
} else if (delta < 0) {
this.zoomOut();
}
this.update();
}
render() {
return null;
}
}
export default Controls;
================================================
FILE: src/components/game/hud/index.jsx
================================================
import React from 'react';
import './index.styl';
// TODO: import Chat from '../chat';
import Portrait from '../portrait';
// TODO: import Quests from '../quests';
import session from '../../wowser/session';
class HUD extends React.Component {
render() {
const player = session.player;
return (
<hud className="hud">
<Portrait self unit={ player } />
{ player.target && <Portrait target unit={ player.target } /> }
</hud>
);
}
}
export default HUD;
================================================
FILE: src/components/game/hud/index.styl
================================================
wowser .game .hud
z-index: 2
display: flex
align-items: center
justify-content: center
================================================
FILE: src/components/game/index.jsx
================================================
import React from 'react';
import THREE from 'three';
import './index.styl';
import Controls from './controls';
import HUD from './hud';
import Stats from './stats';
import session from '../wowser/session';
class GameScreen extends React.Component {
static id = 'game';
static title = 'Game';
constructor() {
super();
this.animate = ::this.animate;
this.resize = ::this.resize;
this.camera = new THREE.PerspectiveCamera(60, this.aspectRatio, 1, 1000);
this.camera.up.set(0, 0, 1);
this.camera.position.set(15, 0, 7);
this.prevCameraRotation = null;
this.prevCameraPosition = null;
this.renderer = null;
this.requestID = null;
// For some reason, we can't use the clock from controls here.
this.clock = new THREE.Clock();
}
componentDidMount() {
this.renderer = new THREE.WebGLRenderer({
alpha: true,
canvas: this.refs.canvas
});
this.forceUpdate();
this.resize();
this.animate();
window.addEventListener('resize', this.resize);
}
componentWillUnmount() {
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
if (this.requestID) {
this.requestID = null;
cancelAnimationFrame(this.requestID);
}
window.removeEventListener('resize', this.resize);
}
get aspectRatio() {
return window.innerWidth / window.innerHeight;
}
resize() {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = this.aspectRatio;
this.camera.updateProjectionMatrix();
}
animate() {
if (!this.renderer) {
return;
}
this.refs.controls.update();
this.refs.stats.forceUpdate();
const cameraMoved =
this.prevCameraRotation === null ||
this.prevCameraPosition === null ||
!this.prevCameraRotation.equals(this.camera.quaternion) ||
!this.prevCameraPosition.equals(this.camera.position);
session.world.animate(this.clock.getDelta(), this.camera, cameraMoved);
this.renderer.render(session.world.scene, this.camera);
this.requestID = requestAnimationFrame(this.animate);
this.prevCameraRotation = this.camera.quaternion.clone();
this.prevCameraPosition = this.camera.position.clone();
}
render() {
return (
<game className="game screen">
<canvas ref="canvas"></canvas>
<HUD />
<Controls ref="controls" for={ session.player } camera={ this.camera } />
<Stats ref="stats" renderer={ this.renderer } map={ session.world.map } />
</game>
);
}
}
export default GameScreen;
================================================
FILE: src/components/game/index.styl
================================================
wowser .game
canvas
position: absolute
top: 0
left: 0
z-index: 1
width: 100%
height: 100%
================================================
FILE: src/components/game/portrait/index.jsx
================================================
import React from 'react';
import classes from 'classnames';
import './index.styl';
class Portrait extends React.Component {
static propTypes = {
self: React.PropTypes.bool,
unit: React.PropTypes.object.isRequired,
target: React.PropTypes.bool
};
render() {
const unit = this.props.unit;
const className = classes('portrait', {
self: this.props.self,
target: this.props.target
});
return (
<portrait className={ className }>
<div className="icon portrait"></div>
<header className="name">{ unit.name }</header>
<aside className="level">{ unit.level }</aside>
<div className="divider"></div>
<div className="health">{ unit.hp } / { unit.maxHp }</div>
<div className="mana">{ unit.mp } / { unit.maxMp }</div>
</portrait>
);
}
}
export default Portrait;
================================================
FILE: src/components/game/portrait/index.styl
================================================
wowser .portrait
display: block
position: relative
width: 178px
height: 60px
background: url('./images/portrait.png') no-repeat
.icon
position: absolute
top: -4px
left: -4px
z-index: 1
.name
position: absolute
top: 4px
left: 64px
color: #FFCC00
font-size: 15px
.level
position: absolute
top: 6px
right: 12px
font-size: 11px
.divider
position: absolute
top: 20px
right: 3px
width: 113px
.health, .mana
width: 115px
border-radius: 4px
font-size: 12px
line-height: 11px
text-shadow: 1px 1px 0px #000000
text-align: center
.health
position: absolute
bottom: 23px
right: 6px
background-image: linear-gradient(180deg, #330000 0%, #990000 100%)
box-shadow: -1px -1px 0px #660000, 1px 1px 0px #E50202
.mana
position: absolute
bottom: 7px
right: 20px
background-image: linear-gradient(180deg, #021D39 0%, #0B4C93 100%)
box-shadow: -1px -1px 0px #063467, 1px 1px 0px #146CD0
&.self
position: absolute
top: 12px
left: 12px
&.target
position: absolute
top: 12px
left: 210px
wowser .icon.portrait
width: 66px
height: 66px
background: url('./images/icon-portrait.png') no-repeat
================================================
FILE: src/components/game/quests/index.jsx
================================================
import React from 'react';
import './index.styl';
class QuestsPanel extends React.Component {
render() {
return (
<quests className="quests panel headless">
<div className="icon portrait"></div>
<h1>Quest Log</h1>
<div className="divider thick"></div>
<p>
Soon™
</p>
</quests>
);
}
}
export default QuestsPanel;
================================================
FILE: src/components/game/quests/index.styl
================================================
wowser .quests
position: absolute
bottom: 0
right: 0
height: 30%
width: 300px
================================================
FILE: src/components/game/stats/index.jsx
================================================
import React from 'react';
import './index.styl';
class Stats extends React.Component {
static propTypes = {
renderer: React.PropTypes.object,
map: React.PropTypes.object
};
mapStats() {
const map = this.props.map;
return (
<div>
<div className="divider"></div>
<h2>Map Chunks</h2>
<div className="divider"></div>
<p>
Loaded: { map ? map.chunks.size : 0 }
</p>
<div className="divider"></div>
<h2>Map Doodads</h2>
<div className="divider"></div>
<p>
Loading: { map ? map.doodadManager.entriesPendingLoad.size : 0 }
</p>
<p>
Loaded: { map ? map.doodadManager.doodads.size : 0 }
</p>
<p>
Animated: { map ? map.doodadManager.animatedDoodads.size : 0 }
</p>
<div className="divider"></div>
<h2>WMOs</h2>
<div className="divider"></div>
<p>
Loading Entries: { map ? map.wmoManager.counters.loadingEntries : 0 }
</p>
<p>
Loaded Entries: { map ? map.wmoManager.counters.loadedEntries : 0 }
</p>
<p>
Loading Groups: { map ? map.wmoManager.counters.loadingGroups : 0 }
</p>
<p>
Loaded Groups: { map ? map.wmoManager.counters.loadedGroups : 0 }
</p>
<p>
Loading Doodads: { map ? map.wmoManager.counters.loadingDoodads : 0 }
</p>
<p>
Loaded Doodads: { map ? map.wmoManager.counters.loadedDoodads : 0 }
</p>
<p>
Animated Doodads: { map ? map.wmoManager.counters.animatedDoodads : 0 }
</p>
</div>
);
}
render() {
const renderer = this.props.renderer;
if (!renderer) {
return null;
}
const map = this.props.map;
const { memory, programs, render } = renderer.info;
return (
<stats className="stats frame thin">
<h2>Memory</h2>
<div className="divider"></div>
<p>
Geometries: { memory.geometries }
</p>
<p>
Textures: { memory.textures }
</p>
<p>
Programs: { programs.length }
</p>
<div className="divider"></div>
<h2>Render</h2>
<div className="divider"></div>
<p>
Calls: { render.calls }
</p>
<p>
Faces: { render.faces }
</p>
<p>
Points: { render.points }
</p>
<p>
Vertices: { render.vertices }
</p>
{ map && this.mapStats() }
</stats>
);
}
}
export default Stats;
================================================
FILE: src/components/game/stats/index.styl
================================================
wowser .stats
position: absolute
bottom: 0
right: 0
z-index: 3
width: 160px
================================================
FILE: src/components/kit/index.jsx
================================================
import React from 'react';
class KitScreen extends React.Component {
static id = 'kit';
static title = 'UI Kit';
render() {
return (
<kit className="screen">
<div className="frame thin">
<h2>Thin frame</h2>
<div className="divider"></div>
<p>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
</p>
<div className="divider thick"></div>
<button>Regular button</button>
<button disabled>Disabled button</button>
<input type="submit" value="Regular submit" />
<input type="submit" value="Disabled submit" disabled />
</div>
<div className="frame thick">
<h2>Thick frame</h2>
<div className="divider"></div>
<p>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
</p>
</div>
<div className="panel">
<div className="icon portrait"></div>
<h1>Regular panel</h1>
<div className="divider"></div>
<p>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
</p>
</div>
<div className="panel headless">
<div className="icon portrait"></div>
<h1>Headless panel</h1>
<div className="divider thick"></div>
<p>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
</p>
</div>
</kit>
);
}
}
export default KitScreen;
================================================
FILE: src/components/realms/index.jsx
================================================
import React from 'react';
import session from '../wowser/session';
class RealmsScreen extends React.Component {
static id = 'realms';
static title = 'Realms Selection';
constructor() {
super();
this.state = {
realm: null,
realms: []
};
this._onAuthenticate = ::this._onAuthenticate;
this._onRealmSelect = ::this._onRealmSelect;
this._onRefresh = ::this._onRefresh;
this._onSubmit = ::this._onSubmit;
session.realms.on('refresh', this._onRefresh);
session.game.on('authenticate', this._onAuthenticate);
this.refresh();
}
componentWillUnmount() {
session.realms.removeListener('refresh', this._onRefresh);
session.game.removeListener('authenticate', this._onAuthenticate);
}
connect(realm) {
session.game.connect('localhost', realm.port);
}
refresh() {
session.realms.refresh();
}
_onAuthenticate() {
session.screen = 'characters';
}
_onRealmSelect(event) {
this.setState({ realm: event.target.value });
}
_onRefresh() {
const realms = session.realms.list;
this.setState({
realm: realms[0],
realms: realms
});
}
_onSubmit(event) {
event.preventDefault();
this.connect(this.state.realm);
}
render() {
return (
<realms className="realms screen">
<div className="panel">
<h1>Realm Selection</h1>
<div className="divider"></div>
<form onSubmit={ this._onSubmit }>
<fieldset>
<select value={ this.state.realm }
onChange={ this._onRealmSelect }>
{ this.state.realms.map((realm) => {
return (
<option key={ realm.id } value={ realm }>
{ realm.name }
</option>
);
}) }
</select>
</fieldset>
<div className="divider"></div>
<input type="submit" value="Connect" autoFocus />
<input type="button" value="Refresh" onClick={ this.refresh } />
</form>
</div>
</realms>
);
}
}
export default RealmsScreen;
================================================
FILE: src/components/wowser/index.jsx
================================================
import React from 'react';
import './index.styl';
import AuthScreen from '../auth';
import CharactersScreen from '../characters';
import GameScreen from '../game';
import RealmsScreen from '../realms';
import Kit from '../kit';
import session from './session';
class Wowser extends React.Component {
static SCREENS = [
AuthScreen,
RealmsScreen,
CharactersScreen,
GameScreen,
Kit
];
constructor() {
super();
this.state = {
screen: session.screen
};
this._onScreenChange = ::this._onScreenChange;
this._onScreenSelect = ::this._onScreenSelect;
session.on('screen:change', this._onScreenChange);
}
get currentScreen() {
const Screen = this.constructor.SCREENS.find((screen) => {
return screen.id === this.state.screen;
});
return <Screen />;
}
_onScreenChange(_from, to) {
this.setState({ screen: to });
}
_onScreenSelect(event) {
session.screen = event.target.value;
}
render() {
const screens = this.constructor.SCREENS;
return (
<wowser>
<div className="branding">
<header>Wowser</header>
<div className="divider"></div>
<div className="slogan">World of Warcraft in the browser</div>
</div>
<select className="screen-selector"
value={ this.state.screen }
onChange={ this._onScreenSelect }>
{ screens.map((screen) => {
return (
<option key={ screen.id } value={ screen.id }>
{ screen.title }
</option>
);
}) }
</select>
{ this.currentScreen }
</wowser>
);
}
}
export default Wowser;
================================================
FILE: src/components/wowser/index.styl
================================================
@import '~normalize.css'
@import './ui';
html, body
width: 100%
height: 100%
overflow: hidden
*
box-sizing: border-box
wowser
display: flex
align-items: center
justify-content: center
width: 100%
height: 100%
background: #222222
font-family: Galdeano
font-size: 13px
color: #FFFFFF
-webkit-font-smoothing: antialiased
&:active
cursor: none
.branding
position: absolute
top: 10px
right: 10px
z-index: 4
header
width: 204px
height: 47px
background: url('./images/logo.png') no-repeat
text-indent: -99999px
.divider
margin: 5px 0
.slogan
color: #FFCC00
letter-spacing: .075em
select.screen-selector
position: absolute
top: 100px
right: 10px
z-index: 4
color: #000000
================================================
FILE: src/components/wowser/session.jsx
================================================
import Client from '../../lib';
class Session extends Client {
constructor() {
super();
this._screen = 'auth';
}
get screen() {
return this._screen;
}
set screen(screen) {
if (this._screen !== screen) {
this.emit('screen:change', this._screen, screen);
this._screen = screen;
}
}
}
export default new Session();
================================================
FILE: src/components/wowser/ui/form/index.styl
================================================
wowser form
fieldset
display: block
border: 0
padding: 0
margin: .5em
border-top: 1px solid transparent
label
display: block
color: #999999
font-size: 14px
margin: .7em 0 .1em
input, select, textarea
padding: .2em .3em
background-color: #111111
border-width: 1px
border-style: solid
border-color: #333333 #666666 #666666 #333333
border-radius: 6px
color: #FFFFFF
font-size: 14px
-webkit-font-smoothing: antialiased
outline: none
margin-bottom: .3em
================================================
FILE: src/components/wowser/ui/frame/dividers/index.styl
================================================
wowser .divider
border-style: solid
&, &.horizontal
border-width: 3px 5px 0 5px
border-image: url('./images/horizontal.png') 3 5 0 5 repeat
&.thick
border-width: 11px 5px 0 5px
border-image: url('./images/thick-horizontal.png') 11 5 0 5 repeat
================================================
FILE: src/components/wowser/ui/frame/index.styl
================================================
@import './dividers';
wowser
.frame, .panel
display: block
position: relative
margin: 10px
&:before
content: ' '
display: block
position: absolute
top: -3px
bottom: -3px
left: -3px
right: -3px
z-index: -1
background: rgba(0, 0, 0, .8)
.frame
border-width: 5px
border-image: url('./images/thin.png') 5 5 5 5 repeat
border-style: solid
.divider
&, &.horizontal
margin-left: -3px
margin-right: -3px
&.thick
border-width: 11px
border-image: url('./images/thick.png') 11 11 11 11 repeat
.divider
&, &.horizontal
margin-left: -5px
margin-right: -5px
.panel
border-width: 23px
border-image: url('./images/panel.png') 23 23 23 23 repeat
border-style: solid
.icon.portrait
position: absolute
top: -30px
left: -46px
& + h1, & + h2, & + h3
margin-left: 1.5em
.divider
&, &.horizontal
margin-left: -6px
margin-right: -6px
&.headless
border-width: 11px 23px 23px 23px
border-image: url('./images/panel-headless.png') 11 23 23 23 repeat
.icon.portrait
top: -20px
================================================
FILE: src/components/wowser/ui/index.styl
================================================
@import './form';
@import './frame';
@import './screen';
@import './type';
@import './widgets';
================================================
FILE: src/components/wowser/ui/screen.styl
================================================
wowser .screen
position: absolute
top: 0
left: 0
z-index: 3
width: 100%
height: 100%
display: flex
align-items: center
justify-content: center
================================================
FILE: src/components/wowser/ui/type.styl
================================================
wowser
h1, h2, h3, h4
margin: .3em
color: #FFCC00
font-weight: normal
h1
font-size: 17px
h2
font-size: 15px
p
margin: .5em
================================================
FILE: src/components/wowser/ui/widgets/button.styl
================================================
wowser
input[type='submit'], input[type='button'], button
margin: .4em 0 .3em .5em
padding: .2em 1em
border: none
border-radius: 4px
background-image: linear-gradient(180deg, #990000 0%, #660000 60%, #660000 100%)
border-width: 1px
border-style: solid
border-color: #E50202 #990000 #770000 #E50202
color: #FFFFFF
text-shadow: 1px 1px 0px #330000
font-size: 13px
-webkit-font-smoothing: antialiased
outline: none
&:enabled:active
background-image: linear-gradient(180deg, #660000 0%, #660000 26%, #990000 100%)
border-color: #330000
transform: scale(.97)
text-shadow: none
box-shadow: -1px -1px 1px rgba(#000000, .2), 1px 1px 1px rgba(#000000, .2)
&:enabled:hover
color: #FFCC00
&:disabled
background-image: linear-gradient(180deg, #4A0000 0%, #2A0000 100%)
border-color: #8A0000 #400000 #400000 #8A0000
color: #990000
================================================
FILE: src/components/wowser/ui/widgets/index.styl
================================================
@import './button';
================================================
FILE: src/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Wowser</title>
<link rel="shortcut icon" href="./favicon.png" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
</head>
<body>
<app></app>
<link href="https://fonts.googleapis.com/css?family=Galdeano" rel="stylesheet" />
</body>
</html>
================================================
FILE: src/lib/auth/challenge-opcode.js
================================================
class ChallengeOpcode {
static SUCCESS = 0x00;
static UNKNOWN0 = 0x01;
static UNKNOWN1 = 0x02;
static ACCOUNT_BANNED = 0x03;
static ACCOUNT_INVALID = 0x04;
static PASSWORD_INVALID = 0x05;
static ALREADY_ONLINE = 0x06;
static OUT_OF_CREDIT = 0x07;
static BUSY = 0x08;
static BUILD_INVALID = 0x09;
static BUILD_UPDATE = 0x0A;
static INVALID_SERVER = 0x0B;
static ACCOUNT_SUSPENDED = 0x0C;
static ACCESS_DENIED = 0x0D;
static SURVEY = 0x0E;
static PARENTAL_CONTROL = 0x0F;
static LOCK_ENFORCED = 0x10;
static TRIAL_EXPIRED = 0x11;
static BATTLE_NET = 0x12;
}
export default ChallengeOpcode;
================================================
FILE: src/lib/auth/handler.js
================================================
import AuthChallengeOpcode from './challenge-opcode';
import AuthOpcode from './opcode';
import AuthPacket from './packet';
import Socket from '../net/socket';
import SRP from '../crypto/srp';
class AuthHandler extends Socket {
// Default port for the auth-server
static PORT = 3724;
// Creates a new authentication handler
constructor(session) {
super();
// Holds session
this.session = session;
// Holds credentials for this session (if any)
this.account = null;
this.password = null;
// Holds Secure Remote Password implementation
this.srp = null;
// Listen for incoming data
this.on('data:receive', this.dataReceived);
// Delegate packets
this.on('packet:receive:LOGON_CHALLENGE', this.handleLogonChallenge);
this.on('packet:receive:LOGON_PROOF', this.handleLogonProof);
}
// Retrieves the session key (if any)
get key() {
return this.srp && this.srp.K;
}
// Connects to given host through given port
connect(host, port = NaN) {
if (!this.connected) {
super.connect(host, port || this.constructor.PORT);
console.info('connecting to auth-server @', this.host, ':', this.port);
}
return this;
}
// Sends authentication request to connected host
authenticate(account, password) {
if (!this.connected) {
return false;
}
this.account = account.toUpperCase();
this.password = password.toUpperCase();
console.info('authenticating', this.account);
// Extract configuration data
const {
build,
majorVersion,
minorVersion,
patchVersion,
game,
raw: {
os, locale, platform
},
timezone
} = this.session.config;
const ap = new AuthPacket(AuthOpcode.LOGON_CHALLENGE, 4 + 29 + 1 + this.account.length);
ap.writeByte(0x00);
ap.writeShort(30 + this.account.length);
ap.writeString(game); // game string
ap.writeByte(majorVersion); // v1 (major)
ap.writeByte(minorVersion); // v2 (minor)
ap.writeByte(patchVersion); // v3 (patch)
ap.writeShort(build); // build
ap.writeString(platform); // platform
ap.writeString(os); // os
ap.writeString(locale); // locale
ap.writeUnsignedInt(timezone); // timezone
ap.writeUnsignedInt(0); // ip
ap.writeByte(this.account.length); // account length
ap.writeString(this.account); // account
this.send(ap);
}
// Data received handler
dataReceived() {
while (true) {
if (!this.connected || this.buffer.available < AuthPacket.HEADER_SIZE) {
return;
}
const ap = new AuthPacket(this.buffer.readByte(), this.buffer.seek(-AuthPacket.HEADER_SIZE).read(), false);
console.log('⟹', ap.toString());
// console.debug ap.toHex()
// console.debug ap.toASCII()
this.emit('packet:receive', ap);
if (ap.opcodeName) {
this.emit(`packet:receive:${ap.opcodeName}`, ap);
}
}
}
// Logon challenge handler (LOGON_CHALLENGE)
handleLogonChallenge(ap) {
ap.readUnsignedByte();
const status = ap.readUnsignedByte();
switch (status) {
case AuthChallengeOpcode.SUCCESS:
console.info('received logon challenge');
const B = ap.read(32); // B
const glen = ap.readUnsignedByte(); // g-length
const g = ap.read(glen); // g
const Nlen = ap.readUnsignedByte(); // n-length
const N = ap.read(Nlen); // N
const salt = ap.read(32); // salt
ap.read(16); // unknown
ap.readUnsignedByte(); // security flags
this.srp = new SRP(N, g);
this.srp.feed(salt, B, this.account, this.password);
const lpp = new AuthPacket(AuthOpcode.LOGON_PROOF, 1 + 32 + 20 + 20 + 2);
lpp.write(this.srp.A.toArray());
lpp.write(this.srp.M1.digest);
lpp.write(new Array(20)); // CRC hash
lpp.writeByte(0x00); // number of keys
lpp.writeByte(0x00); // security flags
this.send(lpp);
break;
case AuthChallengeOpcode.ACCOUNT_INVALID:
console.warn('account invalid');
this.emit('reject');
break;
case AuthChallengeOpcode.BUILD_INVALID:
console.warn('build invalid');
this.emit('reject');
break;
default:
break;
}
}
// Logon proof handler (LOGON_PROOF)
handleLogonProof(ap) {
ap.readByte();
console.info('received proof response');
const M2 = ap.read(20);
if (this.srp.validate(M2)) {
this.emit('authenticate');
} else {
this.emit('reject');
}
}
}
export default AuthHandler;
================================================
FILE: src/lib/auth/opcode.js
================================================
class Opcode {
static LOGON_CHALLENGE = 0x00;
static LOGON_PROOF = 0x01;
static RECONNECT_CHALLENGE = 0x02;
static RECONNECT_PROOF = 0x03;
static REALM_LIST = 0x10;
}
export default Opcode;
================================================
FILE: src/lib/auth/packet.js
================================================
import AuthOpcode from './opcode';
import BasePacket from '../net/packet';
import ObjectUtil from '../utils/object-util';
class AuthPacket extends BasePacket {
// Header size in bytes for both incoming and outgoing packets
static HEADER_SIZE = 1;
constructor(opcode, source, outgoing = true) {
super(opcode, source || AuthPacket.HEADER_SIZE, outgoing);
}
// Retrieves the name of the opcode for this packet (if available)
get opcodeName() {
return ObjectUtil.keyByValue(AuthOpcode, this.opcode);
}
// Finalizes this packet
finalize() {
this.index = 0;
this.writeByte(this.opcode);
}
}
export default AuthPacket;
================================================
FILE: src/lib/characters/character.js
================================================
class Character {
// Short string representation of this character
toString() {
return `[Character; GUID: ${this.guid}]`;
}
}
export default Character;
================================================
FILE: src/lib/characters/handler.js
================================================
import EventEmitter from 'events';
import Character from './character';
import GamePacket from '../game/packet';
import GameOpcode from '../game/opcode';
class CharacterHandler extends EventEmitter {
// Creates a new character handler
constructor(session) {
super();
// Holds session
this.session = session;
// Initially empty list of characters
this.list = [];
// Listen for character list
this.session.game.on('packet:receive:SMSG_CHAR_ENUM', ::this.handleCharacterList);
}
// Requests a fresh list of characters
refresh() {
console.info('refreshing character list');
const gp = new GamePacket(GameOpcode.CMSG_CHAR_ENUM);
return this.session.game.send(gp);
}
// Character list refresh handler (SMSG_CHAR_ENUM)
handleCharacterList(gp) {
const count = gp.readByte(); // number of characters
this.list.length = 0;
for (let i = 0; i < count; ++i) {
const character = new Character();
character.guid = gp.readGUID();
character.name = gp.readCString();
character.race = gp.readUnsignedByte();
character.class = gp.readUnsignedByte();
character.gender = gp.readUnsignedByte();
character.bytes = gp.readUnsignedInt();
character.facial = gp.readUnsignedByte();
character.level = gp.readUnsignedByte();
character.zone = gp.readUnsignedInt();
character.map = gp.readUnsignedInt();
character.x = gp.readFloat();
character.y = gp.readFloat();
character.z = gp.readFloat();
character.guild = gp.readUnsignedInt();
character.flags = gp.readUnsignedInt();
gp.readUnsignedInt(); // character customization
gp.readUnsignedByte(); // (?)
const pet = {
model: gp.readUnsignedInt(),
level: gp.readUnsignedInt(),
family: gp.readUnsignedInt()
};
if (pet.model) {
character.pet = pet;
}
character.equipment = [];
for (let j = 0; j < 23; ++j) {
const item = {
model: gp.readUnsignedInt(),
type: gp.readUnsignedByte(),
enchantment: gp.readUnsignedInt()
};
character.equipment.push(item);
}
this.list.push(character);
}
this.emit('refresh');
}
}
export default CharacterHandler;
================================================
FILE: src/lib/config.js
================================================
class Raw {
constructor(config) {
this.config = config;
}
raw(value) {
return ('\u0000\u0000\u0000\u0000' + value.split('').reverse().join('')).slice(-4);
}
get locale() {
return this.raw(this.config.locale);
}
get os() {
return this.raw(this.config.os);
}
get platform() {
return this.raw(this.config.platform);
}
}
class Config {
constructor() {
this.game = 'Wow ';
this.build = 12340;
this.version = '3.3.5';
this.timezone = 0;
this.locale = 'enUS';
this.os = 'Win';
this.platform = 'x86';
this.raw = new Raw(this);
}
set version(version) {
[
this.majorVersion,
this.minorVersion,
this.patchVersion
] = version.split('.').map(function(bit) {
return parseInt(bit, 10);
});
}
}
export default Config;
================================================
FILE: src/lib/crypto/big-num.js
================================================
import BigInteger from 'jsbn/lib/big-integer';
// C-like BigNum decorator for JSBN's BigInteger
class BigNum {
// Convenience BigInteger.ZERO decorator
static ZERO = new BigNum(BigInteger.ZERO);
// Creates a new BigNum
constructor(value, radix) {
if (typeof value === 'number') {
this._bi = BigInteger.fromInt(value);
} else if (value.constructor === BigInteger) {
this._bi = value;
} else if (value.constructor === BigNum) {
this._bi = value.bi;
} else {
this._bi = new BigInteger(value, radix);
}
}
// Short string description of this BigNum
toString() {
return `[BigNum; Value: ${this._bi}; Hex: ${this._bi.toString(16).toUpperCase()}]`;
}
// Retrieves BigInteger instance being decorated
get bi() {
return this._bi;
}
// Performs a modulus operation
mod(m) {
return new BigNum(this._bi.mod(m.bi));
}
// Performs an exponential+modulus operation
modPow(e, m) {
return new BigNum(this._bi.modPow(e.bi, m.bi));
}
// Performs an addition
add(o) {
return new BigNum(this._bi.add(o.bi));
}
// Performs a subtraction
subtract(o) {
return new BigNum(this._bi.subtract(o.bi));
}
// Performs a multiplication
multiply(o) {
return new BigNum(this._bi.multiply(o.bi));
}
// Performs a division
divide(o) {
return new BigNum(this._bi.divide(o.bi));
}
// Whether the given BigNum is equal to this one
equals(o) {
return this._bi.equals(o.bi);
}
// Generates a byte-array from this BigNum (defaults to little-endian)
toArray(littleEndian = true, unsigned = true) {
const ba = this._bi.toByteArray();
if (unsigned && this._bi.s === 0 && ba[0] === 0) {
ba.shift();
}
if (littleEndian) {
return ba.reverse();
}
return ba;
}
// Creates a new BigNum from given byte-array
static fromArray(bytes, littleEndian = true, unsigned = true) {
if (typeof bytes.toArray !== 'undefined') {
bytes = bytes.toArray();
} else {
bytes = bytes.slice(0);
}
if (littleEndian) {
bytes = bytes.reverse();
}
if (unsigned && bytes[0] & 0x80) {
bytes.unshift(0);
}
return new BigNum(bytes);
}
// Creates a new random BigNum of the given number of bytes
static fromRand(length) {
// TODO: This should use a properly seeded, secure RNG
const bytes = [];
for (let i = 0; i < length; ++i) {
bytes.push(Math.floor(Math.random() * 128));
}
return new BigNum(bytes);
}
}
export default BigNum;
================================================
FILE: src/lib/crypto/crypt.js
================================================
import { HMAC } from 'jsbn/lib/sha1';
import RC4 from 'jsbn/lib/rc4';
import ArrayUtil from '../utils/array-util';
class Crypt {
// Creates crypt
constructor() {
// RC4's for encryption and decryption
this._encrypt = null;
this._decrypt = null;
}
// Encrypts given data through RC4
encrypt(data) {
if (this._encrypt) {
this._encrypt.encrypt(data);
}
return this;
}
// Decrypts given data through RC4
decrypt(data) {
if (this._decrypt) {
this._decrypt.decrypt(data);
}
return this;
}
// Sets session key and initializes this crypt
set key(key) {
console.info('initializing crypt');
// Fresh RC4's
this._encrypt = new RC4();
this._decrypt = new RC4();
// Calculate the encryption hash (through the server decryption key)
const enckey = ArrayUtil.fromHex('C2B3723CC6AED9B5343C53EE2F4367CE');
const enchash = HMAC.fromArrays(enckey, key);
// Calculate the decryption hash (through the client decryption key)
const deckey = ArrayUtil.fromHex('CC98AE04E897EACA12DDC09342915357');
const dechash = HMAC.fromArrays(deckey, key);
// Seed RC4's with the computed hashes
this._encrypt.init(enchash);
this._decrypt.init(dechash);
// Ensure the buffer is synchronized
for (let i = 0; i < 1024; ++i) {
this._encrypt.next();
this._decrypt.next();
}
}
}
export default Crypt;
================================================
FILE: src/lib/crypto/hash/sha1.js
================================================
import SHA1Base from 'jsbn/lib/sha1';
import Hash from '../hash';
// SHA-1 implementation
class SHA1 extends Hash {
// Finalizes this SHA-1 hash
finalize() {
this._digest = SHA1Base.fromArray(this._data.toArray());
}
}
export default SHA1;
================================================
FILE: src/lib/crypto/hash.js
================================================
import ByteBuffer from 'byte-buffer';
// Feedable hash implementation
class Hash {
// Creates a new hash
constructor() {
// Data fed to this hash
this._data = null;
// Resulting digest
this._digest = null;
this.reset();
}
// Retrieves digest (finalizes this hash if needed)
get digest() {
if (!this._digest) {
this.finalize();
}
return this._digest;
}
// Resets this hash, voiding the digest and allowing new feeds
reset() {
this._data = new ByteBuffer(0, ByteBuffer.BIG_ENDIAN, true);
this._digest = null;
return this;
}
// Feeds hash given value
feed(value) {
if (this._digest) {
return this;
}
if (value.constructor === String) {
this._data.writeString(value);
} else {
this._data.write(value);
}
return this;
}
// Finalizes this hash, calculates the digest and blocks additional feeds
finalize() {
return this;
}
}
export default Hash;
================================================
FILE: src/lib/crypto/srp.js
================================================
import equal from 'deep-equal';
import BigNum from './big-num';
import SHA1 from './hash/sha1';
// Secure Remote Password
// http://tools.ietf.org/html/rfc2945
class SRP {
// Creates new SRP instance with given constant prime and generator
constructor(N, g) {
// Constant prime (N)
this._N = BigNum.fromArray(N);
// Generator (g)
this._g = BigNum.fromArray(g);
// Client salt (provided by server)
this._s = null;
// Salted authentication hash
this._x = null;
// Random scrambling parameter
this._u = null;
// Derived key
this._k = new BigNum(3);
// Server's public ephemeral value (provided by server)
this._B = null;
// Password verifier
this._v = null;
// Client-side session key
this._S = null;
// Shared session key
this._K = null;
// Client proof hash
this._M1 = null;
// Expected server proof hash
this._M2 = null;
while (true) {
// Client's private ephemeral value (random)
this._a = BigNum.fromRand(19);
// Client's public ephemeral value based on the above
// A = g ^ a mod N
this._A = this._g.modPow(this._a, this._N);
if (!this._A.mod(this._N).equals(BigNum.ZERO)) {
break;
}
}
}
// Retrieves client's public ephemeral value
get A() {
return this._A;
}
// Retrieves the session key
get K() {
return this._K;
}
// Retrieves the client proof hash
get M1() {
return this._M1;
}
// Feeds salt, server's public ephemeral value, account and password strings
feed(s, B, I, P) {
// Generated salt (s) and server's public ephemeral value (B)
this._s = BigNum.fromArray(s);
this._B = BigNum.fromArray(B);
// Authentication hash consisting of user's account (I), a colon and user's password (P)
// auth = H(I : P)
const auth = new SHA1();
auth.feed(I);
auth.feed(':');
auth.feed(P).finalize();
// Salted authentication hash consisting of the salt and the authentication hash
// x = H(s | auth)
const x = new SHA1();
x.feed(this._s.toArray());
x.feed(auth.digest);
this._x = BigNum.fromArray(x.digest);
// Password verifier
// v = g ^ x mod N
this._v = this._g.modPow(this._x, this._N);
// Random scrambling parameter consisting of the public ephemeral values
// u = H(A | B)
const u = new SHA1();
u.feed(this._A.toArray());
u.feed(this._B.toArray());
this._u = BigNum.fromArray(u.digest);
// Client-side session key
// S = (B - (kg^x)) ^ (a + ux)
const kgx = this._k.multiply(this._g.modPow(this._x, this._N));
const aux = this._a.add(this._u.multiply(this._x));
this._S = this._B.subtract(kgx).modPow(aux, this._N);
// Store odd and even bytes in separate byte-arrays
const S = this._S.toArray();
const S1 = [];
const S2 = [];
for (let i = 0; i < 16; ++i) {
S1[i] = S[i * 2];
S2[i] = S[i * 2 + 1];
}
// Hash these byte-arrays
const S1h = new SHA1();
const S2h = new SHA1();
S1h.feed(S1).finalize();
S2h.feed(S2).finalize();
// Shared session key generation by interleaving the previously generated hashes
this._K = [];
for (let i = 0; i < 20; ++i) {
this._K[i * 2] = S1h.digest[i];
this._K[i * 2 + 1] = S2h.digest[i];
}
// Generate username hash
const userh = new SHA1();
userh.feed(I).finalize();
// Hash both prime and generator
const Nh = new SHA1();
const gh = new SHA1();
Nh.feed(this._N.toArray()).finalize();
gh.feed(this._g.toArray()).finalize();
// XOR N-prime and generator
const Ngh = [];
for (let i = 0; i < 20; ++i) {
Ngh[i] = Nh.digest[i] ^ gh.digest[i];
}
// Calculate M1 (client proof)
// M1 = H( (H(N) ^ H(G)) | H(I) | s | A | B | K )
this._M1 = new SHA1();
this._M1.feed(Ngh);
this._M1.feed(userh.digest);
this._M1.feed(this._s.toArray());
this._M1.feed(this._A.toArray());
this._M1.feed(this._B.toArray());
this._M1.feed(this._K);
this._M1.finalize();
// Pre-calculate M2 (expected server proof)
// M2 = H( A | M1 | K )
this._M2 = new SHA1();
this._M2.feed(this._A.toArray());
this._M2.feed(this._M1.digest);
this._M2.feed(this._K);
this._M2.finalize();
}
// Validates given M2 with expected M2
validate(M2) {
if (!this._M2) {
return false;
}
return equal(M2.toArray(), this._M2.digest);
}
}
export default SRP;
================================================
FILE: src/lib/game/chat/handler.js
================================================
import EventEmitter from 'events';
import Message from './message';
class ChatHandler extends EventEmitter {
// Creates a new chat handler
constructor(session) {
super();
// Holds session
this.session = session;
// Holds messages
this.messages = [
new Message('system', 'Welcome to Wowser!'),
new Message('system', 'This is a very alpha-ish build.'),
new Message('info', 'This is an info message'),
new Message('error', 'This is an error message'),
new Message('area', 'Player: This is a message emitted nearby'),
new Message('channel', '[Trade]: This is a channel message'),
new Message('whisper outgoing', 'To Someone: This is an outgoing whisper'),
new Message('whisper incoming', 'Someone: This is an incoming whisper'),
new Message('guild', '[Guild] Someone: This is a guild message')
];
// Listen for messages
this.session.game.on('packet:receive:SMSG_MESSAGE_CHAT', ::this.handleMessage);
}
// Creates chat message
create() {
return new Message();
}
// Sends given message
send(_message) {
throw new Error('sending chat messages is not yet implemented');
}
// Message handler (SMSG_MESSAGE_CHAT)
handleMessage(gp) {
gp.readUnsignedByte(); // type
gp.readUnsignedInt(); // language
const guid1 = gp.readGUID();
gp.readUnsignedInt();
gp.readGUID(); // guid2
const len = gp.readUnsignedInt();
const text = gp.readString(len);
gp.readUnsignedByte(); // flags
const message = new Message();
message.text = text;
message.guid = guid1;
this.messages.push(message);
this.emit('message', message);
}
}
export default ChatHandler;
================================================
FILE: src/lib/game/chat/message.js
================================================
class ChatMessage {
// Creates a new message
constructor(kind, text) {
this.kind = kind;
this.text = text;
this.timestamp = new Date();
}
// Short string representation of this message
toString() {
return `[Message; Text: ${this.text}; GUID: ${this.guid}]`;
}
}
export default ChatMessage;
================================================
FILE: src/lib/game/entity.js
================================================
import EventEmitter from 'events';
class Entity extends EventEmitter {
constructor() {
super();
this.guid = Math.random() * 1000000 | 0;
}
}
export default Entity;
================================================
FILE: src/lib/game/guid.js
================================================
class GUID {
// GUID byte-length (64-bit)
static LENGTH = 8;
// Creates a new GUID
constructor(buffer) {
// Holds raw byte representation
this.raw = buffer;
// Holds low-part
this.low = buffer.readUnsignedInt();
// Holds high-part
this.high = buffer.readUnsignedInt();
}
// Short string representation of this GUID
toString() {
const high = ('0000' + this.high.toString(16)).slice(-4);
const low = ('0000' + this.low.toString(16)).slice(-4);
return `[GUID; Hex: 0x${high}${low}]`;
}
}
export default GUID;
================================================
FILE: src/lib/game/handler.js
================================================
import ByteBuffer from 'byte-buffer';
import BigNum from '../crypto/big-num';
import Crypt from '../crypto/crypt';
import GameOpcode from './opcode';
import GamePacket from './packet';
import GUID from '../game/guid';
import SHA1 from '../crypto/hash/sha1';
import Socket from '../net/socket';
class GameHandler extends Socket {
// Creates a new game handler
constructor(session) {
super();
// Holds session
this.session = session;
// Listen for incoming data
this.on('data:receive', ::this.dataReceived);
// Delegate packets
this.on('packet:receive:SMSG_AUTH_CHALLENGE', ::this.handleAuthChallenge);
this.on('packet:receive:SMSG_AUTH_RESPONSE', ::this.handleAuthResponse);
this.on('packet:receive:SMSG_LOGIN_VERIFY_WORLD', ::this.handleWorldLogin);
}
// Connects to given host through given port
connect(host, port) {
if (!this.connected) {
super.connect(host, port);
console.info('connecting to game-server @', this.host, ':', this.port);
}
return this;
}
// Finalizes and sends given packet
send(packet) {
const size = packet.bodySize + GamePacket.OPCODE_SIZE_OUTGOING;
packet.front();
packet.writeShort(size, ByteBuffer.BIG_ENDIAN);
packet.writeUnsignedInt(packet.opcode);
// Encrypt header if needed
if (this._crypt) {
this._crypt.encrypt(new Uint8Array(packet.buffer, 0, GamePacket.HEADER_SIZE_OUTGOING));
}
return super.send(packet);
}
// Attempts to join game with given character
join(character) {
if (character) {
console.info('joining game with', character.toString());
const gp = new GamePacket(GameOpcode.CMSG_PLAYER_LOGIN, GamePacket.HEADER_SIZE_OUTGOING + GUID.LENGTH);
gp.writeGUID(character.guid);
return this.send(gp);
}
return false;
}
// Data received handler
dataReceived(_socket) {
while (true) {
if (!this.connected) {
return;
}
if (this.remaining === false) {
if (this.buffer.available < GamePacket.HEADER_SIZE_INCOMING) {
return;
}
// Decrypt header if needed
if (this._crypt) {
this._crypt.decrypt(new Uint8Array(this.buffer.buffer, this.buffer.index, GamePacket.HEADER_SIZE_INCOMING));
}
this.remaining = this.buffer.readUnsignedShort(ByteBuffer.BIG_ENDIAN);
}
if (this.remaining > 0 && this.buffer.available >= this.remaining) {
const size = GamePacket.OPCODE_SIZE_INCOMING + this.remaining;
const gp = new GamePacket(this.buffer.readUnsignedShort(), this.buffer.seek(-GamePacket.HEADER_SIZE_INCOMING).read(size), false);
this.remaining = false;
console.log('⟹', gp.toString());
// console.debug gp.toHex()
// console.debug gp.toASCII()
this.emit('packet:receive', gp);
if (gp.opcodeName) {
this.emit(`packet:receive:${gp.opcodeName}`, gp);
}
} else if (this.remaining !== 0) {
return;
}
}
}
// Auth challenge handler (SMSG_AUTH_CHALLENGE)
handleAuthChallenge(gp) {
console.info('handling auth challenge');
gp.readUnsignedInt(); // (0x01)
const salt = gp.read(4);
const seed = BigNum.fromRand(4);
const hash = new SHA1();
hash.feed(this.session.auth.account);
hash.feed([0, 0, 0, 0]);
hash.feed(seed.toArray());
hash.feed(salt);
hash.feed(this.session.auth.key);
const build = this.session.config.build;
const account = this.session.auth.account;
const size = GamePacket.HEADER_SIZE_OUTGOING + 8 + this.session.auth.account.length + 1 + 4 + 4 + 20 + 20 + 4;
const app = new GamePacket(GameOpcode.CMSG_AUTH_PROOF, size);
app.writeUnsignedInt(build); // build
app.writeUnsignedInt(0); // (?)
app.writeCString(account); // account
app.writeUnsignedInt(0); // (?)
app.write(seed.toArray()); // client-seed
app.writeUnsignedInt(0); // (?)
app.writeUnsignedInt(0); // (?)
app.writeUnsignedInt(0); // (?)
app.writeUnsignedInt(0); // (?)
app.writeUnsignedInt(0); // (?)
app.write(hash.digest); // digest
app.writeUnsignedInt(0); // addon-data
this.send(app);
this._crypt = new Crypt();
this._crypt.key = this.session.auth.key;
}
// Auth response handler (SMSG_AUTH_RESPONSE)
handleAuthResponse(gp) {
console.info('handling auth response');
// Handle result byte
const result = gp.readUnsignedByte();
if (result === 0x0D) {
console.warn('server-side auth/realm failure; try again');
this.emit('reject');
return;
}
if (result === 0x15) {
console.warn('account in use/invalid; aborting');
this.emit('reject');
return;
}
// TODO: Ensure the account is flagged as WotLK (expansion //2)
this.emit('authenticate');
}
// World login handler (SMSG_LOGIN_VERIFY_WORLD)
handleWorldLogin(_gp) {
this.emit('join');
}
}
export default GameHandler;
================================================
FILE: src/lib/game/opcode.js
================================================
class GameOpcode {
static CMSG_CHAR_ENUM = 0x0037;
static SMSG_CHAR_ENUM = 0x003B;
static CMSG_PLAYER_LOGIN = 0x003D;
static SMSG_CHARACTER_LOGIN_FAILED = 0x0041;
static SMSG_LOGIN_SETTIMESPEED = 0x0042;
static SMSG_CONTACT_LIST = 0x0067;
static CMSG_MESSAGE_CHAT = 0x0095;
static SMSG_MESSAGE_CHAT = 0x0096;
static SMSG_UPDATE_OBJECT = 0x00A9;
static SMSG_MONSTER_MOVE = 0x00DD;
static SMSG_TUTORIAL_FLAGS = 0x00FD;
static SMSG_INITIALIZE_FACTIONS = 0x0122;
static SMSG_SET_PROFICIENCY = 0x0127;
static SMSG_ACTION_BUTTONS = 0x0129;
static SMSG_INITIAL_SPELLS = 0x012A;
static SMSG_SPELL_START = 0x0131;
static SMSG_SPELL_GO = 0x0132;
static SMSG_BINDPOINT_UPDATE = 0x0155;
static SMSG_ITEM_TIME_UPDATE = 0x01EA;
static SMSG_AUTH_CHALLENGE = 0x01EC;
static CMSG_AUTH_PROOF = 0x01ED;
static SMSG_AUTH_RESPONSE = 0x01EE;
static SMSG_COMPRESSED_UPDATE_OBJECT = 0x01F6;
static SMSG_ACCOUNT_DATA_TIMES = 0x0209;
static SMSG_LOGIN_VERIFY_WORLD = 0x0236;
static SMSG_SPELL_NON_MELEE_DAMAGE_LOG = 0x0250;
static SMSG_INIT_WORLD_STATES = 0x02C2;
static SMSG_UPDATE_WORLD_STATE = 0x02C3;
static SMSG_WEATHER = 0x02F4;
static MSG_SET_DUNGEON_DIFFICULTY = 0x0329;
static SMSG_UPDATE_INSTANCE_OWNERSHIP = 0x032B;
static SMSG_INSTANCE_DIFFICULTY = 0x033B;
static SMSG_MOTD = 0x033D;
static SMSG_TIME_SYNC_REQ = 0x0390;
static SMSG_FEATURE_SYSTEM_STATUS = 0x03C9;
static SMSG_SERVER_BUCK_DATA = 0x041D;
static SMSG_SEND_UNLEARN_SPELLS = 0x041E;
static SMSG_LEARNED_DANCE_MOVES = 0x0455;
static SMSG_ALL_ACHIEVEMENT_DATA = 0x047D;
static SMSG_POWER_UPDATE = 0x0480;
static SMSG_AURA_UPDATE_ALL = 0x0495;
static SMSG_AURA_UPDATE = 0x0496;
static SMSG_EQUIPMENT_SET_LIST = 0x04BC;
static SMSG_TALENTS_INFO = 0x04C0;
static MSG_SET_RAID_DIFFICULTY = 0x04EB;
}
export default GameOpcode;
================================================
FILE: src/lib/game/packet.js
================================================
import BasePacket from '../net/packet';
import GameOpcode from './opcode';
import GUID from './guid';
import ObjectUtil from '../utils/object-util';
class GamePacket extends BasePacket {
// Header sizes in bytes for both incoming and outgoing packets
static HEADER_SIZE_INCOMING = 4;
static HEADER_SIZE_OUTGOING = 6;
// Opcode sizes in bytes for both incoming and outgoing packets
static OPCODE_SIZE_INCOMING = 2;
static OPCODE_SIZE_OUTGOING = 4;
constructor(opcode, source, outgoing = true) {
if (!source) {
source = (outgoing) ? GamePacket.HEADER_SIZE_OUTGOING : GamePacket.HEADER_SIZE_INCOMING;
}
super(opcode, source, outgoing);
}
// Retrieves the name of the opcode for this packet (if available)
get opcodeName() {
return ObjectUtil.keyByValue(GameOpcode, this.opcode);
}
// Header size in bytes (dependent on packet origin)
get headerSize() {
if (this.outgoing) {
return this.constructor.HEADER_SIZE_OUTGOING;
}
return this.constructor.HEADER_SIZE_INCOMING;
}
// Reads GUID from this packet
readGUID() {
return new GUID(this.read(GUID.LENGTH));
}
// Writes given GUID to this packet
writeGUID(guid) {
this.write(guid.raw);
return this;
}
// // Reads packed GUID from this packet
// // TODO: Implementation
// readPackedGUID: ->
// return null
// // Writes given GUID to this packet in packed form
// // TODO: Implementation
// writePackedGUID: (guid) ->
// return this
}
export default GamePacket;
================================================
FILE: src/lib/game/player.js
================================================
import Unit from './unit';
class Player extends Unit {
constructor() {
super();
this.name = 'Player';
this.hp = this.hp;
this.mp = this.mp;
this.target = null;
this.displayID = 24978;
this.mapID = null;
}
worldport(mapID, x, y, z) {
if (!this.mapID || this.mapID !== mapID) {
this.mapID = mapID;
this.emit('map:change', mapID);
}
this.position.set(x, y, z);
this.emit('position:change', this);
}
}
export default Player;
================================================
FILE: src/lib/game/unit.js
================================================
import THREE from 'three';
import DBC from '../pipeline/dbc';
import Entity from './entity';
import M2Blueprint from '../pipeline/m2/blueprint';
class Unit extends Entity {
constructor() {
super();
this.name = '<unknown>';
this.level = '?';
this.target = null;
this.maxHp = 0;
this.hp = 0;
this.maxMp = 0;
this.mp = 0;
this.rotateSpeed = 2;
this.moveSpeed = 40;
this._view = new THREE.Group();
this._displayID = 0;
this._model = null;
}
get position() {
return this._view.position;
}
get displayID() {
return this._displayID;
}
set displayID(displayID) {
if (!displayID) {
return;
}
DBC.load('CreatureDisplayInfo', displayID).then((displayInfo) => {
this._displayID = displayID;
this.displayInfo = displayInfo;
const { modelID } = displayInfo;
DBC.load('CreatureModelData', modelID).then((modelData) => {
this.modelData = modelData;
this.modelData.path = this.modelData.file.match(/^(.+?)(?:[^\\]+)$/)[1];
this.displayInfo.modelData = this.modelData;
M2Blueprint.load(this.modelData.file).then((m2) => {
m2.displayInfo = this.displayInfo;
this.model = m2;
});
});
});
}
get view() {
return this._view;
}
get model() {
return this._model;
}
set model(m2) {
// TODO: Should this support multiple models? Mounts?
if (this._model) {
this.view.remove(this._model);
}
// TODO: Figure out whether this 180 degree rotation is correct
m2.rotation.z = Math.PI;
m2.updateMatrix();
this.view.add(m2);
// Auto-play animation index 0 in unit model, if present
// TODO: Properly manage unit animations
if (m2.animated && m2.animations.length > 0) {
m2.animations.playAnimation(0);
m2.animations.playAllSequences();
}
this.emit('model:change', this, this._model, m2);
this._model = m2;
}
ascend(delta) {
this.view.translateZ(this.moveSpeed * delta);
this.emit('position:change', this);
}
descend(delta) {
this.view.translateZ(-this.moveSpeed * delta);
this.emit('position:change', this);
}
moveForward(delta) {
this.view.translateX(this.moveSpeed * delta);
this.emit('position:change', this);
}
moveBackward(delta) {
this.view.translateX(-this.moveSpeed * delta);
this.emit('position:change', this);
}
rotateLeft(delta) {
this.view.rotateZ(this.rotateSpeed * delta);
this.emit('position:change', this);
}
rotateRight(delta) {
this.view.rotateZ(-this.rotateSpeed * delta);
this.emit('position:change', this);
}
strafeLeft(delta) {
this.view.translateY(this.moveSpeed * delta);
this.emit('position:change', this);
}
strafeRight(delta) {
this.view.translateY(-this.moveSpeed * delta);
this.emit('position:change', this);
}
}
export default Unit;
================================================
FILE: src/lib/game/world/content-queue.js
================================================
class ContentQueue {
constructor(processor, interval = 1, workFactor = 1, minWork = 1) {
this.processor = processor;
this.interval = interval;
this.workFactor = workFactor;
this.minWork = minWork;
this.queue = new Map();
this.schedule = ::this.schedule;
this.run = ::this.run;
this.schedule();
}
has(key) {
return this.queue.has(key);
}
add(key, job) {
if (this.queue.has(key)) {
return;
}
this.queue.set(key, job);
}
remove(key) {
let count = 0;
if (this.queue.has(key)) {
this.queue.delete(key);
count++;
}
return count;
}
schedule() {
setTimeout(this.run, this.interval);
}
run() {
let count = 0;
const max = Math.min(this.queue.size * this.workFactor, this.minWork);
for (const entry of this.queue) {
const [key, job] = entry;
this.processor(job);
this.queue.delete(key);
count++;
if (count > max) {
break;
}
}
this.schedule();
}
clear() {
this.queue.clear();
}
}
export default ContentQueue;
================================================
FILE: src/lib/game/world/doodad-manager.js
================================================
import M2Blueprint from '../../pipeline/m2/blueprint';
class DoodadManager {
// Proportion of pending doodads to load or unload in a given tick.
static LOAD_FACTOR = 1 / 40;
// Minimum number of pending doodads to load or unload in a given tick.
static MINIMUM_LOAD_THRESHOLD = 2;
// Number of milliseconds to wait before loading another portion of doodads.
static LOAD_INTERVAL = 1;
constructor(map) {
this.map = map;
this.chunkRefs = new Map();
this.doodads = new Map();
this.animatedDoodads = new Map();
this.entriesPendingLoad = new Map();
this.entriesPendingUnload = new Map();
this.loadChunk = ::this.loadChunk;
this.unloadChunk = ::this.unloadChunk;
this.loadDoodads = ::this.loadDoodads;
this.unloadDoodads = ::this.unloadDoodads;
// Kick off intervals.
this.loadDoodads();
this.unloadDoodads();
}
// Process a set of doodad entries for a given chunk index of the world map.
loadChunk(index, entries) {
for (let i = 0, len = entries.length; i < len; ++i) {
const entry = entries[i];
let chunkRefs;
// Fetch or create chunk references for entry.
if (this.chunkRefs.has(entry.id)) {
chunkRefs = this.chunkRefs.get(entry.id);
} else {
chunkRefs = new Set();
this.chunkRefs.set(entry.id, chunkRefs);
}
// Add chunk reference to entry.
chunkRefs.add(index);
// If the doodad is pending unload, remove the pending unload.
if (this.entriesPendingUnload.has(entry.id)) {
this.entriesPendingUnload.delete(entry.id);
}
// Add to pending loads. Actual loading is done by interval.
this.entriesPendingLoad.set(entry.id, entry);
}
}
unloadChunk(index, entries) {
for (let i = 0, len = entries.length; i < len; ++i) {
const entry = entries[i];
const chunkRefs = this.chunkRefs.get(entry.id);
// Remove chunk reference for entry.
chunkRefs.delete(index);
// If at least one chunk reference remains for entry, leave loaded. Typically happens in
// cases where a doodad is shared across multiple chunks.
if (chunkRefs.size > 0) {
continue;
}
// No chunk references remain, so we should remove from pending loads if necessary.
if (this.entriesPendingLoad.has(entry.id)) {
this.entriesPendingLoad.delete(entry.id);
}
// Add to pending unloads. Actual unloading is done by interval.
this.entriesPendingUnload.set(entry.id, entry);
}
}
// Every tick of the load interval, load a portion of any doodads pending load.
loadDoodads() {
let count = 0;
for (const entry of this.entriesPendingLoad.values()) {
if (this.doodads.has(entry.id)) {
this.entriesPendingLoad.delete(entry.id);
continue;
}
this.loadDoodad(entry);
this.entriesPendingLoad.delete(entry.id);
++count;
const shouldYield = count >= this.constructor.MINIMUM_LOAD_THRESHOLD &&
count > this.entriesPendingLoad.size * this.constructor.LOAD_FACTOR;
if (shouldYield) {
setTimeout(this.loadDoodads, this.constructor.LOAD_INTERVAL);
return;
}
}
setTimeout(this.loadDoodads, this.constructor.LOAD_INTERVAL);
}
loadDoodad(entry) {
M2Blueprint.load(entry.filename).then((doodad) => {
if (this.entriesPendingUnload.has(entry.id)) {
return;
}
doodad.entryID = entry.id;
this.doodads.set(entry.id, doodad);
this.placeDoodad(doodad, entry.position, entry.rotation, entry.scale);
if (doodad.animated) {
this.enableDoodadAnimations(entry, doodad);
}
});
}
enableDoodadAnimations(entry, doodad) {
// Maintain separate entries for animated doodads to avoid excessive iterations on each
// call to animate() during the render loop.
this.animatedDoodads.set(entry.id, doodad);
// Auto-play animation index 0 in doodad, if animations are present.
// TODO: Properly manage doodad animations.
if (doodad.animations.length > 0) {
doodad.animations.playAnimation(0);
doodad.animations.playAllSequences();
}
}
// Every tick of the load interval, unload a portion of any doodads pending unload.
unloadDoodads() {
let count = 0;
for (const entry of this.entriesPendingUnload.values()) {
// If the doodad was already unloaded, remove it from the pending unloads.
if (!this.doodads.has(entry.id)) {
this.entriesPendingUnload.delete(entry.id);
continue;
}
this.unloadDoodad(entry);
this.entriesPendingUnload.delete(entry.id);
++count;
const shouldYield = count >= this.constructor.MINIMUM_LOAD_THRESHOLD &&
count > this.entriesPendingUnload.size * this.constructor.LOAD_FACTOR;
if (shouldYield) {
setTimeout(this.unloadDoodads, this.constructor.LOAD_INTERVAL);
return;
}
}
setTimeout(this.unloadDoodads, this.constructor.LOAD_INTERVAL);
return;
}
unloadDoodad(entry) {
const doodad = this.doodads.get(entry.id);
this.doodads.delete(entry.id);
this.animatedDoodads.delete(entry.id);
this.map.remove(doodad);
M2Blueprint.unload(doodad);
}
// Place a doodad on the world map, adhereing to a provided position, rotation, and scale.
placeDoodad(doodad, position, rotation, scale) {
doodad.position.set(
-(position.z - this.map.constructor.ZEROPOINT),
-(position.x - this.map.constructor.ZEROPOINT),
position.y
);
// Provided as (Z, X, -Y)
doodad.rotation.set(
rotation.z * Math.PI / 180,
rotation.x * Math.PI / 180,
-rotation.y * Math.PI / 180
);
// Adjust doodad rotation to match Wowser's axes.
const quat = doodad.quaternion;
quat.set(quat.x, quat.y, quat.z, -quat.w);
if (scale !== 1024) {
const scaleFloat = scale / 1024;
doodad.scale.set(scaleFloat, scaleFloat, scaleFloat);
}
// Add doodad to world map.
this.map.add(doodad);
doodad.updateMatrix();
}
animate(delta, camera, cameraMoved) {
this.animatedDoodads.forEach((doodad) => {
if (!doodad.visible) {
return;
}
if (doodad.receivesAnimationUpdates && doodad.animations.length > 0) {
doodad.animations.update(delta);
}
if (cameraMoved && doodad.billboards.length > 0) {
doodad.applyBillboards(camera);
}
if (doodad.skeletonHelper) {
doodad.skeletonHelper.update();
}
});
}
}
export default DoodadManager;
================================================
FILE: src/lib/game/world/handler.js
================================================
import EventEmitter from 'events';
import THREE from 'three';
import M2Blueprint from '../../pipeline/m2/blueprint';
import WorldMap from './map';
class WorldHandler extends EventEmitter {
constructor(session) {
super();
this.session = session;
this.player = this.session.player;
this.scene = new THREE.Scene();
this.scene.matrixAutoUpdate = false;
this.map = null;
this.changeMap = ::this.changeMap;
this.changeModel = ::this.changeModel;
this.changePosition = ::this.changePosition;
this.entities = new Set();
this.add(this.player);
this.player.on('map:change', this.changeMap);
this.player.on('position:change', this.changePosition);
// Darkshire (Eastern Kingdoms)
this.player.worldport(0, -10559, -1189, 28);
// Booty Bay (Eastern Kingdoms)
// this.player.worldport(0, -14354, 518, 22);
// Stonewrought Dam (Eastern Kingdoms)
// this.player.worldport(0, -4651, -3316, 296);
// Ironforge (Eastern Kingdoms)
// this.player.worldport(0, -4981.25, -881.542, 502.66);
// Darnassus (Kalimdor)
// this.player.worldport(1, 9947, 2557, 1316);
// Astranaar (Kalimdor)
// this.player.worldport(1, 2752, -348, 107);
// Moonglade (Kalimdor)
// this.player.worldport(1, 7827, -2425, 489);
// Un'Goro Crater (Kalimdor)
// this.player.worldport(1, -7183, -1394, -183);
// Everlook (Kalimdor)
// this.player.worldport(1, 6721.44, -4659.09, 721.893);
// Stonetalon Mountains (Kalimdor)
// this.player.worldport(1, 2506.3, 1470.14, 263.722);
// Mulgore (Kalimdor)
// this.player.worldport(1, -1828.913, -426.307, 6.299);
// Thunderbluff (Kalimdor)
// this.player.worldport(1, -1315.901, 138.6357, 302.008);
// Auberdine (Kalimdor)
// this.player.worldport(1, 6355.151, 508.831, 15.859);
// The Exodar (Expansion 01)
// this.player.worldport(530, -4013, -11894, -2);
// Nagrand (Expansion 01)
// this.player.worldport(530, -743.149, 8385.114, 33.435);
// Eversong Woods (Expansion 01)
// this.player.worldport(530, 9152.441, -7442.229, 68.144);
// Daggercap Bay (Northrend)
// this.player.worldport(571, 1031, -5192, 180);
// Dalaran (Northrend)
// this.player.worldport(571, 5797, 629, 647);
}
add(entity) {
this.entities.add(entity);
if (entity.view) {
this.scene.add(entity.view);
entity.on('model:change', this.changeModel);
}
}
remove(entity) {
this.entity.delete(entity);
if (entity.view) {
this.scene.remove(entity.view);
entity.removeListener('model:change', this.changeModel);
}
}
renderAtCoords(x, y) {
if (!this.map) {
return;
}
this.map.render(x, y);
}
changeMap(mapID) {
WorldMap.load(mapID).then((map) => {
if (this.map) {
this.scene.remove(this.map);
}
this.map = map;
this.scene.add(this.map);
this.renderAtCoords(this.player.position.x, this.player.position.y);
});
}
changeModel(_unit, _oldModel, _newModel) {
}
changePosition(player) {
this.renderAtCoords(player.position.x, player.position.y);
}
animate(delta, camera, cameraMoved) {
this.animateEntities(delta, camera, cameraMoved);
if (this.map !== null) {
this.map.animate(delta, camera, cameraMoved);
}
// Send delta updates to instanced M2 animation managers.
M2Blueprint.animate(delta);
}
animateEntities(delta, camera, cameraMoved) {
this.entities.forEach((entity) => {
const { model } = entity;
if (model === null || !model.animated) {
return;
}
if (model.receivesAnimationUpdates && model.animations.length > 0) {
model.animations.update(delta);
}
if (cameraMoved && model.billboards.length > 0) {
model.applyBillboards(camera);
}
if (model.skeletonHelper) {
model.skeletonHelper.update();
}
});
}
}
export default WorldHandler;
================================================
FILE: src/lib/game/world/map.js
================================================
import THREE from 'three';
import ADT from '../../pipeline/adt';
import Chunk from '../../pipeline/adt/chunk';
import DBC from '../../pipeline/dbc';
import WDT from '../../pipeline/wdt';
import DoodadManager from './doodad-manager';
import WMOManager from './wmo-manager';
import TerrainManager from './terrain-manager';
class WorldMap extends THREE.Group {
static ZEROPOINT = ADT.SIZE * 32;
static CHUNKS_PER_ROW = 64 * 16;
// Controls when ADT chunks are loaded and unloaded from the map.
static CHUNK_RENDER_RADIUS = 12;
constructor(data, wdt) {
super();
this.matrixAutoUpdate = false;
this.terrainManager = new TerrainManager(this);
this.doodadManager = new DoodadManager(this);
this.wmoManager = new WMOManager(this);
this.data = data;
this.wdt = wdt;
this.mapID = this.data.id;
this.chunkX = null;
this.chunkY = null;
this.queuedChunks = new Map();
this.chunks = new Map();
}
get internalName() {
return this.data.internalName;
}
render(x, y) {
const chunkX = Chunk.chunkFor(x);
const chunkY = Chunk.chunkFor(y);
if (this.chunkX === chunkX && this.chunkY === chunkY) {
return;
}
this.chunkX = chunkX;
this.chunkY = chunkY;
const radius = this.constructor.CHUNK_RENDER_RADIUS;
const indices = this.chunkIndicesAround(chunkX, chunkY, radius);
indices.forEach((index) => {
this.loadChunkByIndex(index);
});
this.chunks.forEach((_chunk, index) => {
if (indices.indexOf(index) === -1) {
this.unloadChunkByIndex(index);
}
});
}
chunkIndicesAround(chunkX, chunkY, radius) {
const perRow = this.constructor.CHUNKS_PER_ROW;
const base = this.indexFor(chunkX, chunkY);
const indices = [];
for (let y = -radius; y <= radius; ++y) {
for (let x = -radius; x <= radius; ++x) {
indices.push(base + y * perRow + x);
}
}
return indices;
}
loadChunkByIndex(index) {
if (this.queuedChunks.has(index)) {
return;
}
const perRow = this.constructor.CHUNKS_PER_ROW;
const chunkX = (index / perRow) | 0;
const chunkY = index % perRow;
this.queuedChunks.set(index, Chunk.load(this, chunkX, chunkY).then((chunk) => {
this.chunks.set(index, chunk);
this.terrainManager.loadChunk(index, chunk);
this.doodadManager.loadChunk(index, chunk.doodadEntries);
this.wmoManager.loadChunk(index, chunk.wmoEntries);
}));
}
unloadChunkByIndex(index) {
const chunk = this.chunks.get(index);
if (!chunk) {
return;
}
this.terrainManager.unloadChunk(index, chunk);
this.doodadManager.unloadChunk(index, chunk.doodadEntries);
this.wmoManager.unloadChunk(index, chunk.wmoEntries);
this.queuedChunks.delete(index);
this.chunks.delete(index);
}
indexFor(chunkX, chunkY) {
return chunkX * 64 * 16 + chunkY;
}
animate(delta, camera, cameraMoved) {
this.doodadManager.animate(delta, camera, cameraMoved);
this.wmoManager.animate(delta, camera, cameraMoved);
}
static load(id) {
return DBC.load('Map', id).then((data) => {
const { internalName: name } = data;
return WDT.load(`World\\Maps\\${name}\\${name}.wdt`).then((wdt) => {
return new this(data, wdt);
});
});
}
}
export default WorldMap;
================================================
FILE: src/lib/game/world/terrain-manager.js
================================================
class TerrainManager {
constructor(map) {
this.map = map;
}
loadChunk(_index, terrain) {
this.map.add(terrain);
terrain.updateMatrix();
}
unloadChunk(_index, terrain) {
this.map.remove(terrain);
terrain.dispose();
}
}
export default TerrainManager;
================================================
FILE: src/lib/game/world/wmo-manager/index.js
================================================
import ContentQueue from '../content-queue';
import WMOHandler from './wmo-handler';
import WMOBlueprint from '../../../pipeline/wmo/blueprint';
class WMOManager {
static LOAD_ENTRY_INTERVAL = 1;
static LOAD_ENTRY_WORK_FACTOR = 1 / 10;
static LOAD_ENTRY_WORK_MIN = 2;
static UNLOAD_DELAY_INTERVAL = 30000;
constructor(map) {
this.map = map;
this.chunkRefs = new Map();
this.counters = {
loadingEntries: 0,
loadedEntries: 0,
loadingGroups: 0,
loadedGroups: 0,
loadingDoodads: 0,
loadedDoodads: 0,
animatedDoodads: 0
};
this.entries = new Map();
this.queues = {
loadEntry: new ContentQueue(
::this.processLoadEntry,
this.constructor.LOAD_ENTRY_INTERVAL,
this.constructor.LOAD_ENTRY_WORK_FACTOR,
this.constructor.LOAD_ENTRY_WORK_MIN
)
};
}
loadChunk(chunkIndex, wmoEntries) {
for (let i = 0, len = wmoEntries.length; i < len; ++i) {
const wmoEntry = wmoEntries[i];
this.addChunkRef(chunkIndex, wmoEntry);
this.cancelUnloadEntry(wmoEntry);
this.enqueueLoadEntry(wmoEntry);
}
}
unloadChunk(chunkIndex, wmoEntries) {
for (let i = 0, len = wmoEntries.length; i < len; ++i) {
const wmoEntry = wmoEntries[i];
const refCount = this.removeChunkRef(chunkIndex, wmoEntry);
// Still has a chunk reference; don't queue for unload.
if (refCount > 0) {
continue;
}
this.dequeueLoadEntry(wmoEntry);
this.scheduleUnloadEntry(wmoEntry);
}
}
addChunkRef(chunkIndex, wmoEntry) {
let chunkRefs;
// Fetch or create chunk references for entry.
if (this.chunkRefs.has(wmoEntry.id)) {
chunkRefs = this.chunkRefs.get(wmoEntry.id);
} else {
chunkRefs = new Set();
this.chunkRefs.set(wmoEntry.id, chunkRefs);
}
// Add chunk reference to entry.
chunkRefs.add(chunkIndex);
const refCount = chunkRefs.size;
return refCount;
}
removeChunkRef(chunkIndex, wmoEntry) {
const chunkRefs = this.chunkRefs.get(wmoEntry.id);
// Remove chunk reference for entry.
chunkRefs.delete(chunkIndex);
const refCount = chunkRefs.size;
if (chunkRefs.size === 0) {
this.chunkRefs.delete(wmoEntry.id);
}
return refCount;
}
enqueueLoadEntry(wmoEntry) {
const key = wmoEntry.id;
// Already loading or loaded.
if (this.queues.loadEntry.has(key) || this.entries.has(key)) {
return;
}
this.queues.loadEntry.add(key, wmoEntry);
this.counters.loadingEntries++;
}
dequeueLoadEntry(wmoEntry) {
const key = wmoEntry.key;
// Not loading.
if (!this.queues.loadEntry.has(key)) {
return;
}
this.queues.loadEntry.remove(key);
this.counters.loadingEntries--;
}
scheduleUnloadEntry(wmoEntry) {
const wmoHandler = this.entries.get(wmoEntry.id);
if (!wmoHandler) {
return;
}
wmoHandler.scheduleUnload(this.constructor.UNLOAD_DELAY_INTERVAL);
}
cancelUnloadEntry(wmoEntry) {
const wmoHandler = this.entries.get(wmoEntry.id);
if (!wmoHandler) {
return;
}
wmoHandler.cancelUnload();
}
processLoadEntry(wmoEntry) {
const wmoHandler = new WMOHandler(this, wmoEntry);
this.entries.set(wmoEntry.id, wmoHandler);
WMOBlueprint.load(wmoEntry.filename).then((wmoRoot) => {
wmoHandler.load(wmoRoot);
this.counters.loadingEntries--;
this.counters.loadedEntries++;
});
}
animate(delta, camera, cameraMoved) {
this.entries.forEach((wmoHandler) => {
wmoHandler.animate(delta, camera, cameraMoved);
});
}
}
export default WMOManager;
================================================
FILE: src/lib/game/world/wmo-manager/wmo-handler.js
================================================
import ContentQueue from '../content-queue';
import WMOBlueprint from '../../../pipeline/wmo/blueprint';
import WMOGroupBlueprint from '../../../pipeline/wmo/group/blueprint';
import M2Blueprint from '../../../pipeline/m2/blueprint';
class WMOHandler {
static LOAD_GROUP_INTERVAL = 1;
static LOAD_GROUP_WORK_FACTOR = 1 / 10;
static LOAD_GROUP_WORK_MIN = 2;
static LOAD_DOODAD_INTERVAL = 1;
static LOAD_DOODAD_WORK_FACTOR = 1 / 20;
static LOAD_DOODAD_WORK_MIN = 2;
constructor(manager, entry) {
this.manager = manager;
this.entry = entry;
this.root = null;
this.groups = new Map();
this.doodads = new Map();
this.animatedDoodads = new Map();
this.doodadSet = [];
this.doodadRefs = new Map();
this.counters = {
loadingGroups: 0,
loadingDoodads: 0,
loadedGroups: 0,
loadedDoodads: 0,
animatedDoodads: 0
};
this.queues = {
loadGroup: new ContentQueue(
::this.processLoadGroup,
this.constructor.LOAD_GROUP_INTERVAL,
this.constructor.LOAD_GROUP_WORK_FACTOR,
this.constructor.LOAD_GROUP_WORK_MIN
),
loadDoodad: new ContentQueue(
::this.processLoadDoodad,
this.constructor.LOAD_DOODAD_INTERVAL,
this.constructor.LOAD_DOODAD_WORK_FACTOR,
this.constructor.LOAD_DOODAD_WORK_MIN
)
};
this.pendingUnload = null;
this.unloading = false;
}
load(wmoRoot) {
this.root = wmoRoot;
this.doodadSet = this.root.doodadSet(this.entry.doodadSet);
this.placeRoot();
this.enqueueLoadGroups();
}
enqueueLoadGroups() {
const outdoorGroupIDs = this.root.outdoorGroupIDs;
const indoorGroupIDs = this.root.indoorGroupIDs;
for (let ogi = 0, oglen = outdoorGroupIDs.length; ogi < oglen; ++ogi) {
const wmoGroupID = outdoorGroupIDs[ogi];
this.enqueueLoadGroup(wmoGroupID);
}
for (let igi = 0, iglen = indoorGroupIDs.length; igi < iglen; ++igi) {
const wmoGroupID = indoorGroupIDs[igi];
this.enqueueLoadGroup(wmoGroupID);
}
}
enqueueLoadGroup(wmoGroupID) {
// Already loaded.
if (this.groups.has(wmoGroupID)) {
return;
}
this.queues.loadGroup.add(wmoGroupID, wmoGroupID);
this.manager.counters.loadingGroups++;
this.counters.loadingGroups++;
}
processLoadGroup(wmoGroupID) {
// Already loaded.
if (this.groups.has(wmoGroupID)) {
this.manager.counters.loadingGroups--;
this.counters.loadingGroups--;
return;
}
WMOGroupBlueprint.loadWithID(this.root, wmoGroupID).then((wmoGroup) => {
if (this.unloading) {
return;
}
this.loadGroup(wmoGroupID, wmoGroup);
this.manager.counters.loadingGroups--;
this.counters.loadingGroups--;
this.manager.counters.loadedGroups++;
this.counters.loadedGroups++;
});
}
loadGroup(wmoGroupID, wmoGroup) {
this.placeGroup(wmoGroup);
this.groups.set(wmoGroupID, wmoGroup);
if (wmoGroup.data.MODR) {
this.enqueueLoadGroupDoodads(wmoGroup);
}
}
enqueueLoadGroupDoodads(wmoGroup) {
wmoGroup.data.MODR.doodadIndices.forEach((doodadIndex) => {
const wmoDoodadEntry = this.doodadSet[doodadIndex];
// Since the doodad set is filtered based on the requested set in the entry, not all
// doodads referenced by a group will be present.
if (!wmoDoodadEntry) {
return;
}
// Assign the index as an id property on the entry.
wmoDoodadEntry.id = doodadIndex;
const refCount = this.addDoodadRef(wmoDoodadEntry, wmoGroup);
// Only enqueue load on the first reference, since it'll already have been enqueued on
// subsequent references.
if (refCount === 1) {
this.enqueueLoadDoodad(wmoDoodadEntry);
}
});
}
enqueueLoadDoodad(wmoDoodadEntry) {
// Already loading or loaded.
if (this.queues.loadDoodad.has(wmoDoodadEntry.id) || this.doodads.has(wmoDoodadEntry.id)) {
return;
}
this.queues.loadDoodad.add(wmoDoodadEntry.id, wmoDoodadEntry);
this.manager.counters.loadingDoodads++;
this.counters.loadingDoodads++;
}
processLoadDoodad(wmoDoodadEntry) {
// Already loaded.
if (this.doodads.has(wmoDoodadEntry.id)) {
this.manager.counters.loadingDoodads--;
this.counters.loadingDoodads--;
return;
}
M2Blueprint.load(wmoDoodadEntry.filename).then((wmoDoodad) => {
if (this.unloading) {
return;
}
this.loadDoodad(wmoDoodadEntry, wmoDoodad);
this.manager.counters.loadingDoodads--;
this.counters.loadingDoodads--;
this.manager.counters.loadedDoodads++;
this.counters.loadedDoodads++;
if (wmoDoodad.animated) {
this.manager.counters.animatedDoodads++;
this.counters.animatedDoodads++;
}
});
}
loadDoodad(wmoDoodadEntry, wmoDoodad) {
wmoDoodad.entryID = wmoDoodadEntry.id;
this.placeDoodad(wmoDoodadEntry, wmoDoodad);
if (wmoDoodad.animated) {
this.animatedDoodads.set(wmoDoodadEntry.id, wmoDoodad);
if (wmoDoodad.animations.length > 0) {
// TODO: Do WMO doodads have more than one animation? If so, which one should play?
wmoDoodad.animations.playAnimation(0);
wmoDoodad.animations.playAllSequences();
}
}
this.doodads.set(wmoDoodadEntry.id, wmoDoodad);
}
scheduleUnload(unloadDelay = 0) {
this.pendingUnload = setTimeout(::this.unload, unloadDelay);
}
cancelUnload() {
if (this.pendingUnload) {
clearTimeout(this.pendingUnload);
}
}
unload() {
this.unloading = true;
this.manager.entries.delete(this.entry.id);
this.manager.counters.loadedEntries--;
this.queues.loadGroup.clear();
this.queues.loadDoodad.clear();
this.manager.counters.loadingGroups -= this.counters.loadingGroups;
this.manager.counters.loadedGroups -= this.counters.loadedGroups;
this.manager.counters.loadingDoodads -= this.counters.loadingDoodads;
this.manager.counters.loadedDoodads -= this.counters.loadedDoodads;
this.manager.counters.animatedDoodads -= this.counters.animatedDoodads;
this.counters.loadingGroups = 0;
this.counters.loadedGroups = 0;
this.counters.loadingDoodads = 0;
this.counters.loadedDoodads = 0;
this.counters.animatedDoodads = 0;
this.manager.map.remove(this.root);
for (const wmoGroup of this.groups.values()) {
this.root.remove(wmoGroup);
WMOGroupBlueprint.unload(wmoGroup);
}
for (const wmoDoodad of this.doodads.values()) {
this.root.remove(wmoDoodad);
M2Blueprint.unload(wmoDoodad);
}
WMOBlueprint.unload(this.root);
this.groups = new Map();
this.doodads = new Map();
this.animatedDoodads = new Map();
this.doodadRefs = new Map();
this.root = null;
this.entry = null;
}
placeRoot() {
const { position, rotation } = this.entry;
this.root.position.set(
-(position.z - this.manager.map.constructor.ZEROPOINT),
-(position.x - this.manager.map.constructor.ZEROPOINT),
position.y
);
// Provided as (Z, X, -Y)
this.root.rotation.set(
rotation.z * Math.PI / 180,
rotation.x * Math.PI / 180,
-rotation.y * Math.PI / 180
);
// Adjust WMO rotation to match Wowser's axes.
const quat = this.root.quaternion;
quat.set(quat.x, quat.y, quat.z, -quat.w);
this.manager.map.add(this.root);
this.root.updateMatrix();
}
placeGroup(wmoGroup) {
this.root.add(wmoGroup);
wmoGroup.updateMatrix();
}
placeDoodad(wmoDoodadEntry, wmoDoodad) {
const { position, rotation, scale } = wmoDoodadEntry;
wmoDoodad.position.set(-position.x, -position.y, position.z);
// Adjust doodad rotation to match Wowser's axes.
const quat = wmoDoodad.quaternion;
quat.set(rotation.x, rotation.y, -rotation.z, -rotation.w);
wmoDoodad.scale.set(scale, scale, scale);
this.root.add(wmoDoodad);
wmoDoodad.updateMatrix();
}
addDoodadRef(wmoDoodadEntry, wmoGroup) {
const key = wmoDoodadEntry.id;
let doodadRefs;
// Fetch or create group references for doodad.
if (this.doodadRefs.has(key)) {
doodadRefs = this.doodadRefs.get(key);
} else {
doodadRefs = new Set();
this.doodadRefs.set(key, doodadRefs);
}
// Add group reference to doodad.
doodadRefs.add(wmoGroup.groupID);
const refCount = doodadRefs.size;
return refCount;
}
removeDoodadRef(wmoDoodadEntry, wmoGroup) {
const key = wmoDoodadEntry.id;
const doodadRefs = this.doodadRefs.get(key);
if (!doodadRefs) {
return 0;
}
// Remove group reference for doodad.
doodadRefs.delete(wmoGroup.groupID);
const refCount = doodadRefs.size;
if (doodadRefs.size === 0) {
this.doodadRefs.delete(key);
}
return refCount;
}
groupsForDoodad(wmoDoodad) {
const wmoGroupIDs = this.doodadRefs.get(wmoDoodad.entryID);
const wmoGroups = [];
for (const wmoGroupID of wmoGroupIDs) {
const wmoGroup = this.groups.get(wmoGroupID);
if (wmoGroup) {
wmoGroups.push(wmoGroup);
}
}
return wmoGroups;
}
doodadsForGroup(wmoGroup) {
const wmoDoodads = [];
for (const refs of this.doodadRefs) {
const [wmoDoodadEntryID, wmoGroupIDs] = refs;
if (wmoGroupIDs.has(wmoGroup.groupID)) {
const wmoDoodad = this.doodads.get(wmoDoodadEntryID);
if (wmoDoodad) {
wmoDoodads.push(wmoDoodad);
}
}
}
return wmoDoodads;
}
animate(delta, camera, cameraMoved) {
for (const wmoDoodad of this.animatedDoodads.values()) {
if (!wmoDoodad.visible) {
continue;
}
if (wmoDoodad.receivesAnimationUpdates && wmoDoodad.animations.length > 0) {
wmoDoodad.animations.update(delta);
}
if (cameraMoved && wmoDoodad.billboards.length > 0) {
wmoDoodad.applyBillboards(camera);
}
if (wmoDoodad.skeletonHelper) {
wmoDoodad.skeletonHelper.update();
}
}
}
}
export default WMOHandler;
================================================
FILE: src/lib/index.js
================================================
import EventEmitter from 'events';
import AuthHandler from './auth/handler';
import CharactersHandler from './characters/handler';
import ChatHandler from './game/chat/handler';
import Config from './config';
import GameHandler from './game/handler';
import Player from './game/player';
import RealmsHandler from './realms/handler';
import WorldHandler from './game/world/handler';
class Client extends EventEmitter {
constructor(config) {
super();
this.config = config || new Config();
this.auth = new AuthHandler(this);
this.realms = new RealmsHandler(this);
this.game = new GameHandler(this);
this.characters = new CharactersHandler(this);
this.chat = new ChatHandler(this);
this.player = new Player();
this.world = new WorldHandler(this);
}
}
export default Client;
================================================
FILE: src/lib/net/loader.js
================================================
import Promise from 'bluebird';
class Loader {
constructor() {
this.prefix = this.prefix || '/pipeline/';
this.responseType = this.responseType || 'arraybuffer';
}
load(path) {
return new Promise((resolve, _reject) => {
const uri = `${this.prefix}${path}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', encodeURI(uri), true);
xhr.onload = function(_event) {
// TODO: Handle failure
if (this.status >= 200 && this.status < 400) {
resolve(this.response);
}
};
xhr.responseType = this.responseType;
xhr.send();
});
}
}
export default Loader;
================================================
FILE: src/lib/net/packet.js
================================================
import ByteBuffer from 'byte-buffer';
class Packet extends ByteBuffer {
// Creates a new packet with given opcode from given source or length
constructor(opcode, source, outgoing = true) {
super(source, ByteBuffer.LITTLE_ENDIAN);
// Holds the opcode for this packet
this.opcode = opcode;
// Whether this packet is outgoing or incoming
this.outgoing = outgoing;
// Seek past opcode to reserve space for it when finalizing
this.index = this.headerSize;
}
// Header size in bytes
get headerSize() {
return this.constructor.HEADER_SIZE;
}
// Body size in bytes
get bodySize() {
return this.length - this.headerSize;
}
// Retrieves the name of the opcode for this packet (if available)
get opcodeName() {
return null;
}
// Short string representation of this packet
toString() {
const opcode = ('0000' + this.opcode.toString(16).toUpperCase()).slice(-4);
return `[${this.constructor.name}; Opcode: ${this.opcodeName || 'UNKNOWN'} (0x${opcode}); Length: ${this.length}; Body: ${this.bodySize}; Index: ${this._index}]`;
}
// Finalizes this packet
finalize() {
return this;
}
}
export default Packet;
================================================
FILE: src/lib/net/socket.js
================================================
import ByteBuffer from 'byte-buffer';
import EventEmitter from 'events';
// Base-class for any socket including signals and host/port management
class Socket extends EventEmitter {
// Maximum buffer capacity
// TODO: Arbitrarily chosen, determine this cap properly
static BUFFER_CAP = 2048;
// Creates a new socket
constructor() {
super();
// Holds the host, port and uri currently connected to (if any)
this.host = null;
this.port = NaN;
this.uri = null;
// Holds the actual socket
this.socket = null;
// Holds buffered data
this.buffer = null;
// Holds incoming packet's remaining size in bytes (false if no packet is being handled)
this.remaining = false;
}
// Whether this socket is currently connected
get connected() {
return this.socket && this.socket.readyState === WebSocket.OPEN;
}
// Connects to given host through given port (if any; default port is implementation specific)
connect(host, port = NaN) {
if (!this.connected) {
this.host = host;
this.port = port;
this.uri = 'ws://' + this.host + ':' + this.port;
this.buffer = new ByteBuffer(0, ByteBuffer.LITTLE_ENDIAN);
this.remaining = false;
this.socket = new WebSocket(this.uri, 'binary');
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = (e) => {
this.emit('connect', e);
};
this.socket.onclose = (e) => {
this.emit('disconnect', e);
};
this.socket.onmessage = (e) => {
const index = this.buffer.index;
this.buffer.end().append(e.data.byteLength).write(e.data);
this.buffer.index = index;
this.emit('data:receive', this);
if (this.buffer.available === 0 && this.buffer.length > this.constructor.BUFFER_CAP) {
this.buffer.clip();
}
};
this.socket.onerror = function(e) {
console.error(e);
};
}
return this;
}
// Attempts to reconnect to cached host and port
reconnect() {
if (!this.connected && this.host && this.port) {
this.connect(this.host, this.port);
}
return this;
}
// Disconnects this socket
disconnect() {
if (this.connected) {
this.socket.close();
}
return this;
}
// Finalizes and sends given packet
send(packet) {
if (this.connected) {
packet.finalize();
console.log('⟸', packet.toString());
// console.debug packet.toHex()
// console.debug packet.toASCII()
this.socket.send(packet.buffer);
this.emit('packet:send', packet);
return true;
}
return false;
}
}
export default Socket;
================================================
FILE: src/lib/pipeline/adt/chunk/index.js
================================================
import THREE from 'three';
import ADT from '../';
import Material from './material';
class Chunk extends THREE.Mesh {
static SIZE = 33.33333;
static UNIT_SIZE = 33.33333 / 8;
constructor(adt, id) {
super();
this.matrixAutoUpdate = false;
const data = this.data = adt.data.MCNKs[id];
const textureNames = adt.textures;
const size = this.constructor.SIZE;
const unitSize = this.constructor.UNIT_SIZE;
this.position.y = adt.y + -(data.indexX * size);
this.position.x = adt.x + -(data.indexY * size);
this.holes = data.holes;
const vertexCount = data.MCVT.heights.length;
const positions = new Float32Array(vertexCount * 3);
const normals = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
const uvsAlpha = new Float32Array(vertexCount * 2);
// See: http://www.pxr.dk/wowdev/wiki/index.php?title=ADT#MCVT_sub-chunk
data.MCVT.heights.forEach(function(height, index) {
let y = Math.floor(index / 17);
let x = index % 17;
if (x > 8) {
y += 0.5;
x -= 8.5;
}
// Mirror geometry over X and Y axes
positions[index * 3] = -(y * unitSize);
positions[index * 3 + 1] = -(x * unitSize);
positions[index * 3 + 2] = data.position.z + height;
uvs[index * 2] = x;
uvs[index * 2 + 1] = y;
uvsAlpha[index * 2] = x / 8;
uvsAlpha[index * 2 + 1] = y / 8;
});
data.MCNR.normals.forEach(function(normal, index) {
normals[index * 3] = normal.x;
normals[index * 3 + 1] = normal.z;
normals[index * 3 + 2] = normal.y;
});
const indices = new Uint32Array(8 * 8 * 4 * 3);
let faceIndex = 0;
const addFace = (index1, index2, index3) => {
indices[faceIndex * 3] = index1;
indices[faceIndex * 3 + 1] = index2;
indices[faceIndex * 3 + 2] = index3;
faceIndex++;
};
for (let y = 0; y < 8; ++y) {
for (let x = 0; x < 8; ++x) {
if (!this.isHole(y, x)) {
const index = 9 + y * 17 + x;
addFace(index, index - 9, index - 8);
addFace(index, index - 8, index + 9);
addFace(index, index + 9, index + 8);
addFace(index, index + 8, index - 9);
}
}
}
const geometry = this.geometry = new THREE.BufferGeometry();
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2));
geometry.addAttribute('uvAlpha', new THREE.BufferAttribute(uvsAlpha, 2));
this.material = new Material(data, textureNames);
}
get doodadEntries() {
return this.data.MCRF.doodadEntries;
}
get wmoEntries() {
return this.data.MCRF.wmoEntries;
}
isHole(y, x) {
const column = Math.floor(y / 2);
const row = Math.floor(x / 2);
const bit = 1 << (column * 4 + row);
return bit & this.holes;
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
static chunkFor(position) {
return 32 * 16 - (position / this.SIZE) | 0;
}
static tileFor(chunk) {
return (chunk / 16) | 0;
}
static load(map, chunkX, chunkY) {
const tileX = this.tileFor(chunkX);
const tileY = this.tileFor(chunkY);
const offsetX = chunkX - tileX * 16;
const offsetY = chunkY - tileY * 16;
const id = offsetX * 16 + offsetY;
return ADT.loadTile(map.internalName, tileX, tileY, map.wdt.data.flags).then((adt) => {
return new this(adt, id);
});
}
}
export default Chunk;
================================================
FILE: src/lib/pipeline/adt/chunk/material.js
================================================
import THREE from 'three';
import TextureLoader from '../../texture-loader';
import fragmentShader from './shader.frag';
import vertexShader from './shader.vert';
class Material extends THREE.ShaderMaterial {
constructor(data, textureNames) {
super();
this.layers = data.MCLY.layers;
this.rawAlphaMaps = data.MCAL.alphaMaps;
this.textureNames = textureNames;
this.vertexShader = vertexShader;
this.fragmentShader = fragmentShader;
this.side = THREE.BackSide;
this.layerCount = 0;
this.textures = [];
this.alphaMaps = [];
this.loadLayers();
this.uniforms = {
layerCount: { type: 'i', value: this.layerCount },
alphaMaps: { type: 'tv', value: this.alphaMaps },
textures: { type: 'tv', value: this.textures },
// Managed by light manager
lightModifier: { type: 'f', value: '1.0' },
ambientLight: { type: 'c', value: new THREE.Color(0.5, 0.5, 0.5) },
diffuseLight: { type: 'c', value: new THREE.Color(0.25, 0.5, 1.0) },
// Managed by light manager
fogModifier: { type: 'f', value: '1.0' },
fogColor: { type: 'c', value: new THREE.Color(0.25, 0.5, 1.0) },
fogStart: { type: 'f', value: 5.0 },
fogEnd: { type: 'f', value: 400.0 }
};
}
loadLayers() {
this.layerCount = this.layers.length;
this.loadAlphaMaps();
this.loadTextures();
}
loadAlphaMaps() {
const alphaMaps = [];
this.rawAlphaMaps.forEach((raw) => {
const texture = new THREE.DataTexture(raw, 64, 64);
texture.format = THREE.LuminanceFormat;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.needsUpdate = true;
alphaMaps.push(texture);
});
// Texture array uniforms must have at least one value present to be considered valid.
if (alphaMaps.length === 0) {
alphaMaps.push(new THREE.Texture());
}
this.alphaMaps = alphaMaps;
}
loadTextures() {
const textures = [];
this.layers.forEach((layer) => {
const filename = this.textureNames[layer.textureID];
const texture = TextureLoader.load(filename);
textures.push(texture);
});
this.textures = textures;
}
dispose() {
super.dispose();
this.textures.forEach((texture) => {
TextureLoader.unload(texture);
});
this.alphaMaps.forEach((alphaMap) => {
alphaMap.dispose();
});
}
}
export default Material;
================================================
FILE: src/lib/pipeline/adt/chunk/shader.frag
================================================
uniform int layerCount;
uniform sampler2D alphaMaps[4];
uniform sampler2D textures[4];
varying vec2 vUv;
varying vec2 vUvAlpha;
varying vec3 vertexNormal;
varying float cameraDistance;
uniform float lightModifier;
uniform vec3 ambientLight;
uniform vec3 diffuseLight;
uniform float fogModifier;
uniform float fogStart;
uniform float fogEnd;
uniform vec3 fogColor;
vec4 applyFog(vec4 color) {
float fogFactor = (fogEnd - cameraDistance) / (fogEnd - fogStart);
fogFactor = fogFactor * fogModifier;
fogFactor = clamp(fogFactor, 0.0, 1.0);
color.rgb = mix(fogColor.rgb, color.rgb, fogFactor);
// Ensure alpha channel is gone once a sufficient distance into the fog is reached.
if (cameraDistance > fogEnd * 1.5) {
color.a = 1.0;
}
return color;
}
vec4 finalizeColor(vec4 color) {
if (fogModifier > 0.0) {
color = applyFog(color);
}
return color;
}
// Given a light direction and normal, return a directed diffuse light.
vec3 getDirectedDiffuseLight(vec3 lightDirection, vec3 lightNormal, vec3 diffuseLight) {
float light = dot(lightNormal, -lightDirection);
if (light < 0.0) {
light = 0.0;
} else if (light > 0.5) {
light = 0.5 + ((light - 0.5) * 0.65);
}
vec3 directedDiffuseLight = diffuseLight.rgb * light;
return directedDiffuseLight;
}
// Given a layer, light it with diffuse and ambient light.
vec4 lightLayer(vec4 color, vec3 diffuse, vec3 ambient) {
if (lightModifier > 0.0) {
color.rgb *= diffuse + ambient;
color.rgb = saturate(color.rgb);
}
return color;
}
// Given a color, light it, and blend it with a layer.
vec4 lightAndBlendLayer(vec4 color, vec4 layer, vec4 blend, vec3 diffuse, vec3 ambient) {
layer = lightLayer(layer, diffuse, ambient);
color = (layer * blend) + ((1.0 - blend) * color);
return color;
}
void main() {
vec3 lightDirection = normalize(vec3(-1, -1, -1));
vec3 lightNormal = normalize(vertexNormal);
vec3 directedDiffuseLight = getDirectedDiffuseLight(lightDirection, lightNormal, diffuseLight);
vec4 layer;
vec4 blend;
// Base layer
vec4 color = texture2D(textures[0], vUv);
color = lightLayer(color, directedDiffuseLight, ambientLight);
// 2nd layer
if (layerCount > 1) {
layer = texture2D(textures[1], vUv);
blend = texture2D(alphaMaps[0], vUvAlpha);
color = lightAndBlendLayer(color, layer, blend, directedDiffuseLight, ambientLight);
}
// 3rd layer
if (layerCount > 2) {
layer = texture2D(textures[2], vUv);
blend = texture2D(alphaMaps[1], vUvAlpha);
color = lightAndBlendLayer(color, layer, blend, directedDiffuseLight, ambientLight);
}
// 4th layer
if (layerCount > 3) {
layer = texture2D(textures[3], vUv);
blend = texture2D(alphaMaps[2], vUvAlpha);
color = lightAndBlendLayer(color, layer, blend, directedDiffuseLight, ambientLight);
}
color = finalizeColor(color);
gl_FragColor = color;
}
================================================
FILE: src/lib/pipeline/adt/chunk/shader.vert
================================================
precision highp float;
attribute vec2 uvAlpha;
varying vec2 vUv;
varying vec2 vUvAlpha;
varying vec3 vertexNormal;
varying float cameraDistance;
void main() {
vUv = uv;
vUvAlpha = uvAlpha;
// TODO: Potentially necessary for specular lighting
vec3 vertexWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
cameraDistance = distance(cameraPosition, vertexWorldPosition);
vertexNormal = vec3(normal);
// TODO: Potentially unnecessary for ADT shading
// vertexWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
}
================================================
FILE: src/lib/pipeline/adt/index.js
================================================
import WorkerPool from '../worker/pool';
class ADT {
static SIZE = 533.33333;
static cache = {};
constructor(path, data) {
this.path = path;
this.data = data;
const tyx = this.path.match(/(\d+)_(\d+)\.adt$/);
this.tileX = +tyx[2];
this.tileY = +tyx[1];
this.x = this.constructor.positionFor(this.tileX);
this.y = this.constructor.positionFor(this.tileY);
}
get wmos() {
return this.data.MODF.entries;
}
get doodads() {
return this.data.MDDF.entries;
}
get textures() {
return this.data.MTEX.filenames;
}
static positionFor(tile) {
return (32 - tile) * this.SIZE;
}
static tileFor(position) {
return 32 - (position / this.SIZE) | 0;
}
static loadTile(map, tileX, tileY, wdtFlags) {
return ADT.load(`World\\Maps\\${map}\\${map}_${tileY}_${tileX}.adt`, wdtFlags);
}
static loadAtCoords(map, x, y, wdtFlags) {
const tileX = this.tileFor(x);
const tileY = this.tileFor(y);
return this.loadTile(map, tileX, tileY, wdtFlags);
}
static load(path, wdtFlags) {
if (!(path in this.cache)) {
this.cache[path] = WorkerPool.enqueue('ADT', path, wdtFlags).then((args) => {
const [data] = args;
return new this(path, data);
});
}
return this.cache[path];
}
}
export default ADT;
================================================
FILE: src/lib/pipeline/adt/loader.js
================================================
import ADT from 'blizzardry/lib/adt';
import { DecodeStream } from 'blizzardry/lib/restructure';
import Loader from '../../net/loader';
const loader = new Loader();
export default function(path, wdtFlags) {
return loader.load(path).then((raw) => {
const buffer = new Buffer(new Uint8Array(raw));
const stream = new DecodeStream(buffer);
const data = ADT(wdtFlags).decode(stream);
return data;
});
}
================================================
FILE: src/lib/pipeline/dbc/index.js
================================================
import WorkerPool from '../worker/pool';
class DBC {
static cache = {};
constructor(data) {
this.data = data;
this.records = data.records;
this.index();
}
index() {
this.records.forEach(function(record) {
if (record.id === undefined) {
return;
}
this[record.id] = record;
}.bind(this));
}
static load(name, id) {
if (!(name in this.cache)) {
this.cache[name] = WorkerPool.enqueue('DBC', name).then((args) => {
const [data] = args;
return new this(data);
});
}
if (id !== undefined) {
return this.cache[name].then(function(dbc) {
return dbc[id];
});
}
return this.cache[name];
}
}
export default DBC;
================================================
FILE: src/lib/pipeline/dbc/loader.js
================================================
import * as DBC from 'blizzardry/lib/dbc/entities';
import { DecodeStream } from 'blizzardry/lib/restructure';
import Loader from '../../net/loader';
const loader = new Loader();
export default function(name) {
const path = `DBFilesClient\\${name}.dbc`;
const entity = DBC[name];
return loader.load(path).then((raw) => {
const buffer = new Buffer(new Uint8Array(raw));
const stream = new DecodeStream(buffer);
const data = entity.dbc.decode(stream);
// TODO: This property breaks web worker communication for some reason!
delete data.entity;
return data;
});
}
================================================
FILE: src/lib/pipeline/m2/animation-manager.js
================================================
import EventEmitter from 'events';
import THREE from 'three';
class AnimationManager extends EventEmitter {
constructor(root, animationDefs, sequenceDefs) {
super();
// Complicated M2s may have far more than 10 (default listener cap) M2Materials subscribed to
// the same texture animations.
this.setMaxListeners(150);
this.animationDefs = animationDefs;
this.sequenceDefs = sequenceDefs;
this.animationClips = [];
this.sequenceClips = [];
this.loadedAnimations = {};
this.loadedSequences = {};
this.mixer = new THREE.AnimationMixer(root);
// M2 animations are keyframed in milliseconds.
this.mixer.timeScale = 1000.0;
this.registerAnimationClips(this.animationDefs);
this.registerSequenceClips(this.sequenceDefs);
this.length = this.animationClips.length + this.sequenceClips.length;
}
update(delta) {
this.mixer.update(delta);
this.emit('update');
}
loadAnimation(animationIndex) {
// The animation is already loaded.
if (typeof this.loadedAnimations[animationIndex] !== 'undefined') {
return this.loadedAnimations[animationIndex];
}
const clip = this.animationClips[animationIndex];
const action = this.mixer.clipAction(clip);
this.loadedAnimations[animationIndex] = action;
return action;
}
unloadAnimation(animationIndex) {
// The animation isn't loaded.
if (typeof this.loadedAnimations[animationIndex] === 'undefined') {
return;
}
const clip = this.animationClips[animationIndex];
this.mixer.uncacheClip(clip);
delete this.loadedAnimations[animationIndex];
return;
}
playAnimation(animationIndex) {
const action = this.loadAnimation(animationIndex);
action.play();
}
stopAnimation(animationIndex) {
// The animation isn't loaded.
if (typeof this.loadedAnimations[animationIndex] === 'undefined') {
return;
}
const action = this.loadAnimation(animationIndex);
action.stop();
}
loadSequence(sequenceIndex) {
// The sequence is already loaded.
if (typeof this.loadedSequences[sequenceIndex] !== 'undefined') {
return this.loadedSequences[sequenceIndex];
}
const clip = this.sequenceClips[sequenceIndex];
const action = this.mixer.clipAction(clip);
this.loadedSequences[sequenceIndex] = action;
return action;
}
unloadSequence(sequenceIndex) {
// The sequence isn't loaded.
if (typeof this.loadedSquences[sequenceIndex] === 'undefined') {
return;
}
const clip = this.sequenceClips[sequenceIndex];
this.mixer.uncacheClip(clip);
delete this.loadedSequences[sequenceIndex];
return;
}
playSequence(sequenceIndex) {
const action = this.loadSequence(sequenceIndex);
action.play();
}
playAllSequences() {
this.sequenceDefs.forEach((_sequenceDuration, index) => {
this.playSequence(index);
});
}
stopSequence(sequenceIndex) {
// The sequence isn't loaded.
if (typeof this.loadedSequences[sequenceIndex] === 'undefined') {
return;
}
const action = this.loadSequence(sequenceIndex);
action.stop();
}
registerAnimationClips(animationDefs) {
animationDefs.forEach((animationDef, index) => {
const clip = new THREE.AnimationClip('animation-' + index, animationDef.length, []);
this.animationClips[index] = clip;
});
}
registerSequenceClips(sequenceDefs) {
sequenceDefs.forEach((sequenceDuration, index) => {
const clip = new THREE.AnimationClip('sequence-' + index, sequenceDuration, []);
this.sequenceClips[index] = clip;
});
}
unregisterTrack(trackID) {
this.animationClips.forEach((clip) => {
clip.tracks = clip.tracks.filter((track) => {
return track.name !== trackID;
});
clip.trim();
clip.optimize();
});
this.sequenceClips.forEach((clip) => {
clip.tracks = clip.tracks.filter((track) => {
return track.name !== trackID;
});
clip.trim();
clip.optimize();
});
}
registerTrack(opts) {
let trackID;
if (opts.animationBlock.globalSequenceID > -1) {
trackID = this.registerSequenceTrack(opts);
} else {
trackID = this.registerAnimationTrack(opts);
}
return trackID;
}
registerAnimationTrack(opts) {
const trackName = opts.target.uuid + '.' + opts.property;
const animationBlock = opts.animationBlock;
const { valueTransform } = opts;
animationBlock.tracks.forEach((trackDef, animationIndex) => {
const animationDef = this.animationDefs[animationIndex];
// Avoid creating tracks for external .anim animations.
if ((animationDef.flags & 0x130) === 0) {
return;
}
// Avoid creating empty tracks.
if (trackDef.timestamps.length === 0) {
return;
}
const timestamps = trackDef.timestamps;
const values = [];
// Transform values before passing in to track.
trackDef.values.forEach((rawValue) => {
if (valueTransform) {
values.push.apply(values, valueTransform(rawValue));
} else {
values.push.apply(values, rawValue);
}
});
const clip = this.animationClips[animationIndex];
const track = new THREE[opts.trackType](trackName, timestamps, values);
clip.tracks.push(track);
clip.optimize();
});
return trackName;
}
registerSequenceTrack(opts) {
const trackName = opts.target.uuid + '.' + opts.property;
const animationBlock = opts.animationBlock;
const { valueTransform } = opts;
animationBlock.tracks.forEach((trackDef) => {
// Avoid creating empty tracks.
if (trackDef.timestamps.length === 0) {
return;
}
const timestamps = trackDef.timestamps;
const values = [];
// Transform values before passing in to track.
trackDef.values.forEach((rawValue) => {
if (valueTransform) {
values.push.apply(values, valueTransform(rawValue));
} else {
values.push.apply(values, rawValue);
}
});
const track = new THREE[opts.trackType](trackName, timestamps, values);
const clip = this.sequenceClips[animationBlock.globalSequenceID];
clip.tracks.push(track);
clip.optimize();
});
return trackName;
}
}
export default AnimationManager;
================================================
FILE: src/lib/pipeline/m2/batch-manager.js
================================================
class BatchManager {
constructor() {
}
createDefs(data, skinData) {
const defs = [];
skinData.batches.forEach((batchData) => {
const def = this.createDef(data, batchData);
defs.push(def);
});
return defs;
}
createDef(data, batchData) {
const def = this.stubDef();
const { textures } = data;
const { vertexColorAnimations, transparencyAnimations, uvAnimations } = data;
if (!batchData.textureIndices) {
this.resolveTextureIndices(data, batchData);
}
if (!batchData.uvAnimationIndices) {
this.resolveUVAnimationIndices(data, batchData);
}
const { opCount } = batchData;
const { textureMappingIndex, materialIndex } = batchData;
const { vertexColorAnimationIndex, transparencyAnimationLookup } = batchData;
const { textureIndices, uvAnimationIndices } = batchData;
// Batch flags
def.flags = batchData.flags;
// Submesh index and batch layer
def.submeshIndex = batchData.submeshIndex;
def.layer = batchData.layer;
// Op count and shader ID
def.opCount = batchData.opCount;
def.shaderID = batchData.shaderID;
// Texture mapping
// -1 => Env; 0 => T1; 1 => T2
if (textureMappingIndex >= 0) {
const textureMapping = data.textureMappings[textureMappingIndex];
def.textureMapping = textureMapping;
}
// Material (render flags and blending mode)
const material = data.materials[materialIndex];
def.renderFlags = material.renderFlags;
def.blendingMode = material.blendingMode;
// Vertex color animation block
if (vertexColorAnimationIndex > -1 && vertexColorAnimations[vertexColorAnimationIndex]) {
const vertexColorAnimation = vertexColorAnimations[vertexColorAnimationIndex];
def.vertexColorAnimation = vertexColorAnimation;
def.vertexColorAnimationIndex = vertexColorAnimationIndex;
}
// Transparency animation block
// TODO: Do we load multiple values based on opCount?
const transparencyAnimationIndex = data.transparencyAnimationLookups[transparencyAnimationLookup];
if (transparencyAnimationIndex > -1 && transparencyAnimations[transparencyAnimationIndex]) {
const transparencyAnimation = transparencyAnimations[transparencyAnimationIndex];
def.transparencyAnimation = transparencyAnimation;
def.transparencyAnimationIndex = transparencyAnimationIndex;
}
for (let opIndex = 0; opIndex < def.opCount; ++opIndex) {
// Texture
const textureIndex = textureIndices[opIndex];
const texture = textures[textureIndex];
if (texture) {
def.textures[opIndex] = texture;
def.textureIndices[opIndex] = textureIndex;
}
// UV animation block
const uvAnimationIndex = uvAnimationIndices[opIndex];
const uvAnimation = uvAnimations[uvAnimationIndex];
if (uvAnimation) {
def.uvAnimations[opIndex] = uvAnimation;
def.uvAnimationIndices[opIndex] = uvAnimationIndex;
}
}
return def;
}
resolveTextureIndices(data, batchData) {
batchData.textureIndices = [];
for (let opIndex = 0; opIndex < batchData.opCount; opIndex++) {
const textureIndex = data.textureLookups[batchData.textureLookup + opIndex];
batchData.textureIndices.push(textureIndex);
}
}
resolveUVAnimationIndices(data, batchData) {
batchData.uvAnimationIndices = [];
for (let opIndex = 0; opIndex < batchData.opCount; opIndex++) {
const uvAnimationIndex = data.uvAnimationLookups[batchData.uvAnimationLookup + opIndex];
batchData.uvAnimationIndices.push(uvAnimationIndex);
}
}
stubDef() {
const def = {
flags: null,
shaderID: null,
opCount: null,
textureMapping: null,
renderFlags: null,
blendingMode: null,
textures: [],
textureIndices: [],
uvAnimations: [],
uvAnimationIndices: [],
transparencyAnimation: null,
transparencyAnimationIndex: null,
vertexColorAnimation: null,
vertexColorAnimationIndex: null
};
return def;
}
}
export default BatchManager;
================================================
FILE: src/lib/pipeline/m2/blueprint.js
================================================
import WorkerPool from '../worker/pool';
import M2 from './';
class M2Blueprint {
static cache = new Map();
static animationUpdateTargets = new Map();
static references = new Map();
static pendingUnload = new Set();
static unloaderRunning = false;
static UNLOAD_INTERVAL = 15000;
static load(rawPath) {
const path = rawPath.replace(/\.md(x|l)/i, '.m2').toUpperCase();
// Prevent unintended unloading.
if (this.pendingUnload.has(path)) {
this.pendingUnload.delete(path);
}
// Background unloader might need to be started.
if (!this.unloaderRunning) {
this.unloaderRunning = true;
this.backgroundUnload();
}
// Keep track of references.
let refCount = this.references.get(path) || 0;
++refCount;
this.references.set(path, refCount);
if (!this.cache.has(path)) {
this.cache.set(path, WorkerPool.enqueue('M2', path).then((args) => {
const [data, skinData] = args;
const m2 = new M2(path, data, skinData);
if (m2.receivesAnimationUpdates) {
this.animationUpdateTargets.set(path, m2);
}
return m2;
}));
}
return this.cache.get(path).then((m2) => {
return m2.clone();
});
}
static unload(m2) {
const path = m2.path.replace(/\.md(x|l)/i, '.m2').toUpperCase();
// Immediately dispose any non-instanced M2s.
if (!m2.canInstance) {
m2.dispose();
}
let refCount = this.references.get(path) || 1;
--refCount;
if (refCount === 0) {
this.pendingUnload.add(path);
} else {
this.references.set(path, refCount);
}
}
static backgroundUnload() {
this.pendingUnload.forEach((path) => {
// Handle disposal for instanced M2s.
if (this.cache.has(path)) {
this.cache.get(path).then((m2) => {
m2.dispose();
});
}
this.cache.delete(path);
this.animationUpdateTargets.delete(path);
this.references.delete(path);
this.pendingUnload.delete(path);
});
setTimeout(this.backgroundUnload.bind(this), this.UNLOAD_INTERVAL);
}
static animate(delta) {
this.animationUpdateTargets.forEach((m2) => {
// Handle delta updates for instanced M2s (which share animation managers).
if (m2.animations.length > 0) {
m2.animations.update(delta);
}
});
}
}
export default M2Blueprint;
================================================
FILE: src/lib/pipeline/m2/index.js
================================================
import THREE from 'three';
import Submesh from './submesh';
import M2Material from './material';
import AnimationManager from './animation-manager';
import BatchManager from './batch-manager';
class M2 extends THREE.Group {
static cache = {};
constructor(path, data, skinData, instance = null) {
super();
this.matrixAutoUpdate = false;
this.eventListeners = [];
this.name = path.split('\\').slice(-1).pop();
this.path = path;
this.data = data;
this.skinData = skinData;
this.batchManager = new BatchManager();
// Instanceable M2s share geometry, texture units, and animations.
this.canInstance = data.canInstance;
this.animated = data.animated;
this.billboards = [];
// Keep track of whether or not to use skinning. If the M2 has bone animations, useSkinning is
// set to true, and all meshes and materials used in the M2 will be skinning enabled. Otherwise,
// skinning will not be enabled. Skinning has a very significant impact on the render loop in
// three.js.
this.useSkinning = false;
this.mesh = null;
this.submeshes = [];
this.parts = new Map();
this.geometry = null;
this.submeshGeometries = new Map();
this.skeleton = null;
this.bones = [];
this.rootBones = [];
if (instance) {
this.animations = instance.animations;
// To prevent over-updating animation timelines, instanced M2s shouldn't receive animation
// time deltas. Instead, only the original M2 should receive time deltas.
this.receivesAnimationUpdates = false;
} else {
this.animations = new AnimationManager(this, data.animations, data.sequences);
if (this.animated) {
this.receivesAnimationUpdates = true;
} else {
this.receivesAnimationUpdates = false;
}
}
this.createSkeleton(data.bones);
// Instanced M2s can share geometries and texture units.
if (instance) {
this.batches = instance.batches;
this.geometry = instance.geometry;
this.submeshGeometries = instance.submeshGeometries;
} else {
this.createTextureAnimations(data);
this.createBatches(data, skinData);
this.createGeometry(data.vertices);
}
this.createMesh(this.geometry, this.skeleton, this.rootBones);
this.createSubmeshes(data, skinData);
}
createSkeleton(boneDefs) {
const rootBones = [];
const bones = [];
const billboards = [];
for (let boneIndex = 0, len = boneDefs.length; boneIndex < len; ++boneIndex) {
const boneDef = boneDefs[boneIndex];
const bone = new THREE.Bone();
bones.push(bone);
// M2 bone positioning seems to be inverted on X and Y
const { pivotPoint } = boneDef;
const correctedPosition = new THREE.Vector3(-pivotPoint[0], -pivotPoint[1], pivotPoint[2]);
bone.position.copy(correctedPosition);
if (boneDef.parentID > -1) {
const parent = bones[boneDef.parentID];
parent.add(bone);
// Correct bone positioning relative to parent
let up = bone;
while (up = up.parent) {
bone.position.sub(up.position);
}
} else {
bone.userData.isRoot = true;
rootBones.push(bone);
}
// Enable skinning support on this M2 if we have bone animations.
if (boneDef.animated) {
this.useSkinning = true;
}
// Flag billboarded bones
if (boneDef.billboarded) {
bone.userData.billboarded = true;
bone.userData.billboardType = boneDef.billboardType;
billboards.push(bone);
}
// Bone translation animation block
if (boneDef.translation.animated) {
this.animations.registerTrack({
target: bone,
property: 'position',
animationBlock: boneDef.translation,
trackType: 'VectorKeyframeTrack',
valueTransform: function(value) {
return [
bone.position.x + -value[0],
bone.position.y + -value[1],
bone.position.z + value[2]
];
}
});
}
// Bone rotation animation block
if (boneDef.rotation.animated) {
this.animations.registerTrack({
target: bone,
property: 'quaternion',
animationBlock: boneDef.rotation,
trackType: 'QuaternionKeyframeTrack',
valueTransform: function(value) {
return [value[0], value[1], -value[2], -value[3]];
}
});
}
// Bone scaling animation block
if (boneDef.scaling.animated) {
this.animations.registerTrack({
target: bone,
property: 'scale',
animationBlock: boneDef.scaling,
trackType: 'VectorKeyframeTrack'
});
}
}
// Preserve the bones
this.bones = bones;
this.rootBones = rootBones;
this.billboards = billboards;
// Assemble the skeleton
this.skeleton = new THREE.Skeleton(bones);
this.skeleton.matrixAutoUpdate = this.matrixAutoUpdate;
}
// Returns a map of M2Materials indexed by submesh. Each material represents a batch,
// to be rendered in the order of appearance in the map's entry for the submesh index.
createBatches(data, skinData) {
const batches = new Map();
const batchDefs = this.batchManager.createDefs(data, skinData);
const batchLen = batchDefs.length;
for (let batchIndex = 0; batchIndex < batchLen; ++batchIndex) {
const batchDef = batchDefs[batchIndex];
const { submeshIndex } = batchDef;
if (!batches.has(submeshIndex)) {
batches.set(submeshIndex, []);
}
// Array that will contain materials matching each batch.
const submeshBatches = batches.get(submeshIndex);
// Observe the M2's skinning flag in the M2Material.
batchDef.useSkinning = this.useSkinning;
const batchMaterial = new M2Material(this, batchDef);
submeshBatches.unshift(batchMaterial);
}
this.batches = batches;
}
createGeometry(vertices) {
const geometry = new THREE.Geometry();
for (let vertexIndex = 0, len = vertices.length; vertexIndex < len; ++vertexIndex) {
const vertex = vertices[vertexIndex];
const { position } = vertex;
geometry.vertices.push(
// Provided as (X, Z, -Y)
new THREE.Vector3(position[0], position[2], -position[1])
);
geometry.skinIndices.push(
new THREE.Vector4(...vertex.boneIndices)
);
geometry.skinWeights.push(
new THREE.Vector4(...vertex.boneWeights)
);
}
// Mirror geometry over X and Y axes and rotate
const matrix = new THREE.Matrix4();
matrix.makeScale(-1, -1, 1);
geometry.applyMatrix(matrix);
geometry.rotateX(-Math.PI / 2);
// Preserve the geometry
this.geometry = geometry;
}
createMesh(geometry, skeleton, rootBones) {
let mesh;
if (this.useSkinning) {
mesh = new THREE.SkinnedMesh(geometry);
// Assign root bones to mesh
rootBones.forEach((bone) => {
mesh.add(bone);
bone.skin = mesh;
});
// Bind mesh to skeleton
mesh.bind(skeleton);
} else {
mesh = new THREE.Mesh(geometry);
}
mesh.matrixAutoUpdate = this.matrixAutoUpdate;
// Never display the mesh
// TODO: We shouldn't really even have this mesh in the first place, should we?
mesh.visible = false;
// Add mesh to the group
this.add(mesh);
// Assign as root mesh
this.mesh = mesh;
}
createSubmeshes(data, skinData) {
const { vertices } = data;
const { submeshes, indices, triangles } = skinData;
const subLen = submeshes.length;
for (let submeshIndex = 0; submeshIndex < subLen; ++submeshIndex) {
const submeshDef = submeshes[submeshIndex];
// Bring up relevant batches and geometry.
const submeshBatches = this.batches.get(submeshIndex);
const submeshGeometry = this.submeshGeometries.get(submeshIndex) ||
this.createSubmeshGeometry(submeshDef, indices, triangles, vertices);
const submesh = this.createSubmesh(submeshDef, submeshGeometry, submeshBatches);
this.parts.set(submesh.userData.partID, submesh);
this.submeshes.push(submesh);
this.submeshGeometries.set(submeshIndex, submeshGeometry);
this.add(submesh);
}
}
createSubmeshGeometry(submeshDef, indices, triangles, vertices) {
const geometry = this.geometry.clone();
// TODO: Figure out why this isn't cloned by the line above
geometry.skinIndices = Array.from(this.geometry.skinIndices);
geometry.skinWeights = Array.from(this.geometry.skinWeights);
const uvs = [];
const { startTriangle: start, triangleCount: count } = submeshDef;
for (let i = start, faceIndex = 0; i < start + count; i += 3, ++faceIndex) {
const vindices = [
indices[triangles[i]],
indices[triangles[i + 1]],
indices[triangles[i + 2]]
];
const face = new THREE.Face3(vindices[0], vindices[1], vindices[2]);
geometry.faces.push(face);
uvs[faceIndex] = [];
for (let vinIndex = 0, vinLen = vindices.length; vinIndex < vinLen; ++vinIndex) {
const index = vindices[vinIndex];
const { textureCoords, normal } = vertices[index];
uvs[faceIndex].push(new THREE.Vector2(textureCoords[0][0], textureCoords[0][1]));
face.vertexNormals.push(new THREE.Vector3(normal[0], normal[1], normal[2]));
}
}
geometry.faceVertexUvs = [uvs];
const bufferGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
return bufferGeometry;
}
createSubmesh(submeshDef, geometry, batches) {
const rootBone = this.bones[submeshDef.rootBone];
const opts = {
skeleton: this.skeleton,
geometry: geometry,
rootBone: rootBone,
useSkinning: this.useSkinning,
matrixAutoUpdate: this.matrixAutoUpdate
};
const submesh = new Submesh(opts);
submesh.applyBatches(batches);
submesh.userData.partID = submeshDef.partID;
return submesh;
}
createTextureAnimations(data) {
this.textureAnimations = new THREE.Object3D();
this.uvAnimationValues = [];
this.transparencyAnimationValues = [];
this.vertexColorAnimationValues = [];
const { uvAnimations, transparencyAnimations, vertexColorAnimations } = data;
this.createUVAnimations(uvAnimations);
this.createTransparencyAnimations(transparencyAnimations);
this.createVertexColorAnimations(vertexColorAnimations);
}
// TODO: Add support for rotation and scaling in UV animations.
createUVAnimations(uvAnimationDefs) {
if (uvAnimationDefs.length === 0) {
return;
}
uvAnimationDefs.forEach((uvAnimationDef, index) => {
// Default value
this.uvAnimationValues[index] = {
translation: [1.0, 1.0, 1.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scaling: [1.0, 1.0, 1.0],
matrix: new THREE.Matrix4()
};
const { translation } = uvAnimationDef;
this.animations.registerTrack({
target: this,
property: 'uvAnimationValues[' + index + '].translation',
animationBlock: translation,
trackType: 'VectorKeyframeTrack'
});
// Set up event subscription to produce matrix from translation, rotation, and scaling
// values.
const updater = () => {
const animationValue = this.uvAnimationValues[index];
// Set up matrix for use in uv transform in vertex shader.
animationValue.matrix = new THREE.Matrix4().compose(
new THREE.Vector3(...animationValue.translation),
new THREE.Quaternion(...animationValue.rotation),
new THREE.Vector3(...animationValue.scaling)
);
};
this.animations.on('update', updater);
this.eventListeners.push([this.animations, 'update', updater]);
});
}
createTransparencyAnimations(transparencyAnimationDefs) {
if (transparencyAnimationDefs.length === 0) {
return;
}
transparencyAnimationDefs.forEach((transparencyAnimationDef, index) => {
// Default value
this.transparencyAnimationValues[index] = 1.0;
this.animations.registerTrack({
target: this,
property: 'transparencyAnimationValues[' + index + ']',
animationBlock: transparencyAnimationDef,
trackType: 'NumberKeyframeTrack',
valueTransform: function(value) {
return [value];
}
});
});
}
createVertexColorAnimations(vertexColorAnimationDefs) {
if (vertexColorAnimationDefs.length === 0) {
return;
}
vertexColorAnimationDefs.forEach((vertexColorAnimationDef, index) => {
// Default value
this.vertexColorAnimationValues[index] = {
color: [1.0, 1.0, 1.0],
alpha: 1.0
};
const { color, alpha } = vertexColorAnimationDef;
this.animations.registerTrack({
target: this,
property: 'vertexColorAnimationValues[' + index + '].color',
animationBlock: color,
trackType: 'VectorKeyframeTrack'
});
this.animations.registerTrack({
target: this,
property: 'vertexColorAnimationValues[' + index + '].alpha',
animationBlock: alpha,
trackType: 'NumberKeyframeTrack',
valueTransform: function(value) {
return [value];
}
});
});
}
applyBillboards(camera) {
for (let i = 0, len = this.billboards.length; i < len; ++i) {
const bone = this.billboards[i];
switch (bone.userData.billboardType) {
case 0:
this.applySphericalBillboard(camera, bone);
break;
case 3:
this.applyCylindricalZBillboard(camera, bone);
break;
default:
break;
}
}
}
applySphericalBillboard(camera, bone) {
const boneRoot = bone.skin;
if (!boneRoot) {
return;
}
const camPos = this.worldToLocal(camera.position.clone());
const modelForward = new THREE.Vector3(camPos.x, camPos.y, camPos.z);
modelForward.normalize();
const modelVmEl = boneRoot.modelViewMatrix.elements;
const modelRight = new THREE.Vector3(modelVmEl[0], modelVmEl[4], modelVmEl[8]);
modelRight.multiplyScalar(-1);
const modelUp = new THREE.Vector3();
modelUp.crossVectors(modelForward, modelRight);
modelUp.normalize();
const rotateMatrix = new THREE.Matrix4();
rotateMatrix.set(
modelForward.x, modelRight.x, modelUp.x, 0,
modelForward.y, modelRight.y, modelUp.y, 0,
modelForward.z, modelRight.z, modelUp.z, 0,
0, 0, 0, 1
);
bone.rotation.setFromRotationMatrix(rotateMatrix);
}
applyCylindricalZBillboard(camera, bone) {
const boneRoot = bone.skin;
if (!boneRoot) {
return;
}
const camPos = this.worldToLocal(camera.position.clone());
const modelForward = new THREE.Vector3(camPos.x, camPos.y, camPos.z);
modelForward.normalize();
const modelVmEl = boneRoot.modelViewMatrix.elements;
const modelRight = new THREE.Vector3(modelVmEl[0], modelVmEl[4], modelVmEl[8]);
const modelUp = new THREE.Vector3(0, 0, 1);
const rotateMatrix = new THREE.Matrix4();
rotateMatrix.set(
modelForward.x, modelRight.x, modelUp.x, 0,
modelForward.y, modelRight.y, modelUp.y, 0,
modelForward.z, modelRight.z, modelUp.z, 0,
0, 0, 0, 1
);
bone.rotation.setFromRotationMatrix(rotateMatrix);
}
set displayInfo(displayInfo) {
for (let i = 0, len = this.submeshes.length; i < len; ++i) {
this.submeshes[i].displayInfo = displayInfo;
}
}
detachEventListeners() {
this.eventListeners.forEach((entry) => {
const [target, event, listener] = entry;
target.removeListener(event, listener);
});
}
dispose() {
this.detachEventListeners();
this.eventListeners = [];
this.geometry.dispose();
this.mesh.geometry.dispose();
this.submeshes.forEach((submesh) => {
submesh.dispose();
});
}
clone() {
let instance = {};
if (this.canInstance) {
instance.animations = this.animations;
instance.geometry = this.geometry;
instance.submeshGeometries = this.submeshGeometries;
instance.batches = this.batches;
} else {
instance = null;
}
return new this.constructor(this.path, this.data, this.skinData, instance);
}
}
export default M2;
================================================
FILE: src/lib/pipeline/m2/loader.js
================================================
import { DecodeStream } from 'blizzardry/lib/restructure';
import M2 from 'blizzardry/lib/m2';
import Skin from 'blizzardry/lib/m2/skin';
import Loader from '../../net/loader';
const loader = new Loader();
export default function(path) {
return loader.load(path).then((raw) => {
let buffer = new Buffer(new Uint8Array(raw));
let stream = new DecodeStream(buffer);
const data = M2.decode(stream);
// TODO: Allow configuring quality
const quality = data.viewCount - 1;
const skinPath = path.replace(/\.m2/i, `0${quality}.skin`);
return loader.load(skinPath).then((rawSkin) => {
buffer = new Buffer(new Uint8Array(rawSkin));
stream = new DecodeStream(buffer);
const skinData = Skin.decode(stream);
return [data, skinData];
});
});
}
================================================
FILE: src/lib/pipeline/m2/material/index.js
================================================
import THREE from 'three';
import TextureLoader from '../../texture-loader';
import vertexShader from './shader.vert';
import fragmentShader from './shader.frag';
class M2Material extends THREE.ShaderMaterial {
constructor(m2, def) {
if (def.useSkinning) {
super({ skinning: true });
} else {
super({ skinning: false });
}
this.m2 = m2;
this.eventListeners = [];
const vertexShaderMode = this.vertexShaderModeFromID(def.shaderID, def.opCount);
const fragmentShaderMode = this.fragmentShaderModeFromID(def.shaderID, def.opCount);
this.uniforms = {
textureCount: { type: 'i', value: 0 },
textures: { type: 'tv', value: [] },
blendingMode: { type: 'i', value: 0 },
vertexShaderMode: { type: 'i', value: vertexShaderMode },
fragmentShaderMode: { type: 'i', value: fragmentShaderMode },
billboarded: { type: 'f', value: 0.0 },
// Animated vertex colors
animatedVertexColorRGB: { type: 'v3', value: new THREE.Vector3(1.0, 1.0, 1.0) },
animatedVertexColorAlpha: { type: 'f', value: 1.0 },
// Animated transparency
animatedTransparency: { type: 'f', value: 1.0 },
// Animated texture coordinate transform matrices
animatedUVs: {
type: 'm4v',
value: [
new THREE.Matrix4(),
new THREE.Matrix4(),
new THREE.Matrix4(),
new THREE.Matrix4()
]
},
// Managed by light manager
lightModifier: { type: 'f', value: '1.0' },
ambientLight: { type: 'c', value: new THREE.Color(0.5, 0.5, 0.5) },
diffuseLight: { type: 'c', value: new THREE.Color(0.25, 0.5, 1.0) },
// Managed by light manager
fogModifier: { type: 'f', value: '1.0' },
fogColor: { type: 'c', value: new THREE.Color(0.25, 0.5, 1.0) },
fogStart: { type: 'f', value: 5.0 },
fogEnd: { type: 'f', value: 400.0 }
};
this.vertexShader = vertexShader;
this.fragmentShader = fragmentShader;
this.applyRenderFlags(def.renderFlags);
this.applyBlendingMode(def.blendingMode);
// Shader ID is a masked int that determines mode for vertex and fragment shader.
this.shaderID = def.shaderID;
// Loaded by calling updateSkinTextures()
this.skins = {};
this.skins.skin1 = null;
this.skins.skin2 = null;
this.skins.skin3 = null;
this.textures = [];
this.textureDefs = def.textures;
this.loadTextures();
this.registerAnimations(def);
}
// TODO: Fully expand these lookups.
vertexShaderModeFromID(shaderID, opCount) {
if (opCount === 1) {
return 0;
}
if (shaderID === 0) {
return 1;
}
return -1;
}
// TODO: Fully expand these lookups.
fragmentShaderModeFromID(shaderID, opCount) {
if (opCount === 1) {
// fragCombinersWrath1Pass
return 0;
}
if (shaderID === 0) {
// fragCombinersWrath2Pass
return 1;
}
// Unknown / unhandled
return -1;
}
enableBillboarding() {
// TODO: Make billboarding happen in the vertex shader.
this.uniforms.billboarded = { type: 'f', value: '1.0' };
// TODO: Shouldn't this be FrontSide? Billboarding logic currently seems to flips the mesh
// backward.
this.side = THREE.BackSide;
}
applyRenderFlags(renderFlags) {
// Flag 0x01 (unlit)
if (renderFlags & 0x01) {
this.uniforms.lightModifier = { type: 'f', value: '0.0' };
}
// Flag 0x02 (unfogged)
if (renderFlags & 0x02) {
this.uniforms.fogModifier = { type: 'f', value: '0.0' };
}
// Flag 0x04 (no backface culling)
if (renderFlags & 0x04) {
this.side = THREE.DoubleSide;
this.transparent = true;
}
// Flag 0x10 (no z-buffer write)
if (renderFlags & 0x10) {
this.depthWrite = false;
}
}
applyBlendingMode(blendingMode) {
this.uniforms.blendingMode.value = blendingMode;
if (blendingMode === 1) {
this.uniforms.alphaKey = { type: 'f', value: 1.0 };
} else {
this.uniforms.alphaKey = { type: 'f', value: 0.0 };
}
if (blendingMode >= 1) {
this.transparent = true;
this.blending = THREE.CustomBlending;
}
switch (blendingMode) {
case 0:
this.blending = THREE.NoBlending;
this.blendSrc = THREE.OneFactor;
this.blendDst = THREE.ZeroFactor;
break;
case 1:
this.alphaTest = 0.5;
this.side = THREE.DoubleSide;
this.blendSrc = THREE.OneFactor;
this.blendDst = THREE.ZeroFactor;
this.blendSrcAlpha = THREE.OneFactor;
this.blendDstAlpha = THREE.ZeroFactor;
break;
case 2:
this.blendSrc = THREE.SrcAlphaFactor;
this.blendDst = THREE.OneMinusSrcAlphaFactor;
this.blendSrcAlpha = THREE.SrcAlphaFactor;
this.blendDstAlpha = THREE.OneMinusSrcAlphaFactor;
break;
case 3:
this.blendSrc = THREE.SrcColorFactor;
this.blendDst = THREE.DstColorFactor;
this.blendSrcAlpha = THREE.SrcAlphaFactor;
this.blendDstAlpha = THREE.DstAlphaFactor;
break;
case 4:
this.blendSrc = THREE.SrcAlphaFactor;
this.blendDst = THREE.OneFactor;
this.blendSrcAlpha = THREE.SrcAlphaFactor;
this.blendDstAlpha = THREE.OneFactor;
break;
case 5:
this.blendSrc = THREE.DstColorFactor;
this.blendDst = THREE.ZeroFactor;
this.blendSrcAlpha = THREE.DstAlphaFactor;
this.blendDstAlpha = THREE.ZeroFactor;
break;
case 6:
this.blendSrc = THREE.DstColorFactor;
this.blendDst = THREE.SrcColorFactor;
this.blendSrcAlpha = THREE.DstAlphaFactor;
this.blendDstAlpha = THREE.SrcAlphaFactor;
break;
default:
break;
}
}
loadTextures() {
const textureDefs = this.textureDefs;
const textures = [];
textureDefs.forEach((textureDef) => {
textures.push(this.loadTexture(textureDef));
});
this.textures = textures;
// Update shader uniforms to reflect loaded textures.
this.uniforms.textures = { type: 'tv', value: textures };
this.uniforms.textureCount = { type: 'i', value: textures.length };
}
loadTexture(textureDef) {
const wrapS = THREE.RepeatWrapping;
const wrapT = THREE.RepeatWrapping;
const flipY = false;
let path = null;
switch (textureDef.type) {
case 0:
// Hardcoded texture
path = textureDef.filename;
break;
case 11:
if (this.skins.skin1) {
path = this.skins.skin1;
}
break;
case 12:
if (this.skins.skin2) {
path = this.skins.skin2;
}
break;
case 13:
if (this.skins.skin3) {
path = this.skins.skin3;
}
break;
default:
break;
}
if (path) {
return TextureLoader.load(path, wrapS, wrapT, flipY);
} else {
return null;
}
}
registerAnimations(def) {
const { uvAnimationIndices, transparencyAnimationIndex, vertexColorAnimationIndex } = def;
this.registerUVAnimations(uvAnimationIndices);
this.registerTransparencyAnimation(transparencyAnimationIndex);
this.registerVertexColorAnimation(vertexColorAnimationIndex);
}
registerUVAnimations(uvAnimationIndices) {
if (uvAnimationIndices.length === 0) {
return;
}
const { animations, uvAnimationValues } = this.m2;
const updater = () => {
uvAnimationIndices.forEach((uvAnimationIndex, opIndex) => {
const target = this.uniforms.animatedUVs;
const source = uvAnimationValues[uvAnimationIndex];
target.value[opIndex] = source.matrix;
});
};
animations.on('update', updater);
this.eventListeners.push([animations, 'update', updater]);
}
registerTransparencyAnimation(transparencyAnimationIndex) {
if (transparencyAnimationIndex === null || transparencyAnimationIndex === -1) {
return;
}
const { animations, transparencyAnimationValues } = this.m2;
const target = this.uniforms.animatedTransparency;
const source = transparencyAnimationValues;
const valueIndex = transparencyAnimationIndex;
const updater = () => {
target.value = source[valueIndex];
};
animations.on('update', updater);
this.eventListeners.push([animations, 'update', updater]);
}
registerVertexColorAnimation(vertexColorAnimationIndex) {
if (vertexColorAnimationIndex === null || vertexColorAnimationIndex === -1) {
return;
}
const { animations, vertexColorAnimationValues } = this.m2;
const targetRGB = this.uniforms.animatedVertexColorRGB;
const targetAlpha = this.uniforms.animatedVertexColorAlpha;
const source = vertexColorAnimationValues;
const valueIndex = vertexColorAnimationIndex;
const updater = () => {
targetRGB.value = source[valueIndex].color;
targetAlpha.value = source[valueIndex].alpha;
};
animations.on('update', updater);
this.eventListeners.push([animations, 'update', updater]);
}
detachEventListeners() {
this.eventListeners.forEach((entry) => {
const [target, event, listener] = entry;
target.removeListener(event, listener);
});
}
updateSkinTextures(skin1, skin2, skin3) {
this.skins.skin1 = skin1;
this.skins.skin2 = skin2;
this.skins.skin3 = skin3;
this.loadTextures();
}
dispose() {
super.dispose();
this.detachEventListeners();
this.eventListeners = [];
this.textures.forEach((texture) => {
TextureLoader.unload(texture);
});
}
}
export default M2Material;
================================================
FILE: src/lib/pipeline/m2/material/shader.frag
================================================
uniform int fragmentShaderMode;
uniform int textureCount;
uniform sampler2D textures[4];
varying vec2 uv1;
varying vec2 uv2;
varying float cameraDistance;
varying vec3 vertexWorldNormal;
varying vec4 animatedVertexColor;
uniform float animatedTransparency;
uniform float alphaKey;
uniform float lightModifier;
uniform vec3 ambientLight;
uniform vec3 diffuseLight;
uniform float fogModifier;
uniform float fogStart;
uniform float fogEnd;
uniform vec3 fogColor;
uniform int blendingMode;
vec4 fragCombinersWrath1Pass(sampler2D texture1, vec2 uv1) {
vec4 texture1Color = texture2D(texture1, uv1);
if (alphaKey == 1.0 && texture1Color.a <= 0.5) {
discard;
}
vec4 c1 = texture1Color;
// Apply animated transparency (defaults to 1.0)
c1.a *= animatedTransparency;
// Blend with vertex color
c1.rgb *= (animatedVertexColor.rgb * animatedVertexColor.a);
// Restore full color intensity after blending with vertexColor
c1.rgb *= 2.0;
// Force transparent pixels to fully opaque if in opaque blending mode (0). Needed to prevent
// transparent pixels from becoming inappropriately bright.
if (blendingMode == 0) {
c1.a = 1.0;
}
vec4 outputColor = c1;
return outputColor;
}
vec4 fragCombinersWrath2Pass(sampler2D texture1, vec2 uv1, sampler2D texture2, vec2 uv2) {
vec4 texture1Color = texture2D(texture1, uv1);
vec4 texture2Color = texture2D(texture2, uv2);
if (alphaKey == 1.0 && texture1Color.a <= 0.5) {
discard;
}
vec4 c1 = texture1Color;
vec4 c2 = texture2Color;
// Apply animated transparency (defaults to 1.0)
c1.a *= animatedTransparency;
// Blend texture alphas
c1.a *= c2.a;
// Blend with vertex color
c1.rgb *= (animatedVertexColor.rgb * animatedVertexColor.a);
// Restore full color intensity after blending with vertexColor
c1.rgb *= 2.0;
vec4 outputColor = c1;
return outputColor;
}
vec4 applyDiffuseLighting(vec4 color) {
vec3 lightDirection = vec3(1, 1, -1);
float light = saturate(dot(vertexWorldNormal, normalize(-lightDirection)));
vec3 diffusion = diffuseLight.rgb * light;
diffusion += ambientLight.rgb;
diffusion = saturate(diffusion);
color.rgb *= diffusion;
return color;
}
vec4 applyFog(vec4 color) {
float fogFactor = (fogEnd - cameraDistance) / (fogEnd - fogStart);
fogFactor = 1.0 - clamp(fogFactor, 0.0, 1.0);
float fogColorFactor = fogFactor * fogModifier;
// Only mix fog color for simple blending modes.
if (blendingMode <= 2) {
color.rgb = mix(color.rgb, fogColor.rgb, fogColorFactor);
}
// Ensure certain blending mode pixels become fully opaque by fog end.
if (cameraDistance >= fogEnd) {
color.rgb = fogColor.rgb;
color.a = 1.0;
}
// Ensure certain blending mode pixels fade out as fog increases.
if (blendingMode >= 2 && blendingMode < 6) {
color.a *= 1.0 - fogFactor;
}
return color;
}
vec4 finalizeColor(vec4 color) {
if (lightModifier > 0.0) {
color = applyDiffuseLighting(color);
}
color = applyFog(color);
return color;
}
void main() {
vec4 color;
// -1 = unknown / unhandled
// Stopgap until all shaders are implemented and verified
if (fragmentShaderMode == -1) {
color = texture2D(textures[0], uv1);
} else if (fragmentShaderMode == 0) {
color = fragCombinersWrath1Pass(textures[0], uv1);
} else if (fragmentShaderMode == 1) {
color = fragCombinersWrath2Pass(textures[0], uv1, textures[1], uv2);
}
// Apply lighting and fog.
color = finalizeColor(color);
gl_FragColor = color;
}
================================================
FILE: src/lib/pipeline/m2/material/shader.vert
================================================
precision highp float;
varying vec2 uv1;
varying vec2 uv2;
varying float cameraDistance;
varying vec3 vertexWorldNormal;
uniform vec3 animatedVertexColorRGB;
uniform float animatedVertexColorAlpha;
uniform float animatedTransparency;
uniform mat4 animatedUVs[4];
varying vec4 animatedVertexColor;
uniform float billboarded;
#ifdef USE_SKINNING
uniform mat4 bindMatrix;
uniform mat4 bindMatrixInverse;
#ifdef BONE_TEXTURE
uniform sampler2D boneTexture;
uniform int boneTextureWidth;
uniform int boneTextureHeight;
mat4 getBoneMatrix( const in float i ) {
float j = i * 4.0;
float x = mod( j, float( boneTextureWidth ) );
float y = floor( j / float( boneTextureWidth ) );
float dx = 1.0 / float( boneTextureWidth );
float dy = 1.0 / float( boneTextureHeight );
y = dy * ( y + 0.5 );
vec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );
vec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );
vec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );
vec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );
mat4 bone = mat4( v1, v2, v3, v4 );
return bone;
}
#else
uniform mat4 boneGlobalMatrices[ MAX_BONES ];
mat4 getBoneMatrix( const in float i ) {
mat4 bone = boneGlobalMatrices[ int(i) ];
return bone;
}
#endif
#endif
void main() {
// TODO: Use vertexShaderMode to determine coordinates
uv1 = vec2(uv[0], uv[1]);
uv2 = vec2(uv[0], uv[1]);
// Apply texture animations
vec4 uv1a = animatedUVs[0] * vec4(uv1, 0, 1.0);
uv1 = uv1a.xy / uv1a.w;
vec4 uv2a = animatedUVs[1] * vec4(uv2, 0, 1.0);
uv2 = uv2a.xy / uv2a.w;
// TODO: Will this be needed in the fragment shader at some point?
vec3 vertexWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
cameraDistance = distance(cameraPosition, vertexWorldPosition);
// Account for adjustments (eg. model rotation) in world space
// TODO: Do we need to account for skinning?
vertexWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
animatedVertexColor.rgb = animatedVertexColorRGB.xyz * 0.5;
animatedVertexColor.a = animatedVertexColorAlpha;
vec3 transformed = vec3(position);
#ifdef USE_SKINNING
mat4 boneMatX = getBoneMatrix(skinIndex.x);
mat4 boneMatY = getBoneMatrix(skinIndex.y);
mat4 boneMatZ = getBoneMatrix(skinIndex.z);
mat4 boneMatW = getBoneMatrix(skinIndex.w);
#endif
#ifdef USE_SKINNING
vec4 skinVertex = bindMatrix * vec4(transformed, 1.0);
vec4 skinned = vec4( 0.0 );
skinned += boneMatX * skinVertex * skinWeight.x;
skinned += boneMatY * skinVertex * skinWeight.y;
skinned += boneMatZ * skinVertex * skinWeight.z;
skinned += boneMatW * skinVertex * skinWeight.w;
skinned = bindMatrixInverse * skinned;
#endif
#ifdef USE_SKINNING
vec4 mvPosition = modelViewMatrix * skinned;
#else
vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.0);
#endif
gl_Position = projectionMatrix * mvPosition;
}
================================================
FILE: src/lib/pipeline/m2/submesh.js
================================================
import THREE from 'three';
class Submesh extends THREE.Group {
constructor(opts) {
super();
this.matrixAutoUpdate = opts.matrixAutoUpdate;
this.useSkinning = opts.useSkinning;
this.rootBone = null;
this.billboarded = false;
if (this.useSkinning) {
// Preserve the rootBone for the submesh such that its skin property can be assigned to the
// first child batch mesh.
this.rootBone = opts.rootBone;
this.billboarded = opts.rootBone.userData.billboarded;
// Preserve the skeleton for use in applying batches.
this.skeleton = opts.skeleton;
}
// Preserve the geometry for use in applying batches.
this.geometry = opts.geometry;
}
// Submeshes get one mesh per batch, which allows them to effectively simulate multiple
// render passes. Batch mesh rendering order should be handled properly by the three.js
// renderer.
applyBatches(batches) {
this.clearBatches();
const batchLen = batches.length;
for (let batchIndex = 0; batchIndex < batchLen; ++batchIndex) {
const batchMaterial = batches[batchIndex];
// If the submesh is billboarded, flag the material as billboarded.
if (this.billboarded) {
batchMaterial.enableBillboarding();
}
let batchMesh;
// Only use a skinned mesh if the submesh uses skinning.
if (this.useSkinning) {
batchMesh = new THREE.SkinnedMesh(this.geometry, batchMaterial);
batchMesh.bind(this.skeleton);
} else {
batchMesh = new THREE.Mesh(this.geometry, batchMaterial);
}
batchMesh.matrixAutoUpdate = this.matrixAutoUpdate;
this.add(batchMesh);
}
if (this.useSkinning) {
this.rootBone.skin = this.children[0];
}
}
// Remove any existing child batch meshes.
clearBatches() {
const childrenLength = this.children.length;
for (let childIndex = 0; childIndex < childrenLength; ++childIndex) {
const child = this.children[childIndex];
this.remove(child);
}
if (this.useSkinning) {
// If all batch meshes are cleared, there is no longer a skin to associate with the
// root bone.
this.rootBone.skin = null;
}
}
// Update all existing batch mesh materials to point to the new skins (textures).
set displayInfo(displayInfo) {
const { path } = displayInfo.modelData;
const skin1 = `${path}${displayInfo.skin1}.blp`;
const skin2 = `${path}${displayInfo.skin2}.blp`;
const skin3 = `${path}${displayInfo.skin3}.blp`;
const childrenLength = this.children.length;
for (let childIndex = 0; childIndex < childrenLength; ++childIndex) {
const child = this.children[childIndex];
child.material.updateSkinTextures(skin1, skin2, skin3);
}
}
dispose() {
this.geometry.dispose();
this.children.forEach((child) => {
child.geometry.dispose();
child.material.dispose();
});
}
}
export default Submesh;
================================================
FILE: src/lib/pipeline/material.js
================================================
import THREE from 'three';
const loader = new THREE.TextureLoader();
class Material extends THREE.MeshBasicMaterial {
constructor(params = {}) {
params.wireframe = true;
super(params);
}
set texture(path) {
loader.load(encodeURI(`pipeline/${path}.png`), (texture) => {
texture.flipY = false;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
this.wireframe = false;
this.map = texture;
this.needsUpdate = true;
});
}
}
export default Material;
================================================
FILE: src/lib/pipeline/texture-loader.js
================================================
import THREE from 'three';
const loader = new THREE.TextureLoader();
class TextureLoader {
static cache = new Map();
static references = new Map();
static pendingUnload = new Set();
static unloaderRunning = false;
static UNLOAD_INTERVAL = 15000;
static load(rawPath, wrapS = THREE.RepeatWrapping, wrapT = THREE.RepeatWrapping, flipY = true) {
const path = rawPath.toUpperCase();
// Ensure we cache based on texture settings. Some textures are reused with different settings.
const textureKey = `${path};ws:${wrapS.toString()};wt:${wrapT.toString()};fy:${flipY}}`;
// Prevent unintended unloading.
if (this.pendingUnload.has(textureKey)) {
this.pendingUnload.delete(textureKey);
}
// Background unloader might need to be started.
if (!this.unloaderRunning) {
this.unloaderRunning = true;
this.backgroundUnload();
}
// Keep track of references.
let refCount = this.references.get(textureKey) || 0;
++refCount;
this.references.set(textureKey, refCount);
const encodedPath = encodeURI(`pipeline/${path}.png`);
if (!this.cache.has(textureKey)) {
// TODO: Promisify THREE's TextureLoader callbacks
this.cache.set(textureKey, loader.load(encodedPath, function(texture) {
texture.sourceFile = path;
texture.textureKey = textureKey;
texture.wrapS = wrapS;
texture.wrapT = wrapT;
texture.flipY = flipY;
texture.needsUpdate = true;
}));
}
return this.cache.get(textureKey);
}
static unload(texture) {
const textureKey = texture.textureKey;
let refCount = this.references.get(textureKey) || 1;
--refCount;
if (refCount === 0) {
this.pendingUnload.add(textureKey);
} else {
this.references.set(textureKey, refCount);
}
}
static backgroundUnload() {
this.pendingUnload.forEach((textureKey) => {
if (this.cache.has(textureKey)) {
this.cache.get(textureKey).dispose();
}
this.cache.delete(textureKey);
this.references.delete(textureKey);
this.pendingUnload.delete(textureKey);
});
setTimeout(this.backgroundUnload.bind(this), this.UNLOAD_INTERVAL);
}
}
export default TextureLoader;
================================================
FILE: src/lib/pipeline/wdt/index.js
================================================
import WorkerPool from '../worker/pool';
class WDT {
static cache = {};
constructor(data) {
this.data = data;
}
static load(path) {
if (!(path in this.cache)) {
this.cache[path] = WorkerPool.enqueue('WDT', path).then((args) => {
const [data] = args;
return new this(data);
});
}
return this.cache[path];
}
}
export default WDT;
================================================
FILE: src/lib/pipeline/wdt/loader.js
================================================
import { DecodeStream } from 'blizzardry/lib/restructure';
import WDT from 'blizzardry/lib/wdt';
import Loader from '../../net/loader';
const loader = new Loader();
export default function(path) {
return loader.load(path).then((raw) => {
const buffer = new Buffer(new Uint8Array(raw));
const stream = new DecodeStream(buffer);
const data = WDT.decode(stream);
return data;
});
}
================================================
FILE: src/lib/pipeline/wmo/blueprint.js
================================================
import WorkerPool from '../worker/pool';
import WMO from './';
class WMOBlueprint {
static cache = new Map();
static references = new Map();
static pendingUnload = new Set();
static unloaderRunning = false;
static UNLOAD_INTERVAL = 15000;
static load(rawPath) {
const path = rawPath.toUpperCase();
// Prevent unintended unloading.
if (this.pendingUnload.has(path)) {
this.pendingUnload.delete(path);
}
// Background unloader might need to be started.
if (!this.unloaderRunning) {
this.unloaderRunning = true;
this.backgroundUnload();
}
// Keep track of references.
let refCount = this.references.get(path) || 0;
++refCount;
this.references.set(path, refCount);
if (!this.cache.has(path)) {
this.cache.set(path, WorkerPool.enqueue('WMO', path).then((args) => {
const [data] = args;
return new WMO(path, data);
}));
}
return this.cache.get(path).then((wmo) => {
return wmo.clone();
});
}
static unload(wmo) {
const path = wmo.path.toUpperCase();
let refCount = this.references.get(path) || 1;
--refCount;
if (refCount === 0) {
this.pendingUnload.add(path);
} else {
this.references.set(path, refCount);
}
}
static backgroundUnload() {
this.pendingUnload.forEach((path) => {
this.cache.delete(path);
this.references.delete(path);
this.pendingUnload.delete(path);
});
setTimeout(this.backgroundUnload.bind(this), this.UNLOAD_INTERVAL);
}
}
export default WMOBlueprint;
================================================
FILE: src/lib/pipeline/wmo/group/blueprint.js
================================================
import WorkerPool from '../../worker/pool';
import WMOGroup from './';
class WMOGroupBlueprint {
static cache = new Map();
static references = new Map();
static pendingUnload = new Set();
static unloaderRunning = false;
static UNLOAD_INTERVAL = 15000;
static load(wmo, id, rawPath) {
const path = rawPath.toUpperCase();
// Prevent unintended unloading.
if (this.pendingUnload.has(path)) {
this.pendingUnload.delete(path);
}
// Background unloader might need to be started.
if (!this.unloaderRunning) {
this.unloaderRunning = true;
this.backgroundUnload();
}
// Keep track of references.
let refCount = this.references.get(path) || 0;
++refCount;
this.references.set(path, refCount);
if (!this.cache.has(path)) {
this.cache.set(path, WorkerPool.enqueue('WMOGroup', path).then((args) => {
const [data] = args;
return new WMOGroup(wmo, id, data, path);
}));
}
return this.cache.get(path).then((wmoGroup) => {
return wmoGroup.clone();
});
}
static loadWithID(wmo, id) {
const suffix = `000${id}`.slice(-3);
const groupPath = wmo.path.replace(/\.wmo/i, `_${suffix}.wmo`);
return this.load(wmo, id, groupPath);
}
static unload(wmoGroup) {
wmoGroup.dispose();
const path = wmoGroup.path.toUpperCase();
let refCount = this.references.get(path) || 1;
--refCount;
if (refCount === 0) {
this.pendingUnload.add(path);
} else {
this.references.set(path, refCount);
}
}
static backgroundUnload() {
this.pendingUnload.forEach((path) => {
if (this.cache.has(path)) {
this.cache.get(path).then((wmoGroup) => {
wmoGroup.dispose();
});
}
this.cache.delete(path);
this.references.delete(path);
this.pendingUnload.delete(path);
});
setTimeout(this.backgroundUnload.bind(this), this.UNLOAD_INTERVAL);
}
}
export default WMOGroupBlueprint;
================================================
FILE: src/lib/pipeline/wmo/group/index.js
================================================
import THREE from 'three';
import WMOMaterial from '../material';
class WMOGroup extends THREE.Mesh {
static cache = {};
constructor(wmo, id, data, path) {
super();
this.dispose = ::this.dispose;
this.matrixAutoUpdate = false;
this.wmo = wmo;
this.groupID = id;
this.data = data;
this.path = path;
this.indoor = data.indoor;
this.animated = false;
const vertexCount = data.MOVT.vertices.length;
const textureCoords = data.MOTV.textureCoords;
const positions = new Float32Array(vertexCount * 3);
const normals = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
const colors = new Float32Array(vertexCount * 3);
const alphas = new Float32Array(vertexCount);
data.MOVT.vertices.forEach(function(vertex, index) {
// Provided as (X, Z, -Y)
positions[index * 3] = vertex[0];
positions[index * 3 + 1] = vertex[2];
positions[index * 3 + 2] = -vertex[1];
uvs[index * 2] = textureCoords[index][0];
uvs[index * 2 + 1] = textureCoords[index][1];
});
data.MONR.normals.forEach(function(normal, index) {
normals[index * 3] = normal[0];
normals[index * 3 + 1] = normal[2];
normals[index * 3 + 2] = -normal[1];
});
if ('MOCV' in data) {
data.MOCV.colors.forEach(function(color, index) {
colors[index * 3] = color.r / 255.0;
colors[index * 3 + 1] = color.g / 255.0;
colors[index * 3 + 2] = color.b / 255.0;
alphas[index] = color.a / 255.0;
});
} else if (this.indoor) {
// Default indoor vertex color: rgba(0.5, 0.5, 0.5, 1.0)
data.MOVT.vertices.forEach(function(_vertex, index) {
colors[index * 3] = 127.0 / 255.0;
colors[index * 3 + 1] = 127.0 / 255.0;
colors[index * 3 + 2] = 127.0 / 255.0;
alphas[index] = 1.0;
});
}
const indices = new Uint32Array(data.MOVI.triangles);
const geometry = this.geometry = new THREE.BufferGeometry();
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2));
// TODO: Perhaps it is possible to directly use a vec4 here? Currently, color + alpha is
// combined into a vec4 in the material's vertex shader. For some reason, attempting to
// directly use a BufferAttribute with a length of 4 resulted in incorrect ordering for the
// values in the shader.
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.addAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
// Mirror geometry over X and Y axes and rotate
const matrix = new THREE.Matrix4();
matrix.makeScale(-1, -1, 1);
geometry.applyMatrix(matrix);
geometry.rotateX(-Math.PI / 2);
const materialIDs = [];
data.MOBA.batches.forEach(function(batch) {
materialIDs.push(batch.materialID);
geometry.addGroup(batch.firstIndex, batch.indexCount, batch.materialID);
});
const materialDefs = this.wmo.data.MOM
gitextract_5ysnh6kx/ ├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin/ │ └── serve ├── gulpfile.babel.js ├── package.json ├── src/ │ ├── bootstrapper.jsx │ ├── components/ │ │ ├── auth/ │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── characters/ │ │ │ └── index.jsx │ │ ├── game/ │ │ │ ├── chat/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ ├── controls.jsx │ │ │ ├── hud/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ ├── index.jsx │ │ │ ├── index.styl │ │ │ ├── portrait/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ ├── quests/ │ │ │ │ ├── index.jsx │ │ │ │ └── index.styl │ │ │ └── stats/ │ │ │ ├── index.jsx │ │ │ └── index.styl │ │ ├── kit/ │ │ │ └── index.jsx │ │ ├── realms/ │ │ │ └── index.jsx │ │ └── wowser/ │ │ ├── index.jsx │ │ ├── index.styl │ │ ├── session.jsx │ │ └── ui/ │ │ ├── form/ │ │ │ └── index.styl │ │ ├── frame/ │ │ │ ├── dividers/ │ │ │ │ └── index.styl │ │ │ └── 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/ │ │ │ │ └── sha1.js │ │ │ ├── hash.js │ │ │ └── srp.js │ │ ├── game/ │ │ │ ├── chat/ │ │ │ │ ├── handler.js │ │ │ │ └── message.js │ │ │ ├── entity.js │ │ │ ├── guid.js │ │ │ ├── handler.js │ │ │ ├── opcode.js │ │ │ ├── packet.js │ │ │ ├── player.js │ │ │ ├── unit.js │ │ │ └── world/ │ │ │ ├── content-queue.js │ │ │ ├── doodad-manager.js │ │ │ ├── handler.js │ │ │ ├── map.js │ │ │ ├── terrain-manager.js │ │ │ └── wmo-manager/ │ │ │ ├── index.js │ │ │ └── wmo-handler.js │ │ ├── index.js │ │ ├── net/ │ │ │ ├── loader.js │ │ │ ├── packet.js │ │ │ └── socket.js │ │ ├── pipeline/ │ │ │ ├── adt/ │ │ │ │ ├── chunk/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── material.js │ │ │ │ │ ├── shader.frag │ │ │ │ │ └── shader.vert │ │ │ │ ├── index.js │ │ │ │ └── loader.js │ │ │ ├── dbc/ │ │ │ │ ├── index.js │ │ │ │ └── loader.js │ │ │ ├── m2/ │ │ │ │ ├── animation-manager.js │ │ │ │ ├── batch-manager.js │ │ │ │ ├── blueprint.js │ │ │ │ ├── index.js │ │ │ │ ├── loader.js │ │ │ │ ├── material/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── shader.frag │ │ │ │ │ └── shader.vert │ │ │ │ └── submesh.js │ │ │ ├── material.js │ │ │ ├── texture-loader.js │ │ │ ├── wdt/ │ │ │ │ ├── index.js │ │ │ │ └── loader.js │ │ │ ├── wmo/ │ │ │ │ ├── blueprint.js │ │ │ │ ├── group/ │ │ │ │ │ ├── blueprint.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── loader.js │ │ │ │ ├── index.js │ │ │ │ ├── loader.js │ │ │ │ └── material/ │ │ │ │ ├── index.js │ │ │ │ ├── shader.frag │ │ │ │ └── shader.vert │ │ │ └── worker/ │ │ │ ├── index.js │ │ │ ├── pool.js │ │ │ ├── task.js │ │ │ └── thread.js │ │ ├── realms/ │ │ │ ├── handler.js │ │ │ └── realm.js │ │ ├── server/ │ │ │ ├── .babelrc │ │ │ ├── cluster.js │ │ │ ├── config/ │ │ │ │ ├── index.js │ │ │ │ └── setup-prompts.js │ │ │ ├── index.js │ │ │ └── pipeline/ │ │ │ ├── archive.js │ │ │ └── index.js │ │ └── utils/ │ │ ├── array-util.js │ │ └── object-util.js │ └── spec/ │ ├── .eslintrc │ ├── sample-spec.js │ └── spec-helper.js └── webpack.config.js
SYMBOL INDEX (475 symbols across 75 files)
FILE: src/components/auth/index.jsx
class AuthScreen (line 5) | class AuthScreen extends React.Component {
method constructor (line 10) | constructor() {
method componentWillUnmount (line 30) | componentWillUnmount() {
method connect (line 36) | connect(host, port) {
method authenticate (line 40) | authenticate(username, password) {
method _onAuthenticate (line 44) | _onAuthenticate() {
method _onChange (line 48) | _onChange(event) {
method _onConnect (line 54) | _onConnect() {
method _onSubmit (line 58) | _onSubmit(event) {
method render (line 63) | render() {
FILE: src/components/characters/index.jsx
class CharactersScreen (line 5) | class CharactersScreen extends React.Component {
method constructor (line 10) | constructor() {
method componentWillUnmount (line 29) | componentWillUnmount() {
method join (line 34) | join(character) {
method refresh (line 38) | refresh() {
method _onCharacterSelect (line 42) | _onCharacterSelect(event) {
method _onJoin (line 46) | _onJoin() {
method _onRefresh (line 50) | _onRefresh() {
method _onSubmit (line 58) | _onSubmit(event) {
method render (line 63) | render() {
FILE: src/components/game/chat/index.jsx
class ChatPanel (line 8) | class ChatPanel extends React.Component {
method constructor (line 10) | constructor() {
method componentDidUpdate (line 25) | componentDidUpdate() {
method send (line 29) | send(text) {
method _onChange (line 35) | _onChange(event) {
method _onMessage (line 39) | _onMessage() {
method _onSubmit (line 43) | _onSubmit(event) {
method render (line 51) | render() {
FILE: src/components/game/controls.jsx
class Controls (line 5) | class Controls extends React.Component {
method constructor (line 12) | constructor(props) {
method componentWillUnmount (line 71) | componentWillUnmount() {
method update (line 79) | update() {
method rotateHorizontally (line 163) | rotateHorizontally(angle) {
method rotateVertically (line 167) | rotateVertically(angle) {
method zoomOut (line 171) | zoomOut() {
method zoomIn (line 175) | zoomIn() {
method _onMouseDown (line 179) | _onMouseDown(event) {
method _onMouseUp (line 184) | _onMouseUp() {
method _onMouseMove (line 188) | _onMouseMove(event) {
method _onMouseWheel (line 209) | _onMouseWheel(event) {
method render (line 223) | render() {
FILE: src/components/game/hud/index.jsx
class HUD (line 10) | class HUD extends React.Component {
method render (line 12) | render() {
FILE: src/components/game/index.jsx
class GameScreen (line 11) | class GameScreen extends React.Component {
method constructor (line 16) | constructor() {
method componentDidMount (line 36) | componentDidMount() {
method componentWillUnmount (line 49) | componentWillUnmount() {
method aspectRatio (line 63) | get aspectRatio() {
method resize (line 67) | resize() {
method animate (line 73) | animate() {
method render (line 96) | render() {
FILE: src/components/game/portrait/index.jsx
class Portrait (line 6) | class Portrait extends React.Component {
method render (line 14) | render() {
FILE: src/components/game/quests/index.jsx
class QuestsPanel (line 5) | class QuestsPanel extends React.Component {
method render (line 7) | render() {
FILE: src/components/game/stats/index.jsx
class Stats (line 5) | class Stats extends React.Component {
method mapStats (line 12) | mapStats() {
method render (line 68) | render() {
FILE: src/components/kit/index.jsx
class KitScreen (line 3) | class KitScreen extends React.Component {
method render (line 8) | render() {
FILE: src/components/realms/index.jsx
class RealmsScreen (line 5) | class RealmsScreen extends React.Component {
method constructor (line 10) | constructor() {
method componentWillUnmount (line 29) | componentWillUnmount() {
method connect (line 34) | connect(realm) {
method refresh (line 38) | refresh() {
method _onAuthenticate (line 42) | _onAuthenticate() {
method _onRealmSelect (line 46) | _onRealmSelect(event) {
method _onRefresh (line 50) | _onRefresh() {
method _onSubmit (line 58) | _onSubmit(event) {
method render (line 63) | render() {
FILE: src/components/wowser/index.jsx
class Wowser (line 12) | class Wowser extends React.Component {
method constructor (line 22) | constructor() {
method currentScreen (line 35) | get currentScreen() {
method _onScreenChange (line 42) | _onScreenChange(_from, to) {
method _onScreenSelect (line 46) | _onScreenSelect(event) {
method render (line 50) | render() {
FILE: src/components/wowser/session.jsx
class Session (line 3) | class Session extends Client {
method constructor (line 5) | constructor() {
method screen (line 11) | get screen() {
method screen (line 15) | set screen(screen) {
FILE: src/lib/auth/challenge-opcode.js
class ChallengeOpcode (line 1) | class ChallengeOpcode {
FILE: src/lib/auth/handler.js
class AuthHandler (line 7) | class AuthHandler extends Socket {
method constructor (line 13) | constructor(session) {
method key (line 35) | get key() {
method connect (line 40) | connect(host, port = NaN) {
method authenticate (line 49) | authenticate(account, password) {
method dataReceived (line 93) | dataReceived() {
method handleLogonChallenge (line 113) | handleLogonChallenge(ap) {
method handleLogonProof (line 160) | handleLogonProof(ap) {
FILE: src/lib/auth/opcode.js
class Opcode (line 1) | class Opcode {
FILE: src/lib/auth/packet.js
class AuthPacket (line 5) | class AuthPacket extends BasePacket {
method constructor (line 10) | constructor(opcode, source, outgoing = true) {
method opcodeName (line 15) | get opcodeName() {
method finalize (line 20) | finalize() {
FILE: src/lib/characters/character.js
class Character (line 1) | class Character {
method toString (line 4) | toString() {
FILE: src/lib/characters/handler.js
class CharacterHandler (line 7) | class CharacterHandler extends EventEmitter {
method constructor (line 10) | constructor(session) {
method refresh (line 24) | refresh() {
method handleCharacterList (line 33) | handleCharacterList(gp) {
FILE: src/lib/config.js
class Raw (line 1) | class Raw {
method constructor (line 2) | constructor(config) {
method raw (line 6) | raw(value) {
method locale (line 10) | get locale() {
method os (line 14) | get os() {
method platform (line 18) | get platform() {
class Config (line 24) | class Config {
method constructor (line 26) | constructor() {
method version (line 39) | set version(version) {
FILE: src/lib/crypto/big-num.js
class BigNum (line 4) | class BigNum {
method constructor (line 10) | constructor(value, radix) {
method toString (line 23) | toString() {
method bi (line 28) | get bi() {
method mod (line 33) | mod(m) {
method modPow (line 38) | modPow(e, m) {
method add (line 43) | add(o) {
method subtract (line 48) | subtract(o) {
method multiply (line 53) | multiply(o) {
method divide (line 58) | divide(o) {
method equals (line 63) | equals(o) {
method toArray (line 68) | toArray(littleEndian = true, unsigned = true) {
method fromArray (line 83) | static fromArray(bytes, littleEndian = true, unsigned = true) {
method fromRand (line 102) | static fromRand(length) {
FILE: src/lib/crypto/crypt.js
class Crypt (line 6) | class Crypt {
method constructor (line 9) | constructor() {
method encrypt (line 18) | encrypt(data) {
method decrypt (line 26) | decrypt(data) {
method key (line 34) | set key(key) {
FILE: src/lib/crypto/hash.js
class Hash (line 4) | class Hash {
method constructor (line 7) | constructor() {
method digest (line 19) | get digest() {
method reset (line 27) | reset() {
method feed (line 34) | feed(value) {
method finalize (line 49) | finalize() {
FILE: src/lib/crypto/hash/sha1.js
class SHA1 (line 6) | class SHA1 extends Hash {
method finalize (line 9) | finalize() {
FILE: src/lib/crypto/srp.js
class SRP (line 8) | class SRP {
method constructor (line 11) | constructor(N, g) {
method A (line 65) | get A() {
method K (line 70) | get K() {
method M1 (line 75) | get M1() {
method feed (line 80) | feed(s, B, I, P) {
method validate (line 176) | validate(M2) {
FILE: src/lib/game/chat/handler.js
class ChatHandler (line 5) | class ChatHandler extends EventEmitter {
method constructor (line 8) | constructor(session) {
method create (line 33) | create() {
method send (line 38) | send(_message) {
method handleMessage (line 43) | handleMessage(gp) {
FILE: src/lib/game/chat/message.js
class ChatMessage (line 1) | class ChatMessage {
method constructor (line 4) | constructor(kind, text) {
method toString (line 11) | toString() {
FILE: src/lib/game/entity.js
class Entity (line 3) | class Entity extends EventEmitter {
method constructor (line 5) | constructor() {
FILE: src/lib/game/guid.js
class GUID (line 1) | class GUID {
method constructor (line 7) | constructor(buffer) {
method toString (line 21) | toString() {
FILE: src/lib/game/handler.js
class GameHandler (line 11) | class GameHandler extends Socket {
method constructor (line 14) | constructor(session) {
method connect (line 30) | connect(host, port) {
method send (line 39) | send(packet) {
method join (line 55) | join(character) {
method dataReceived (line 68) | dataReceived(_socket) {
method handleAuthChallenge (line 110) | handleAuthChallenge(gp) {
method handleAuthResponse (line 152) | handleAuthResponse(gp) {
method handleWorldLogin (line 175) | handleWorldLogin(_gp) {
FILE: src/lib/game/opcode.js
class GameOpcode (line 1) | class GameOpcode {
FILE: src/lib/game/packet.js
class GamePacket (line 6) | class GamePacket extends BasePacket {
method constructor (line 16) | constructor(opcode, source, outgoing = true) {
method opcodeName (line 24) | get opcodeName() {
method headerSize (line 29) | get headerSize() {
method readGUID (line 37) | readGUID() {
method writeGUID (line 42) | writeGUID(guid) {
FILE: src/lib/game/player.js
class Player (line 3) | class Player extends Unit {
method constructor (line 5) | constructor() {
method worldport (line 18) | worldport(mapID, x, y, z) {
FILE: src/lib/game/unit.js
class Unit (line 7) | class Unit extends Entity {
method constructor (line 9) | constructor() {
method position (line 31) | get position() {
method displayID (line 35) | get displayID() {
method displayID (line 39) | set displayID(displayID) {
method view (line 62) | get view() {
method model (line 66) | get model() {
method model (line 70) | set model(m2) {
method ascend (line 93) | ascend(delta) {
method descend (line 98) | descend(delta) {
method moveForward (line 103) | moveForward(delta) {
method moveBackward (line 108) | moveBackward(delta) {
method rotateLeft (line 113) | rotateLeft(delta) {
method rotateRight (line 118) | rotateRight(delta) {
method strafeLeft (line 123) | strafeLeft(delta) {
method strafeRight (line 128) | strafeRight(delta) {
FILE: src/lib/game/world/content-queue.js
class ContentQueue (line 1) | class ContentQueue {
method constructor (line 3) | constructor(processor, interval = 1, workFactor = 1, minWork = 1) {
method has (line 18) | has(key) {
method add (line 22) | add(key, job) {
method remove (line 30) | remove(key) {
method schedule (line 41) | schedule() {
method run (line 45) | run() {
method clear (line 65) | clear() {
FILE: src/lib/game/world/doodad-manager.js
class DoodadManager (line 3) | class DoodadManager {
method constructor (line 14) | constructor(map) {
method loadChunk (line 35) | loadChunk(index, entries) {
method unloadChunk (line 62) | unloadChunk(index, entries) {
method loadDoodads (line 88) | loadDoodads() {
method loadDoodad (line 115) | loadDoodad(entry) {
method enableDoodadAnimations (line 133) | enableDoodadAnimations(entry, doodad) {
method unloadDoodads (line 147) | unloadDoodads() {
method unloadDoodad (line 176) | unloadDoodad(entry) {
method placeDoodad (line 186) | placeDoodad(doodad, position, rotation, scale) {
method animate (line 214) | animate(delta, camera, cameraMoved) {
FILE: src/lib/game/world/handler.js
class WorldHandler (line 7) | class WorldHandler extends EventEmitter {
method constructor (line 9) | constructor(session) {
method add (line 84) | add(entity) {
method remove (line 92) | remove(entity) {
method renderAtCoords (line 100) | renderAtCoords(x, y) {
method changeMap (line 107) | changeMap(mapID) {
method changeModel (line 118) | changeModel(_unit, _oldModel, _newModel) {
method changePosition (line 121) | changePosition(player) {
method animate (line 125) | animate(delta, camera, cameraMoved) {
method animateEntities (line 136) | animateEntities(delta, camera, cameraMoved) {
FILE: src/lib/game/world/map.js
class WorldMap (line 11) | class WorldMap extends THREE.Group {
method constructor (line 20) | constructor(data, wdt) {
method internalName (line 40) | get internalName() {
method render (line 44) | render(x, y) {
method chunkIndicesAround (line 69) | chunkIndicesAround(chunkX, chunkY, radius) {
method loadChunkByIndex (line 84) | loadChunkByIndex(index) {
method unloadChunkByIndex (line 102) | unloadChunkByIndex(index) {
method indexFor (line 116) | indexFor(chunkX, chunkY) {
method animate (line 120) | animate(delta, camera, cameraMoved) {
method load (line 125) | static load(id) {
FILE: src/lib/game/world/terrain-manager.js
class TerrainManager (line 1) | class TerrainManager {
method constructor (line 3) | constructor(map) {
method loadChunk (line 7) | loadChunk(_index, terrain) {
method unloadChunk (line 12) | unloadChunk(_index, terrain) {
FILE: src/lib/game/world/wmo-manager/index.js
class WMOManager (line 5) | class WMOManager {
method constructor (line 13) | constructor(map) {
method loadChunk (line 40) | loadChunk(chunkIndex, wmoEntries) {
method unloadChunk (line 51) | unloadChunk(chunkIndex, wmoEntries) {
method addChunkRef (line 67) | addChunkRef(chunkIndex, wmoEntry) {
method removeChunkRef (line 86) | removeChunkRef(chunkIndex, wmoEntry) {
method enqueueLoadEntry (line 101) | enqueueLoadEntry(wmoEntry) {
method dequeueLoadEntry (line 114) | dequeueLoadEntry(wmoEntry) {
method scheduleUnloadEntry (line 127) | scheduleUnloadEntry(wmoEntry) {
method cancelUnloadEntry (line 137) | cancelUnloadEntry(wmoEntry) {
method processLoadEntry (line 147) | processLoadEntry(wmoEntry) {
method animate (line 159) | animate(delta, camera, cameraMoved) {
FILE: src/lib/game/world/wmo-manager/wmo-handler.js
class WMOHandler (line 6) | class WMOHandler {
method constructor (line 16) | constructor(manager, entry) {
method load (line 57) | load(wmoRoot) {
method enqueueLoadGroups (line 67) | enqueueLoadGroups() {
method enqueueLoadGroup (line 82) | enqueueLoadGroup(wmoGroupID) {
method processLoadGroup (line 94) | processLoadGroup(wmoGroupID) {
method loadGroup (line 116) | loadGroup(wmoGroupID, wmoGroup) {
method enqueueLoadGroupDoodads (line 126) | enqueueLoadGroupDoodads(wmoGroup) {
method enqueueLoadDoodad (line 149) | enqueueLoadDoodad(wmoDoodadEntry) {
method processLoadDoodad (line 161) | processLoadDoodad(wmoDoodadEntry) {
method loadDoodad (line 188) | loadDoodad(wmoDoodadEntry, wmoDoodad) {
method scheduleUnload (line 206) | scheduleUnload(unloadDelay = 0) {
method cancelUnload (line 210) | cancelUnload() {
method unload (line 216) | unload() {
method placeRoot (line 260) | placeRoot() {
method placeGroup (line 284) | placeGroup(wmoGroup) {
method placeDoodad (line 289) | placeDoodad(wmoDoodadEntry, wmoDoodad) {
method addDoodadRef (line 304) | addDoodadRef(wmoDoodadEntry, wmoGroup) {
method removeDoodadRef (line 325) | removeDoodadRef(wmoDoodadEntry, wmoGroup) {
method groupsForDoodad (line 346) | groupsForDoodad(wmoDoodad) {
method doodadsForGroup (line 361) | doodadsForGroup(wmoGroup) {
method animate (line 379) | animate(delta, camera, cameraMoved) {
FILE: src/lib/index.js
class Client (line 12) | class Client extends EventEmitter {
method constructor (line 14) | constructor(config) {
FILE: src/lib/net/loader.js
class Loader (line 3) | class Loader {
method constructor (line 5) | constructor() {
method load (line 10) | load(path) {
FILE: src/lib/net/packet.js
class Packet (line 3) | class Packet extends ByteBuffer {
method constructor (line 6) | constructor(opcode, source, outgoing = true) {
method headerSize (line 20) | get headerSize() {
method bodySize (line 25) | get bodySize() {
method opcodeName (line 30) | get opcodeName() {
method toString (line 35) | toString() {
method finalize (line 41) | finalize() {
FILE: src/lib/net/socket.js
class Socket (line 5) | class Socket extends EventEmitter {
method constructor (line 12) | constructor() {
method connected (line 31) | get connected() {
method connect (line 36) | connect(host, port = NaN) {
method reconnect (line 77) | reconnect() {
method disconnect (line 85) | disconnect() {
method send (line 93) | send(packet) {
FILE: src/lib/pipeline/adt/chunk/index.js
class Chunk (line 6) | class Chunk extends THREE.Mesh {
method constructor (line 11) | constructor(adt, id) {
method doodadEntries (line 94) | get doodadEntries() {
method wmoEntries (line 98) | get wmoEntries() {
method isHole (line 102) | isHole(y, x) {
method dispose (line 110) | dispose() {
method chunkFor (line 115) | static chunkFor(position) {
method tileFor (line 119) | static tileFor(chunk) {
method load (line 123) | static load(map, chunkX, chunkY) {
FILE: src/lib/pipeline/adt/chunk/material.js
class Material (line 7) | class Material extends THREE.ShaderMaterial {
method constructor (line 9) | constructor(data, textureNames) {
method loadLayers (line 45) | loadLayers() {
method loadAlphaMaps (line 52) | loadAlphaMaps() {
method loadTextures (line 72) | loadTextures() {
method dispose (line 85) | dispose() {
FILE: src/lib/pipeline/adt/index.js
class ADT (line 3) | class ADT {
method constructor (line 9) | constructor(path, data) {
method wmos (line 20) | get wmos() {
method doodads (line 24) | get doodads() {
method textures (line 28) | get textures() {
method positionFor (line 32) | static positionFor(tile) {
method tileFor (line 36) | static tileFor(position) {
method loadTile (line 40) | static loadTile(map, tileX, tileY, wdtFlags) {
method loadAtCoords (line 44) | static loadAtCoords(map, x, y, wdtFlags) {
method load (line 50) | static load(path, wdtFlags) {
FILE: src/lib/pipeline/dbc/index.js
class DBC (line 3) | class DBC {
method constructor (line 7) | constructor(data) {
method index (line 13) | index() {
method load (line 22) | static load(name, id) {
FILE: src/lib/pipeline/m2/animation-manager.js
class AnimationManager (line 4) | class AnimationManager extends EventEmitter {
method constructor (line 6) | constructor(root, animationDefs, sequenceDefs) {
method update (line 32) | update(delta) {
method loadAnimation (line 38) | loadAnimation(animationIndex) {
method unloadAnimation (line 52) | unloadAnimation(animationIndex) {
method playAnimation (line 66) | playAnimation(animationIndex) {
method stopAnimation (line 71) | stopAnimation(animationIndex) {
method loadSequence (line 81) | loadSequence(sequenceIndex) {
method unloadSequence (line 95) | unloadSequence(sequenceIndex) {
method playSequence (line 108) | playSequence(sequenceIndex) {
method playAllSequences (line 113) | playAllSequences() {
method stopSequence (line 119) | stopSequence(sequenceIndex) {
method registerAnimationClips (line 129) | registerAnimationClips(animationDefs) {
method registerSequenceClips (line 136) | registerSequenceClips(sequenceDefs) {
method unregisterTrack (line 143) | unregisterTrack(trackID) {
method registerTrack (line 163) | registerTrack(opts) {
method registerAnimationTrack (line 175) | registerAnimationTrack(opts) {
method registerSequenceTrack (line 216) | registerSequenceTrack(opts) {
FILE: src/lib/pipeline/m2/batch-manager.js
class BatchManager (line 1) | class BatchManager {
method constructor (line 3) | constructor() {
method createDefs (line 6) | createDefs(data, skinData) {
method createDef (line 17) | createDef(data, batchData) {
method resolveTextureIndices (line 96) | resolveTextureIndices(data, batchData) {
method resolveUVAnimationIndices (line 105) | resolveUVAnimationIndices(data, batchData) {
method stubDef (line 114) | stubDef() {
FILE: src/lib/pipeline/m2/blueprint.js
class M2Blueprint (line 4) | class M2Blueprint {
method load (line 15) | static load(rawPath) {
method unload (line 53) | static unload(m2) {
method backgroundUnload (line 72) | static backgroundUnload() {
method animate (line 90) | static animate(delta) {
FILE: src/lib/pipeline/m2/index.js
class M2 (line 8) | class M2 extends THREE.Group {
method constructor (line 12) | constructor(path, data, skinData, instance = null) {
method createSkeleton (line 84) | createSkeleton(boneDefs) {
method createBatches (line 183) | createBatches(data, skinData) {
method createGeometry (line 212) | createGeometry(vertices) {
method createMesh (line 244) | createMesh(geometry, skeleton, rootBones) {
method createSubmeshes (line 275) | createSubmeshes(data, skinData) {
method createSubmeshGeometry (line 300) | createSubmeshGeometry(submeshDef, indices, triangles, vertices) {
method createSubmesh (line 340) | createSubmesh(submeshDef, geometry, batches) {
method createTextureAnimations (line 360) | createTextureAnimations(data) {
method createUVAnimations (line 374) | createUVAnimations(uvAnimationDefs) {
method createTransparencyAnimations (line 416) | createTransparencyAnimations(transparencyAnimationDefs) {
method createVertexColorAnimations (line 438) | createVertexColorAnimations(vertexColorAnimationDefs) {
method applyBillboards (line 472) | applyBillboards(camera) {
method applySphericalBillboard (line 489) | applySphericalBillboard(camera, bone) {
method applyCylindricalZBillboard (line 521) | applyCylindricalZBillboard(camera, bone) {
method displayInfo (line 550) | set displayInfo(displayInfo) {
method detachEventListeners (line 556) | detachEventListeners() {
method dispose (line 563) | dispose() {
method clone (line 575) | clone() {
FILE: src/lib/pipeline/m2/material/index.js
class M2Material (line 7) | class M2Material extends THREE.ShaderMaterial {
method constructor (line 9) | constructor(m2, def) {
method vertexShaderModeFromID (line 86) | vertexShaderModeFromID(shaderID, opCount) {
method fragmentShaderModeFromID (line 99) | fragmentShaderModeFromID(shaderID, opCount) {
method enableBillboarding (line 114) | enableBillboarding() {
method applyRenderFlags (line 123) | applyRenderFlags(renderFlags) {
method applyBlendingMode (line 146) | applyBlendingMode(blendingMode) {
method loadTextures (line 217) | loadTextures() {
method loadTexture (line 233) | loadTexture(textureDef) {
method registerAnimations (line 275) | registerAnimations(def) {
method registerUVAnimations (line 283) | registerUVAnimations(uvAnimationIndices) {
method registerTransparencyAnimation (line 304) | registerTransparencyAnimation(transparencyAnimationIndex) {
method registerVertexColorAnimation (line 324) | registerVertexColorAnimation(vertexColorAnimationIndex) {
method detachEventListeners (line 346) | detachEventListeners() {
method updateSkinTextures (line 353) | updateSkinTextures(skin1, skin2, skin3) {
method dispose (line 361) | dispose() {
FILE: src/lib/pipeline/m2/submesh.js
class Submesh (line 3) | class Submesh extends THREE.Group {
method constructor (line 5) | constructor(opts) {
method applyBatches (line 32) | applyBatches(batches) {
method clearBatches (line 65) | clearBatches() {
method displayInfo (line 80) | set displayInfo(displayInfo) {
method dispose (line 94) | dispose() {
FILE: src/lib/pipeline/material.js
class Material (line 5) | class Material extends THREE.MeshBasicMaterial {
method constructor (line 7) | constructor(params = {}) {
method texture (line 12) | set texture(path) {
FILE: src/lib/pipeline/texture-loader.js
class TextureLoader (line 5) | class TextureLoader {
method load (line 14) | static load(rawPath, wrapS = THREE.RepeatWrapping, wrapT = THREE.Repea...
method unload (line 55) | static unload(texture) {
method backgroundUnload (line 68) | static backgroundUnload() {
FILE: src/lib/pipeline/wdt/index.js
class WDT (line 3) | class WDT {
method constructor (line 7) | constructor(data) {
method load (line 11) | static load(path) {
FILE: src/lib/pipeline/wmo/blueprint.js
class WMOBlueprint (line 4) | class WMOBlueprint {
method load (line 14) | static load(rawPath) {
method unload (line 46) | static unload(wmo) {
method backgroundUnload (line 60) | static backgroundUnload() {
FILE: src/lib/pipeline/wmo/group/blueprint.js
class WMOGroupBlueprint (line 4) | class WMOGroupBlueprint {
method load (line 14) | static load(wmo, id, rawPath) {
method loadWithID (line 46) | static loadWithID(wmo, id) {
method unload (line 53) | static unload(wmoGroup) {
method backgroundUnload (line 68) | static backgroundUnload() {
FILE: src/lib/pipeline/wmo/group/index.js
class WMOGroup (line 5) | class WMOGroup extends THREE.Mesh {
method constructor (line 9) | constructor(wmo, id, data, path) {
method createMultiMaterial (line 100) | createMultiMaterial(materialIDs, materialDefs, texturePaths) {
method createMaterial (line 127) | createMaterial(materialDef, texturePaths) {
method clone (line 146) | clone() {
method dispose (line 150) | dispose() {
FILE: src/lib/pipeline/wmo/index.js
class WMO (line 3) | class WMO extends THREE.Group {
method constructor (line 7) | constructor(path, data) {
method doodadSet (line 34) | doodadSet(doodadSet) {
method clone (line 43) | clone() {
FILE: src/lib/pipeline/wmo/material/index.js
class WMOMaterial (line 7) | class WMOMaterial extends THREE.ShaderMaterial {
method constructor (line 9) | constructor(def, textureDefs) {
method loadTextures (line 88) | loadTextures(textureDefs) {
method dispose (line 105) | dispose() {
FILE: src/lib/pipeline/worker/pool.js
class WorkerPool (line 4) | class WorkerPool {
method constructor (line 6) | constructor(concurrency = this.defaultConcurrency) {
method defaultConcurrency (line 14) | get defaultConcurrency() {
method thread (line 18) | get thread() {
method enqueue (line 31) | enqueue(...args) {
method next (line 38) | next() {
FILE: src/lib/pipeline/worker/task.js
class Task (line 3) | class Task {
method constructor (line 5) | constructor(...args) {
FILE: src/lib/pipeline/worker/thread.js
class Thread (line 3) | class Thread {
method constructor (line 5) | constructor() {
method busy (line 12) | get busy() {
method idle (line 16) | get idle() {
method execute (line 20) | execute(task) {
method _onMessage (line 26) | _onMessage(event) {
FILE: src/lib/realms/handler.js
class RealmsHandler (line 7) | class RealmsHandler extends EventEmitter {
method constructor (line 10) | constructor(session) {
method refresh (line 24) | refresh() {
method handleRealmList (line 36) | handleRealmList(ap) {
FILE: src/lib/realms/realm.js
class Realm (line 1) | class Realm {
method constructor (line 4) | constructor() {
method toString (line 27) | toString() {
method host (line 32) | get host() {
method port (line 37) | get port() {
method address (line 42) | get address() {
method address (line 47) | set address(address) {
FILE: src/lib/server/cluster.js
class Cluster (line 6) | class Cluster {
method clustered (line 8) | get clustered() {
method workerCount (line 12) | get workerCount() {
method serverPort (line 16) | get serverPort() {
method start (line 20) | start() {
method spawn (line 37) | spawn() {
FILE: src/lib/server/config/index.js
class ServerConfig (line 8) | class ServerConfig {
method constructor (line 17) | constructor(defaults = this.constructor.DEFAULTS) {
method isFirstRun (line 21) | get isFirstRun() {
method verify (line 25) | verify() {
method prompt (line 32) | prompt() {
FILE: src/lib/server/index.js
class Server (line 6) | class Server {
method constructor (line 8) | constructor(port, root = process.pwd) {
method start (line 20) | start() {
FILE: src/lib/server/pipeline/archive.js
class Archive (line 4) | class Archive {
method build (line 22) | static build(root) {
FILE: src/lib/server/pipeline/index.js
class Pipeline (line 11) | class Pipeline {
method DATA_DIR (line 13) | static get DATA_DIR() {
method constructor (line 17) | constructor() {
method archive (line 26) | get archive() {
method resource (line 31) | resource(req, _res, next, path) {
method blp (line 46) | blp(req, res) {
method dbc (line 58) | dbc(req, res) {
method find (line 85) | find(req, res) {
method serve (line 99) | serve(req, res) {
FILE: src/lib/utils/array-util.js
class ArrayUtil (line 1) | class ArrayUtil {
method fromHex (line 4) | static fromHex(hex) {
FILE: src/lib/utils/object-util.js
class ObjectUtil (line 1) | class ObjectUtil {
method keyByValue (line 4) | static keyByValue(object, target) {
Condensed preview — 126 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (229K chars).
[
{
"path": ".babelrc",
"chars": 260,
"preview": "{\n \"plugins\": [\n \"transform-class-properties\",\n \"transform-export-extensions\",\n \"transform-function-bind\",\n "
},
{
"path": ".codeclimate.yml",
"chars": 57,
"preview": "languages:\n JavaScript: true\nexclude_paths:\n - 'lib/*'\n"
},
{
"path": ".editorconfig",
"chars": 188,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_"
},
{
"path": ".eslintrc",
"chars": 463,
"preview": "{\n \"extends\": \"timkurvers/react\",\n \"rules\": {\n // Allow vertically aligning values\n \"no-multi-spaces\": [0],\n\n "
},
{
"path": ".gitignore",
"chars": 91,
"preview": "/coverage/\n/lib/\n/node_modules/\n/public/scripts/\n/public/styles/\n/public/templates/\n/spec/\n"
},
{
"path": ".istanbul.yml",
"chars": 115,
"preview": "instrumentation:\n excludes: ['public/**', 'src/**', 'bundle.js', 'gulpfile.babel.js']\n include-all-sources: true\n"
},
{
"path": ".travis.yml",
"chars": 508,
"preview": "sudo: false\nlanguage: node_js\nnode_js:\n - '4'\n - '5'\n - '6'\nmatrix:\n fast_finish: true\naddons:\n apt:\n sources:\n "
},
{
"path": "AUTHORS",
"chars": 84,
"preview": "fallenoak (https://github.com/fallenoak)\ntimkurvers (https://github.com/timkurvers)\n"
},
{
"path": "CHANGELOG.md",
"chars": 208,
"preview": "# Changelog\n\n### v0.0.2 - March 20, 2020\n\n- Ensure package on `npm` contains current project status.\n\n### v0.0.1 - Novem"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "MIT License\n\nCopyright (c) 2012-2018 Wowser Contributors\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 5102,
"preview": "# Wowser\n\n[](https://www.npmjs.org/package/wowser)\n[![Join"
},
{
"path": "bin/serve",
"chars": 222,
"preview": "#!/usr/bin/env node\n\nconst Cluster = require('../lib/server/cluster');\nconst ServerConfig = require('../lib/server/confi"
},
{
"path": "gulpfile.babel.js",
"chars": 1123,
"preview": "import Config from 'configstore';\nimport babel from 'gulp-babel';\nimport cache from 'gulp-cached';\nimport del from 'del'"
},
{
"path": "package.json",
"chars": 2970,
"preview": "{\n \"name\": \"wowser\",\n \"version\": \"0.0.2\",\n \"description\": \"World of Warcraft in the browser using JavaScript and WebG"
},
{
"path": "src/bootstrapper.jsx",
"chars": 165,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport Wowser from './components/wowser';\n\nReactDOM.render"
},
{
"path": "src/components/auth/index.jsx",
"chars": 2735,
"preview": "import React from 'react';\n\nimport session from '../wowser/session';\n\nclass AuthScreen extends React.Component {\n\n stat"
},
{
"path": "src/components/auth/index.styl",
"chars": 44,
"preview": "wowser .auth\n\n .panel\n max-width: 300px\n"
},
{
"path": "src/components/characters/index.jsx",
"chars": 2329,
"preview": "import React from 'react';\n\nimport session from '../wowser/session';\n\nclass CharactersScreen extends React.Component {\n\n"
},
{
"path": "src/components/game/chat/index.jsx",
"chars": 1614,
"preview": "import React from 'react';\nimport classes from 'classnames';\n\nimport './index.styl';\n\nimport session from '../../wowser/"
},
{
"path": "src/components/game/chat/index.styl",
"chars": 464,
"preview": "wowser .chat\n position: absolute\n bottom: 0\n left: 0\n width: 400px\n\n ul\n height: 182px\n padding: 0\n margin"
},
{
"path": "src/components/game/controls.jsx",
"chars": 5639,
"preview": "import React from 'react';\nimport THREE from 'three';\nimport key from 'keymaster';\n\nclass Controls extends React.Compone"
},
{
"path": "src/components/game/hud/index.jsx",
"chars": 499,
"preview": "import React from 'react';\n\nimport './index.styl';\n\n// TODO: import Chat from '../chat';\nimport Portrait from '../portra"
},
{
"path": "src/components/game/hud/index.styl",
"chars": 95,
"preview": "wowser .game .hud\n z-index: 2\n display: flex\n align-items: center\n justify-content: center\n"
},
{
"path": "src/components/game/index.jsx",
"chars": 2598,
"preview": "import React from 'react';\nimport THREE from 'three';\n\nimport './index.styl';\n\nimport Controls from './controls';\nimport"
},
{
"path": "src/components/game/index.styl",
"chars": 117,
"preview": "wowser .game\n\n canvas\n position: absolute\n top: 0\n left: 0\n z-index: 1\n width: 100%\n height: 100%\n"
},
{
"path": "src/components/game/portrait/index.jsx",
"chars": 872,
"preview": "import React from 'react';\nimport classes from 'classnames';\n\nimport './index.styl';\n\nclass Portrait extends React.Compo"
},
{
"path": "src/components/game/portrait/index.styl",
"chars": 1265,
"preview": "wowser .portrait\n display: block\n position: relative\n width: 178px\n height: 60px\n background: url('./images/portrai"
},
{
"path": "src/components/game/quests/index.jsx",
"chars": 394,
"preview": "import React from 'react';\n\nimport './index.styl';\n\nclass QuestsPanel extends React.Component {\n\n render() {\n return"
},
{
"path": "src/components/game/quests/index.styl",
"chars": 88,
"preview": "wowser .quests\n position: absolute\n bottom: 0\n right: 0\n height: 30%\n width: 300px\n"
},
{
"path": "src/components/game/stats/index.jsx",
"chars": 2646,
"preview": "import React from 'react';\n\nimport './index.styl';\n\nclass Stats extends React.Component {\n\n static propTypes = {\n re"
},
{
"path": "src/components/game/stats/index.styl",
"chars": 86,
"preview": "wowser .stats\n position: absolute\n bottom: 0\n right: 0\n z-index: 3\n width: 160px\n"
},
{
"path": "src/components/kit/index.jsx",
"chars": 1658,
"preview": "import React from 'react';\n\nclass KitScreen extends React.Component {\n\n static id = 'kit';\n static title = 'UI Kit';\n\n"
},
{
"path": "src/components/realms/index.jsx",
"chars": 2176,
"preview": "import React from 'react';\n\nimport session from '../wowser/session';\n\nclass RealmsScreen extends React.Component {\n\n st"
},
{
"path": "src/components/wowser/index.jsx",
"chars": 1712,
"preview": "import React from 'react';\n\nimport './index.styl';\n\nimport AuthScreen from '../auth';\nimport CharactersScreen from '../c"
},
{
"path": "src/components/wowser/index.styl",
"chars": 802,
"preview": "@import '~normalize.css'\n\n@import './ui';\n\nhtml, body\n width: 100%\n height: 100%\n overflow: hidden\n\n*\n box-sizing: b"
},
{
"path": "src/components/wowser/session.jsx",
"chars": 363,
"preview": "import Client from '../../lib';\n\nclass Session extends Client {\n\n constructor() {\n super();\n\n this._screen = 'aut"
},
{
"path": "src/components/wowser/ui/form/index.styl",
"chars": 538,
"preview": "wowser form\n\n fieldset\n display: block\n border: 0\n padding: 0\n margin: .5em\n border-top: 1px solid trans"
},
{
"path": "src/components/wowser/ui/frame/dividers/index.styl",
"chars": 274,
"preview": "wowser .divider\n border-style: solid\n\n &, &.horizontal\n border-width: 3px 5px 0 5px\n border-image: url('./images"
},
{
"path": "src/components/wowser/ui/frame/index.styl",
"chars": 1233,
"preview": "@import './dividers';\n\nwowser\n\n .frame, .panel\n display: block\n position: relative\n margin: 10px\n\n &:before"
},
{
"path": "src/components/wowser/ui/index.styl",
"chars": 96,
"preview": "@import './form';\n@import './frame';\n@import './screen';\n@import './type';\n@import './widgets';\n"
},
{
"path": "src/components/wowser/ui/screen.styl",
"chars": 161,
"preview": "wowser .screen\n position: absolute\n top: 0\n left: 0\n z-index: 3\n width: 100%\n height: 100%\n display: flex\n align"
},
{
"path": "src/components/wowser/ui/type.styl",
"chars": 159,
"preview": "wowser\n\n h1, h2, h3, h4\n margin: .3em\n color: #FFCC00\n font-weight: normal\n\n h1\n font-size: 17px\n\n h2\n "
},
{
"path": "src/components/wowser/ui/widgets/button.styl",
"chars": 942,
"preview": "wowser\n\n input[type='submit'], input[type='button'], button\n margin: .4em 0 .3em .5em\n padding: .2em 1em\n bord"
},
{
"path": "src/components/wowser/ui/widgets/index.styl",
"chars": 20,
"preview": "@import './button';\n"
},
{
"path": "src/index.html",
"chars": 394,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <title>Wowser</title>\n <link rel=\"shortcut icon\" hre"
},
{
"path": "src/lib/auth/challenge-opcode.js",
"chars": 745,
"preview": "class ChallengeOpcode {\n\n static SUCCESS = 0x00;\n static UNKNOWN0 = 0x01;\n static UNKNOWN1 "
},
{
"path": "src/lib/auth/handler.js",
"chars": 4749,
"preview": "import AuthChallengeOpcode from './challenge-opcode';\nimport AuthOpcode from './opcode';\nimport AuthPacket from './packe"
},
{
"path": "src/lib/auth/opcode.js",
"chars": 228,
"preview": "class Opcode {\n\n static LOGON_CHALLENGE = 0x00;\n static LOGON_PROOF = 0x01;\n static RECONNECT_CHALLENGE ="
},
{
"path": "src/lib/auth/packet.js",
"chars": 655,
"preview": "import AuthOpcode from './opcode';\nimport BasePacket from '../net/packet';\nimport ObjectUtil from '../utils/object-util'"
},
{
"path": "src/lib/characters/character.js",
"chars": 165,
"preview": "class Character {\n\n // Short string representation of this character\n toString() {\n return `[Character; GUID: ${thi"
},
{
"path": "src/lib/characters/handler.js",
"chars": 2294,
"preview": "import EventEmitter from 'events';\n\nimport Character from './character';\nimport GamePacket from '../game/packet';\nimport"
},
{
"path": "src/lib/config.js",
"chars": 830,
"preview": "class Raw {\n constructor(config) {\n this.config = config;\n }\n\n raw(value) {\n return ('\\u0000\\u0000\\u0000\\u0000'"
},
{
"path": "src/lib/crypto/big-num.js",
"chars": 2551,
"preview": "import BigInteger from 'jsbn/lib/big-integer';\n\n// C-like BigNum decorator for JSBN's BigInteger\nclass BigNum {\n\n // Co"
},
{
"path": "src/lib/crypto/crypt.js",
"chars": 1422,
"preview": "import { HMAC } from 'jsbn/lib/sha1';\nimport RC4 from 'jsbn/lib/rc4';\n\nimport ArrayUtil from '../utils/array-util';\n\ncla"
},
{
"path": "src/lib/crypto/hash/sha1.js",
"chars": 255,
"preview": "import SHA1Base from 'jsbn/lib/sha1';\n\nimport Hash from '../hash';\n\n// SHA-1 implementation\nclass SHA1 extends Hash {\n\n "
},
{
"path": "src/lib/crypto/hash.js",
"chars": 979,
"preview": "import ByteBuffer from 'byte-buffer';\n\n// Feedable hash implementation\nclass Hash {\n\n // Creates a new hash\n construct"
},
{
"path": "src/lib/crypto/srp.js",
"chars": 4514,
"preview": "import equal from 'deep-equal';\n\nimport BigNum from './big-num';\nimport SHA1 from './hash/sha1';\n\n// Secure Remote Passw"
},
{
"path": "src/lib/game/chat/handler.js",
"chars": 1713,
"preview": "import EventEmitter from 'events';\n\nimport Message from './message';\n\nclass ChatHandler extends EventEmitter {\n\n // Cre"
},
{
"path": "src/lib/game/chat/message.js",
"chars": 322,
"preview": "class ChatMessage {\n\n // Creates a new message\n constructor(kind, text) {\n this.kind = kind;\n this.text = text;\n"
},
{
"path": "src/lib/game/entity.js",
"chars": 180,
"preview": "import EventEmitter from 'events';\n\nclass Entity extends EventEmitter {\n\n constructor() {\n super();\n this.guid = "
},
{
"path": "src/lib/game/guid.js",
"chars": 567,
"preview": "class GUID {\n\n // GUID byte-length (64-bit)\n static LENGTH = 8;\n\n // Creates a new GUID\n constructor(buffer) {\n\n "
},
{
"path": "src/lib/game/handler.js",
"chars": 5027,
"preview": "import ByteBuffer from 'byte-buffer';\n\nimport BigNum from '../crypto/big-num';\nimport Crypt from '../crypto/crypt';\nimpo"
},
{
"path": "src/lib/game/opcode.js",
"chars": 2516,
"preview": "class GameOpcode {\n\n static CMSG_CHAR_ENUM = 0x0037;\n\n static SMSG_CHAR_ENUM ="
},
{
"path": "src/lib/game/packet.js",
"chars": 1529,
"preview": "import BasePacket from '../net/packet';\nimport GameOpcode from './opcode';\nimport GUID from './guid';\nimport ObjectUtil "
},
{
"path": "src/lib/game/player.js",
"chars": 494,
"preview": "import Unit from './unit';\n\nclass Player extends Unit {\n\n constructor() {\n super();\n\n this.name = 'Player';\n t"
},
{
"path": "src/lib/game/unit.js",
"chars": 2933,
"preview": "import THREE from 'three';\n\nimport DBC from '../pipeline/dbc';\nimport Entity from './entity';\nimport M2Blueprint from '."
},
{
"path": "src/lib/game/world/content-queue.js",
"chars": 1100,
"preview": "class ContentQueue {\n\n constructor(processor, interval = 1, workFactor = 1, minWork = 1) {\n this.processor = process"
},
{
"path": "src/lib/game/world/doodad-manager.js",
"chars": 6598,
"preview": "import M2Blueprint from '../../pipeline/m2/blueprint';\n\nclass DoodadManager {\n\n // Proportion of pending doodads to loa"
},
{
"path": "src/lib/game/world/handler.js",
"chars": 4003,
"preview": "import EventEmitter from 'events';\nimport THREE from 'three';\n\nimport M2Blueprint from '../../pipeline/m2/blueprint';\nim"
},
{
"path": "src/lib/game/world/map.js",
"chars": 3343,
"preview": "import THREE from 'three';\n\nimport ADT from '../../pipeline/adt';\nimport Chunk from '../../pipeline/adt/chunk';\nimport D"
},
{
"path": "src/lib/game/world/terrain-manager.js",
"chars": 286,
"preview": "class TerrainManager {\n\n constructor(map) {\n this.map = map;\n }\n\n loadChunk(_index, terrain) {\n this.map.add(te"
},
{
"path": "src/lib/game/world/wmo-manager/index.js",
"chars": 3684,
"preview": "import ContentQueue from '../content-queue';\nimport WMOHandler from './wmo-handler';\nimport WMOBlueprint from '../../../"
},
{
"path": "src/lib/game/world/wmo-manager/wmo-handler.js",
"chars": 10135,
"preview": "import ContentQueue from '../content-queue';\nimport WMOBlueprint from '../../../pipeline/wmo/blueprint';\nimport WMOGroup"
},
{
"path": "src/lib/index.js",
"chars": 816,
"preview": "import EventEmitter from 'events';\n\nimport AuthHandler from './auth/handler';\nimport CharactersHandler from './character"
},
{
"path": "src/lib/net/loader.js",
"chars": 651,
"preview": "import Promise from 'bluebird';\n\nclass Loader {\n\n constructor() {\n this.prefix = this.prefix || '/pipeline/';\n th"
},
{
"path": "src/lib/net/packet.js",
"chars": 1193,
"preview": "import ByteBuffer from 'byte-buffer';\n\nclass Packet extends ByteBuffer {\n\n // Creates a new packet with given opcode fr"
},
{
"path": "src/lib/net/socket.js",
"chars": 2665,
"preview": "import ByteBuffer from 'byte-buffer';\nimport EventEmitter from 'events';\n\n// Base-class for any socket including signals"
},
{
"path": "src/lib/pipeline/adt/chunk/index.js",
"chars": 3682,
"preview": "import THREE from 'three';\n\nimport ADT from '../';\nimport Material from './material';\n\nclass Chunk extends THREE.Mesh {\n"
},
{
"path": "src/lib/pipeline/adt/chunk/material.js",
"chars": 2430,
"preview": "import THREE from 'three';\n\nimport TextureLoader from '../../texture-loader';\nimport fragmentShader from './shader.frag'"
},
{
"path": "src/lib/pipeline/adt/chunk/shader.frag",
"chars": 2902,
"preview": "uniform int layerCount;\nuniform sampler2D alphaMaps[4];\nuniform sampler2D textures[4];\n\nvarying vec2 vUv;\nvarying vec2 v"
},
{
"path": "src/lib/pipeline/adt/chunk/shader.vert",
"chars": 646,
"preview": "precision highp float;\n\nattribute vec2 uvAlpha;\n\nvarying vec2 vUv;\nvarying vec2 vUvAlpha;\n\nvarying vec3 vertexNormal;\nva"
},
{
"path": "src/lib/pipeline/adt/index.js",
"chars": 1322,
"preview": "import WorkerPool from '../worker/pool';\n\nclass ADT {\n\n static SIZE = 533.33333;\n\n static cache = {};\n\n constructor(p"
},
{
"path": "src/lib/pipeline/adt/loader.js",
"chars": 422,
"preview": "import ADT from 'blizzardry/lib/adt';\nimport { DecodeStream } from 'blizzardry/lib/restructure';\n\nimport Loader from '.."
},
{
"path": "src/lib/pipeline/dbc/index.js",
"chars": 737,
"preview": "import WorkerPool from '../worker/pool';\n\nclass DBC {\n\n static cache = {};\n\n constructor(data) {\n this.data = data;"
},
{
"path": "src/lib/pipeline/dbc/loader.js",
"chars": 599,
"preview": "import * as DBC from 'blizzardry/lib/dbc/entities';\nimport { DecodeStream } from 'blizzardry/lib/restructure';\n\nimport L"
},
{
"path": "src/lib/pipeline/m2/animation-manager.js",
"chars": 6401,
"preview": "import EventEmitter from 'events';\nimport THREE from 'three';\n\nclass AnimationManager extends EventEmitter {\n\n construc"
},
{
"path": "src/lib/pipeline/m2/batch-manager.js",
"chars": 4116,
"preview": "class BatchManager {\n\n constructor() {\n }\n\n createDefs(data, skinData) {\n const defs = [];\n\n skinData.batches.f"
},
{
"path": "src/lib/pipeline/m2/blueprint.js",
"chars": 2402,
"preview": "import WorkerPool from '../worker/pool';\nimport M2 from './';\n\nclass M2Blueprint {\n\n static cache = new Map();\n static"
},
{
"path": "src/lib/pipeline/m2/index.js",
"chars": 16589,
"preview": "import THREE from 'three';\n\nimport Submesh from './submesh';\nimport M2Material from './material';\nimport AnimationManage"
},
{
"path": "src/lib/pipeline/m2/loader.js",
"chars": 795,
"preview": "import { DecodeStream } from 'blizzardry/lib/restructure';\nimport M2 from 'blizzardry/lib/m2';\nimport Skin from 'blizzar"
},
{
"path": "src/lib/pipeline/m2/material/index.js",
"chars": 9678,
"preview": "import THREE from 'three';\n\nimport TextureLoader from '../../texture-loader';\nimport vertexShader from './shader.vert';\n"
},
{
"path": "src/lib/pipeline/m2/material/shader.frag",
"chars": 3535,
"preview": "uniform int fragmentShaderMode;\n\nuniform int textureCount;\nuniform sampler2D textures[4];\n\nvarying vec2 uv1;\nvarying vec"
},
{
"path": "src/lib/pipeline/m2/material/shader.vert",
"chars": 2973,
"preview": "precision highp float;\n\nvarying vec2 uv1;\nvarying vec2 uv2;\n\nvarying float cameraDistance;\n\nvarying vec3 vertexWorldNorm"
},
{
"path": "src/lib/pipeline/m2/submesh.js",
"chars": 2966,
"preview": "import THREE from 'three';\n\nclass Submesh extends THREE.Group {\n\n constructor(opts) {\n super();\n\n this.matrixAuto"
},
{
"path": "src/lib/pipeline/material.js",
"chars": 536,
"preview": "import THREE from 'three';\n\nconst loader = new THREE.TextureLoader();\n\nclass Material extends THREE.MeshBasicMaterial {\n"
},
{
"path": "src/lib/pipeline/texture-loader.js",
"chars": 2249,
"preview": "import THREE from 'three';\n\nconst loader = new THREE.TextureLoader();\n\nclass TextureLoader {\n\n static cache = new Map()"
},
{
"path": "src/lib/pipeline/wdt/index.js",
"chars": 389,
"preview": "import WorkerPool from '../worker/pool';\n\nclass WDT {\n\n static cache = {};\n\n constructor(data) {\n this.data = data;"
},
{
"path": "src/lib/pipeline/wdt/loader.js",
"chars": 402,
"preview": "import { DecodeStream } from 'blizzardry/lib/restructure';\nimport WDT from 'blizzardry/lib/wdt';\n\nimport Loader from '.."
},
{
"path": "src/lib/pipeline/wmo/blueprint.js",
"chars": 1584,
"preview": "import WorkerPool from '../worker/pool';\nimport WMO from './';\n\nclass WMOBlueprint {\n\n static cache = new Map();\n\n sta"
},
{
"path": "src/lib/pipeline/wmo/group/blueprint.js",
"chars": 1997,
"preview": "import WorkerPool from '../../worker/pool';\nimport WMOGroup from './';\n\nclass WMOGroupBlueprint {\n\n static cache = new "
},
{
"path": "src/lib/pipeline/wmo/group/index.js",
"chars": 4817,
"preview": "import THREE from 'three';\n\nimport WMOMaterial from '../material';\n\nclass WMOGroup extends THREE.Mesh {\n\n static cache "
},
{
"path": "src/lib/pipeline/wmo/group/loader.js",
"chars": 421,
"preview": "import { DecodeStream } from 'blizzardry/lib/restructure';\nimport WMOGroup from 'blizzardry/lib/wmo/group';\n\nimport Load"
},
{
"path": "src/lib/pipeline/wmo/index.js",
"chars": 1020,
"preview": "import THREE from 'three';\n\nclass WMO extends THREE.Group {\n\n static cache = {};\n\n constructor(path, data) {\n super"
},
{
"path": "src/lib/pipeline/wmo/loader.js",
"chars": 402,
"preview": "import { DecodeStream } from 'blizzardry/lib/restructure';\nimport WMO from 'blizzardry/lib/wmo';\n\nimport Loader from '.."
},
{
"path": "src/lib/pipeline/wmo/material/index.js",
"chars": 3163,
"preview": "import THREE from 'three';\n\nimport TextureLoader from '../../texture-loader';\nimport vertexShader from './shader.vert';\n"
},
{
"path": "src/lib/pipeline/wmo/material/shader.frag",
"chars": 2766,
"preview": "varying vec2 vUv;\n\nvarying vec4 vertexColor;\nvarying vec3 vertexWorldNormal;\nvarying float cameraDistance;\n\nuniform int "
},
{
"path": "src/lib/pipeline/wmo/material/shader.vert",
"chars": 1101,
"preview": "precision highp float;\n\nvarying vec2 vUv;\n\nvarying vec3 vertexWorldNormal;\nvarying float cameraDistance;\n\nattribute vec3"
},
{
"path": "src/lib/pipeline/worker/index.js",
"chars": 839,
"preview": "import ADT from '../adt/loader';\nimport DBC from '../dbc/loader';\nimport M2 from '../m2/loader';\nimport WDT from '../wdt"
},
{
"path": "src/lib/pipeline/worker/pool.js",
"chars": 1005,
"preview": "import Task from './task';\nimport Thread from './thread';\n\nclass WorkerPool {\n\n constructor(concurrency = this.defaultC"
},
{
"path": "src/lib/pipeline/worker/task.js",
"chars": 243,
"preview": "import Promise from 'bluebird';\n\nclass Task {\n\n constructor(...args) {\n this.args = args;\n this.promise = new Pro"
},
{
"path": "src/lib/pipeline/worker/thread.js",
"chars": 627,
"preview": "import Worker from 'worker!./';\n\nclass Thread {\n\n constructor() {\n this._onMessage = ::this._onMessage;\n\n this.wo"
},
{
"path": "src/lib/realms/handler.js",
"chars": 1879,
"preview": "import EventEmitter from 'events';\n\nimport AuthOpcode from '../auth/opcode';\nimport AuthPacket from '../auth/packet';\nim"
},
{
"path": "src/lib/realms/realm.js",
"chars": 1122,
"preview": "class Realm {\n\n // Creates a new realm\n constructor() {\n\n // Holds host, port and address\n this._host = null;\n "
},
{
"path": "src/lib/server/.babelrc",
"chars": 262,
"preview": "{\n \"plugins\": [\n \"transform-class-properties\",\n \"transform-export-extensions\",\n \"transform-function-bind\",\n "
},
{
"path": "src/lib/server/cluster.js",
"chars": 956,
"preview": "import cluster from 'cluster';\n\nimport Server from './';\nimport ServerConfig from './config';\n\nclass Cluster {\n\n get cl"
},
{
"path": "src/lib/server/config/index.js",
"chars": 1145,
"preview": "import Configstore from 'configstore';\nimport Promise from 'bluebird';\nimport inquirer from 'inquirer';\n\nimport pkg from"
},
{
"path": "src/lib/server/config/setup-prompts.js",
"chars": 922,
"preview": "import fs from 'fs';\nimport os from 'os';\n\nexport default [\n {\n type: 'input',\n name: 'clientData',\n message: "
},
{
"path": "src/lib/server/index.js",
"chars": 477,
"preview": "import express from 'express';\nimport logger from 'morgan';\n\nimport Pipeline from './pipeline';\n\nclass Server {\n\n const"
},
{
"path": "src/lib/server/pipeline/archive.js",
"chars": 767,
"preview": "import MPQ from 'blizzardry/lib/mpq';\nimport glob from 'globby';\n\nclass Archive {\n\n static CHAIN = [\n 'common.MPQ',\n"
},
{
"path": "src/lib/server/pipeline/index.js",
"chars": 2734,
"preview": "import BLP from 'blizzardry/lib/blp';\nimport * as DBC from 'blizzardry/lib/dbc/entities';\nimport { DecodeStream } from '"
},
{
"path": "src/lib/utils/array-util.js",
"chars": 265,
"preview": "class ArrayUtil {\n\n // Generates array from given hex string\n static fromHex(hex) {\n const array = [];\n for (let"
},
{
"path": "src/lib/utils/object-util.js",
"chars": 438,
"preview": "class ObjectUtil {\n\n // Retrieves key for given value (if any) in object\n static keyByValue(object, target) {\n if ("
},
{
"path": "src/spec/.eslintrc",
"chars": 37,
"preview": "{\n \"env\": {\n \"mocha\": true\n }\n}\n"
},
{
"path": "src/spec/sample-spec.js",
"chars": 109,
"preview": "import {} from './spec-helper';\n\ndescribe('Wowser', function() {\n\n xit('will have specs (hopefully)');\n\n});\n"
},
{
"path": "src/spec/spec-helper.js",
"chars": 291,
"preview": "import bridge from 'sinon-chai';\nimport chai from 'chai';\nimport sinon from 'sinon';\n\nchai.use(bridge);\n\nbeforeEach(func"
},
{
"path": "webpack.config.js",
"chars": 1430,
"preview": "const HtmlWebpackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = {\n context: p"
}
]
About this extraction
This page contains the full source code of the wowserhq/wowser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 126 files (207.4 KB), approximately 58.9k tokens, and a symbol index with 475 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.