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