Full Code of wowserhq/wowser for AI

master 5fcd3e607db7 cached
126 files
207.4 KB
58.9k tokens
475 symbols
1 requests
Download .txt
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

[![Version](https://img.shields.io/npm/v/wowser.svg?style=flat)](https://www.npmjs.org/package/wowser)
[![Join Community](https://img.shields.io/badge/discord-join_community-blue.svg?style=flat)](https://discord.gg/DeVVKVg)
[![Build Status](https://img.shields.io/travis/wowserhq/wowser.svg?style=flat)](https://travis-ci.org/wowserhq/wowser)
[![Known Vulnerabilities](https://snyk.io/test/github/wowserhq/wowser/badge.svg)](https://snyk.io/test/github/wowserhq/wowser)
[![Maintainability](https://api.codeclimate.com/v1/badges/863393c7addcb1cd7be7/maintainability)](https://codeclimate.com/github/wowserhq/wowser/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/863393c7addcb1cd7be7/test_coverage)](https://codeclimate.com/github/wowserhq/wowser/test_coverage)

World of Warcraft in the browser using JavaScript and WebGL.

Licensed under the [**MIT** license](LICENSE).

[![See Wowser tech demo](https://user-images.githubusercontent.com/378235/27762818-800fd91c-5e79-11e7-8301-733d736dd065.jpg)](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
Download .txt
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
Download .txt
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[![Version](https://img.shields.io/npm/v/wowser.svg?style=flat)](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.

Copied to clipboard!