[
  {
    "path": ".dockerignore",
    "content": "node_modules\nnpm-debug.log\n.gitignore\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/content-feature-plugin.md",
    "content": "---\nname: Content Feature/Plugin\nabout: New game content features or plugin requests\ntitle: \"[CONTENT]\"\nlabels: enhancement, needs workshopping\nassignees: ''\n\n---\n\n### Content/Plugin Description\n> Example: As a player, I expect to receive a bucket of milk when I click to milk a cow while having a bucket in my inventory.\n\n###  Acceptance Criteria\n> Any criteria required to consider this new plugin or feature completed.\n- [ ] required item 1\n- [ ] required item 2\n- [ ] required item 3\n\n## Additional Information\n> Other additional details relevant to the new feature that were not described above.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/game-engine-feature.md",
    "content": "---\nname: Game Engine Feature\nabout: New game engine or content API features\ntitle: \"[ENGINE] \"\nlabels: needs workshopping, new feature\nassignees: ''\n\n---\n\n### Game Engine Feature Description\n> Example: As a plugin developer, I expect to be able to be able to hook into the player init event.\n\n###  Acceptance Criteria\n> Any criteria required to consider this new feature completed.\n- [ ] required item 1\n- [ ] required item 2\n- [ ] required item 3\n\n## Additional Information\n> Other additional details relevant to the new feature that were not described above.\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Check & Build Project\n\non:\n    pull_request:\n        branches: [ master, develop, feature/** ]\n    push:\n        branches: [ master, develop ]\n\njobs:\n    build:\n        runs-on: ubuntu-latest\n\n        steps:\n            -   name: Checkout Repository\n                uses: actions/checkout@v2\n\n            -   name: Setup Node\n                uses: actions/setup-node@v1\n                with:\n                    node-version: '24.x'\n\n            -   name: Install Node Modules\n                run: npm i\n\n            -   name: Run Linter\n                run: npm run lint:fin\n\n            -   name: Run Formatter\n                run: npm run format:fin\n\n            -   name: Run Tests\n                run: npm run test:fin\n\n            -   name: Run Typecheck\n                run: npm run typecheck\n\n            -   name: Build Project\n                run: npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n/dist\n/data/dump\n/data/houses\n/data/saves/*.json\n/config/server-config.json\nserver-config.yaml\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories.ts and files\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n/.vs\n\n/coverage/"
  },
  {
    "path": ".swcrc",
    "content": "{\n    \"$schema\": \"https://swc.rs/schema.json\",\n    \"exclude\": \"node_modules/\",\n    \"sourceMaps\": true,\n    \"module\": {\n        \"type\": \"commonjs\"\n    },\n    \"jsc\": {\n        \"target\": \"esnext\",\n        \"baseUrl\": \"./src\",\n        \"paths\": {\n            \"@engine/*\": [\"./engine/*\"],\n            \"@plugins/*\": [\"./plugins/*\"],\n            \"@server/*\": [\"./server/*\"]\n        },\n        \"parser\": {\n            \"syntax\": \"typescript\",\n            \"tsx\": false,\n            \"decorators\": true,\n            \"dynamicImport\": false\n        }\n    }\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.tabSize\": 4,\n    \"files.trimTrailingWhitespace\": true,\n    \"files.trimFinalNewlines\": true,\n    \"files.insertFinalNewline\": true\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to RuneJS\n\nRuneJS was created with the intention of utilizing JavaScript/TypeScript and Node's innovative features. RxJS was imported for reactive programming as well, opening up opportunities for easy content development. As such, there are a few things we're looking to avoid...\n\n1. Direct ports/copying from Java servers\n    - This defeats the purpose of RuneJS by implementing basic flows that any regular Java-based server would use. Think outside the box and really utilize ES6, TypeScript, Node, and RxJS! :)\n2. Additional/outside dependencies\n    - Sometimes additional dependencies cannot be avoided, but we'd like to avoid them as much as possible. RuneJS intends to be simple and easy for anyone to pick up, without requiring the user to set up any databases or additional third party systems.\n    - In some cases this is of course unavoidable, as such we'll handle them on a case-by-case basis.\n\nUltimately if you're looking to contribute, it's best to check in with us on Discord to see if we're already working on a specific feature or have plans for it already. Add us at **Tyn#0001**\n\n## Code Style\n\nCode style (linting and formatting) are handled by [Biome](https://biomejs.dev/). It is recommended that you install [the Biome extension for your IDE](https://biomejs.dev/guides/editors/first-party-extensions/).\n\nRunning `npm run fin` will perform all necessary checks (linting, formatting, typechecking and tests).\n\n## Testing\n\nUnit tests can be written using Jest. To execute the test suite, run `npm test`\n\n- Test files should be located next to the file under test, and called `file-name.test.ts`\n- Tests should use the `when / then` pattern made up of composable `describe` statements\n- Make use of `beforeEach` to set up state before each test\n\nAfter running the tests, you can find code coverage in the `./coverage/` folder.\n\n### When / Then testing pattern\n\nTests should be broken down into a series of `describe` statements, which set up their own internal state when possible.\n\n```ts\ndescribe('when there is a player', () => {\n  let player: Player\n\n  beforeEach(() => {\n    player = createMockPlayer()\n  })\n\n  describe('when player is wearing a hat', () => {\n    beforeEach(() => {\n      player.equipment().set(0, someHatItem)\n    })\n\n    test('should return true', () => {\n      const result = isWearingHat(player)\n\n      expect(result).toEqual(true)\n    })\n  })\n})\n```\n\nThere are two main benefits to this kind of design:\n\n- It serves as living documentation: from reading the `beforeEach` block you can clearly see how the prerequisite of \"player is wearing a hat\" is achieved\n- It allows for easy expansion of test cases: future developers can add further\n  statements inside \"when player is wearing a hat\" if they want to make use of\n  that setup\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:24\nWORKDIR /usr/src/app\nCOPY package.json ./\nCOPY package-lock.json ./\n\nRUN apt update\nRUN apt install -y libsdl-pango-dev\n\nRUN npm ci\n\nCOPY src ./src\nCOPY tsconfig.json ./\nCOPY .swcrc ./\n\nRUN npm run build\n\nEXPOSE 43594\nCMD [ \"npm\", \"run\", \"start:standalone\" ]\n"
  },
  {
    "path": "FEATURES.md",
    "content": "[![RuneJS Discord Server](https://img.shields.io/discord/678751302297059336?label=RuneJS%20Discord&logo=discord)](https://discord.gg/5P74nSh)\n\n\n![RuneJS](https://i.imgur.com/pmkdSfc.png)\n\n# RuneJS Feature List\n\n## Game Server\n\n* RSA + ISAAC ciphering :heavy_check_mark:\n* Game Update Server :heavy_check_mark:\n* Authentication Server :heavy_check_mark:\n* Server side cache loading :heavy_check_mark:\n    * Client pathing validation via cache mapdata :heavy_check_mark:\n    * Item/object/npc definitions :heavy_check_mark:\n* Packet queueing  :heavy_check_mark:\n\n### Technical Features\n\n* Asynchronous server infrastructure w/ Promises & RxJS Observables\n* A diverse TypeScript plugin system for easily writing new content based off of in-game actions\n* A simplified JavaScript plugin system for quickly and easily bootstrapping game content\n* Flexible quest and dialogue systems for more advanced content development\n* Code compilation via Babel, offering more seamless compilation and redeployment of plugins\n\n## Game World\n\n* Private & group Player Instances :heavy_check_mark:\n* Personal player instance objects and world items :heavy_check_mark:\n* Bank :heavy_check_mark:\n    * Withdraw/Deposit 1,5,10,All :heavy_check_mark:\n    * As note  :heavy_check_mark:\n    * Swap slot :heavy_check_mark:\n    * Insert mode: :heavy_check_mark:\n    * Deposit box :heavy_check_mark:\n* Audio :yellow_square:\n    * Music :yellow_square:\n        * Playing music :heavy_check_mark:\n        * Music Regions :x:\n        * Music Player tab :x:\n    * Sounds :yellow_square:\n        * Playing sounds :heavy_check_mark:\n        * Sound effects for actions :yellow_square:\n* Home Teleport :heavy_check_mark:\n* Emotes :heavy_check_mark:\n    * Skillcape emotes :heavy_check_mark:\n    * Unlockable emotes w/ requirements :heavy_check_mark:\n* Shop support :heavy_check_mark:\n* Inventory support :heavy_check_mark:\n    * Swapping items :heavy_check_mark:\n    * Dropping items :heavy_check_mark:\n    * Picking up ground items :heavy_check_mark:\n    * Equipping items :heavy_check_mark:\n* Doors/gates :yellow_square:\n    * NSEW doors :heavy_check_mark:\n    * Diagonal doors :yellow_square:\n    * Double doors :heavy_check_mark:\n    * Wooden gates :heavy_check_mark:\n* Climbing ladders & stairs :yellow_square:\n* Clue Scrolls :x:\n\n### Quests\n* Cook's Assistant :heavy_check_mark:\n\n### Skills\n\n* Combat :yellow_square:\n    * Melee :yellow_square:\n    * Ranged :x:\n    * Magic :x:\n* Prayer :x:\n* Cooking :x:\n* Fletching :x:\n* Fishing :x:\n* Firemaking :yellow_square:\n    * Fire lighting :yellow_square:\n    * Chain fires w/ movement :yellow_square:\n* Herblore :x:\n* Agility :x:\n* Thieving :x:\n* Slayer :x:\n* Farming :x:\n* Runecrafting :x:\n* Construction :x:\n* Woodcutting :yellow_square:\n    * Formula for success :heavy_check_mark:\n    * Chopping Trees :heavy_check_mark:\n    * Axes :heavy_check_mark:\n    * Birds nests  :heavy_check_mark:\n    * Stump ids :yellow_square:\n    * Canoes :x:\n* Mining :yellow_square:\n    * Formula for success :heavy_check_mark:\n    * Mining ores :heavy_check_mark:\n    * Pickaxes :heavy_check_mark:\n    * Random gems  :heavy_check_mark:\n    * Gem ores :heavy_check_mark:\n    * Essence mining :heavy_check_mark:\n    * Empty Rock ids :yellow_square:\n* Crafting :yellow_square:\n    * Spinning wheel :heavy_check_mark:\n* Smithing :yellow_square:\n    * Smelting ore to bars :heavy_check_mark:\n    * Forging :yellow_square:\n        * Correct items :heavy_check_mark:\n        * Hiding non-applicable items :yellow_square:\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "[![RuneJS Discord Server](https://img.shields.io/discord/678751302297059336?label=RuneJS%20Discord&logo=discord)](https://discord.gg/5P74nSh)\n\n[![RuneJS](https://i.imgur.com/QSXNzwC.png)](https://github.com/runejs/)\n\n# RuneJS Game Server\n\nRuneJS is a RuneScape game server written in TypeScript and JavaScript. The aim of this project is to create a game server that is both fun and easy to use, while also providing simple content development systems.\n\nThe game server currently runs a build of RuneScape from October 30th-31st, 2006 (game build #435). No other builds are supported at this time, but may become available in the future.\n\n**RuneJS is completely open-source and open to all pull requests and/or issues. Many plugins have been added by contributor pull requests and we're always happy to have more!**\n\n![RuneJS Lumbridge](https://i.imgur.com/KVCqKSb.png)\n\n## Setup\n\n### Prerequisites\n\n- [`docker`](https://docs.docker.com/get-docker/) and [`docker-compose`](https://docs.docker.com/compose/install/)\n  - If on Windows, `docker-compose` comes with `docker`\n\n### Running the Game Server\n\n1. Copy the `config/server-config.example.json` and paste it into the same folder using the name `server-config.json`\n2. Go into your new `server-config.json` file and modify your RSA modulus and exponent with the ones matching your game client\n    - You may also modify the server's port and host address from this configuration file\n3. Build the docker image with `docker-compose build`\n4. Run the game server with `docker-compose up`\n\nThe game server will spin up and be accessible via port 43594.\n\n## Game Client\n\nThe [RuneScape Java Client #435](https://github.com/runejs/refactored-client-435) must be used to log into a RuneJS game server.\n\n## Additional Commands\n\nBefore running these commands, you must:\n\n1. have [NodeJS **version 24 or higher**](https://nodejs.org/en/) installed on your machine\n2. run `npm install` from the root of this project\n\n* `npm run game` Launches the game server by itself without building\n* `npm run game:dev` Builds and launches the game server by itself in watch mode\n* `npm run login` Launches the login server by itself without building\n* `npm run update` Launches the update server by itself without building\n* `npm run infra` Launches both the login and update server without building\n* `npm run standalone` Launches all three servers concurrently without building\n* `npm run build:watch` Builds the application and watches for changes\n* `npm run build` Builds the application\n* `npm run lint` Runs Biome in linting mode, use `lint:fix` to autofix\n* `npm run format` Runs Biome in formatting mode, use `format:fix` to autofix\n* `npm run test` Runs all tests with Jest\n* `npm run typecheck` Typechecks the project\n* `npm run fin` Combines lint:fix, format:fix, test and typecheck into a single command\n"
  },
  {
    "path": "biome.json",
    "content": "{\n    \"$schema\": \"https://biomejs.dev/schemas/1.9.4/schema.json\",\n    \"vcs\": {\n        \"enabled\": true,\n        \"clientKind\": \"git\",\n        \"useIgnoreFile\": true\n    },\n    \"files\": {\n        \"ignoreUnknown\": false,\n        \"ignore\": []\n    },\n    \"formatter\": {\n        \"enabled\": true,\n        \"indentStyle\": \"space\",\n        \"indentWidth\": 4,\n        \"lineWidth\": 140\n    },\n    \"organizeImports\": {\n        \"enabled\": true\n    },\n    \"linter\": {\n        \"enabled\": true,\n        \"rules\": {\n            \"recommended\": true,\n            \"complexity\": {\n                \"recommended\": true,\n                \"noBannedTypes\": \"off\",\n                \"noForEach\": \"off\",\n                \"noStaticOnlyClass\": \"off\",\n                \"noUselessConstructor\": \"off\",\n                \"noUselessSwitchCase\": \"off\",\n                \"useLiteralKeys\": \"off\",\n                \"useOptionalChain\": \"off\"\n            },\n            \"correctness\": {\n                \"recommended\": true,\n                \"noSwitchDeclarations\": \"off\"\n            },\n            \"performance\": {\n                \"recommended\": true,\n                \"noAccumulatingSpread\": \"off\",\n                \"noDelete\": \"off\"\n            },\n            \"suspicious\": {\n                \"recommended\": true,\n                \"noAssignInExpressions\": \"off\",\n                \"noConfusingVoidType\": \"off\",\n                \"noDoubleEquals\": \"off\",\n                \"noDuplicateObjectKeys\": \"off\",\n                \"noExplicitAny\": \"off\",\n                \"noGlobalIsNan\": \"off\",\n                \"noImplicitAnyLet\": \"off\",\n                \"noSelfCompare\": \"off\",\n                \"noUnsafeDeclarationMerging\": \"off\"\n            },\n            \"style\": {\n                \"recommended\": true,\n                \"noInferrableTypes\": \"off\",\n                \"noNonNullAssertion\": \"off\",\n                \"noParameterAssign\": \"off\",\n                \"noUnusedTemplateLiteral\": \"off\",\n                \"noUselessElse\": \"off\",\n                \"useEnumInitializers\": \"off\",\n                \"useImportType\": \"off\",\n                \"useNodejsImportProtocol\": \"off\",\n                \"useNumberNamespace\": \"off\",\n                \"useSingleVarDeclarator\": \"off\",\n                \"useTemplate\": \"off\"\n            }\n        }\n    },\n    \"javascript\": {\n        \"formatter\": {\n            \"quoteStyle\": \"single\",\n            \"arrowParentheses\": \"asNeeded\"\n        }\n    }\n}\n"
  },
  {
    "path": "cache/file-names.properties",
    "content": "-1863637185=miscgraphics,4\n-1863637184=miscgraphics,5\n-1863637187=miscgraphics,2\n-1863637186=miscgraphics,3\n-1863637181=miscgraphics,8\n-1863637180=miscgraphics,9\n-440204630=alls fairy in love n war\n-1863637183=miscgraphics,6\n-1253085654=scorpia_dances\n-1863637182=miscgraphics,7\n1356196826=backvmid3\n-751102526=high seas\n1356196825=backvmid2\n-1863637189=miscgraphics,0\n549358875=camelot\n-1863637188=miscgraphics,1\n-1619800378=emotes_locked,19\n-1619800379=emotes_locked,18\n792536868=understanding\n1356196824=backvmid1\n-1773559904=title_mute\n-342013218=p11_full\n1523653533=pirates of peril\n696768774=harmony\n-1701556831=magicoff2,27\n281586976=orb_icon,3\n286265996=ready for battle\n-1701556832=magicoff2,26\n281586977=orb_icon,4\n-645977478=scape scared\n-1701556833=magicoff2,25\n281586978=orb_icon,5\n-1701556834=magicoff2,24\n-1701556830=magicoff2,28\n-1052794696=dance of death\n-1619800383=emotes_locked,14\n-1619800384=emotes_locked,13\n-1619800385=emotes_locked,12\n-1556842207=sl_flags\n-1619800386=emotes_locked,11\n1614826739=mapletree\n-1619800387=emotes_locked,10\n-1701556835=magicoff2,23\n-1701556836=magicoff2,22\n1363656441=serenade\n-1701556837=magicoff2,21\n3089326=door\n-1701556838=magicoff2,20\n-1619800380=emotes_locked,17\n-1619800381=emotes_locked,16\n-1619800382=emotes_locked,15\n-1037172987=tomorrow\n825974316=melodrama\n281586973=orb_icon,0\n281586974=orb_icon,1\n281586975=orb_icon,2\n-1701556864=magicoff2,15\n-1701556865=magicoff2,14\n-1701556866=magicoff2,13\n1509400204=sarcophagus\n-1701556867=magicoff2,12\n-1701556860=magicoff2,19\n-1701556861=magicoff2,18\n-1701556862=magicoff2,17\n1868377358=lower_depths\n-1701556863=magicoff2,16\n1086036315=reggae2\n-1220755677=hermit\n-1544597765=sl_stars\n-1619800350=emotes_locked,26\n-1619800351=emotes_locked,25\n-1636062434=tex_brown\n-1619800352=emotes_locked,24\n-1619800353=emotes_locked,23\n-2122174648=back to life\n-1619800354=emotes_locked,22\n-1701556868=magicoff2,11\n-1619800355=emotes_locked,21\n-1701556869=magicoff2,10\n-1619800356=emotes_locked,20\n1619539773=side_icons,7\n1121239524=scape wild\n368271413=diango's little helpers\n1619539772=side_icons,6\n1619539775=side_icons,9\n1619539774=side_icons,8\n-1938172321=miscgraphics2,0\n1619539771=side_icons,5\n1619539770=side_icons,4\n-926977577=the enchanter\n1202794514=doorways\n1343649581=schools out\n-1097177625=q8_full\n756012174=wornicons,4\n756012173=wornicons,3\n756012176=wornicons,6\n756012175=wornicons,5\n756012170=wornicons,0\n827249681=ogre the top\n756012172=wornicons,2\n756012171=wornicons,1\n-1741764817=poles apart\n756012178=wornicons,8\n-1938172320=miscgraphics2,1\n756012177=wornicons,7\n-43136286=the last shanty\n756012179=wornicons,9\n837131331=mapback\n-1938172313=miscgraphics2,8\n907740319=the depths\n-1938172312=miscgraphics2,9\n-2099722614=cave of beasts\n-1938172317=miscgraphics2,4\n-1938172316=miscgraphics2,5\n-1938172315=miscgraphics2,6\n-1938172314=miscgraphics2,7\n1619539769=side_icons,3\n-1367483767=cavern\n-1938172319=miscgraphics2,2\n50474489=treestump\n-1938172318=miscgraphics2,3\n1619539766=side_icons,0\n1619539768=side_icons,2\n1619539767=side_icons,1\n-607416954=prayeron,11\n1837251043=melzars maze\n-607416953=prayeron,12\n1318893900=have an ice day\n-607416952=prayeron,13\n-1701556800=magicoff2,37\n-607416951=prayeron,14\n-1701556801=magicoff2,36\n-56804840=woodland\n-607416950=prayeron,15\n1120636327=scape cave\n358884868=button_red\n-1701556806=magicoff2,31\n-1701556807=magicoff2,30\n-1701556802=magicoff2,35\n-1701556803=magicoff2,34\n-1701556804=magicoff2,33\n-1858265682=monster melee\n-1701556805=magicoff2,32\n-649484675=land of the dwarves\n-1789903512=golden touch\n1328851780=close_buttons,6\n1080306793=shayzien_march\n-1339126929=damage\n4820960=monkey sadness\n-1951786153=bone dance\n1328851781=close_buttons,7\n939546513=forlorn_homestead\n-1019905269=shadowland\n-607416955=prayeron,10\n-2048535896=pheasant peasant\n-1025233830=monkey madness\n1121125956=scape soft\n-1562452687=etcetera\n1969878996=emotes,47\n1969878994=emotes,45\n-1228279872=ge_icons,3\n1969878995=emotes,46\n-1228279871=ge_icons,4\n1969878992=emotes,43\n-1228279870=ge_icons,5\n1969878993=emotes,44\n1959803992=invback\n399048409=mage arena\n1969878990=emotes,41\n-47057524=lasting\n1969878991=emotes,42\n-1228279875=ge_icons,0\n-1228279874=ge_icons,1\n-1228279873=ge_icons,2\n-1860080918=inspiration\n1997757502=redstone2\n1997757503=redstone3\n-1060046352=tribal2\n1997757501=redstone1\n-1701556829=magicoff2,29\n1890607210=magicon2,37\n1890607211=magicon2,38\n1890607212=magicon2,39\n-1408684838=ascent\n-848436598=fishing\n-119984250=combaticons2,17\n-119984251=combaticons2,16\n1769177816=jungle island\n-119984252=combaticons2,15\n-119984253=combaticons2,14\n-119984254=combaticons2,13\n-119984255=combaticons2,12\n-119984256=combaticons2,11\n-119984257=combaticons2,10\n1890607205=magicon2,32\n1890607206=magicon2,33\n1890607207=magicon2,34\n1890607208=magicon2,35\n135141185=zeah_combat\n1890607209=magicon2,36\n-180851958=norse code\n112903447=water\n922007495=talking forest\n-672706748=miracle dance\n-1110089645=lament\n-1237461365=grotto\n1890607203=magicon2,30\n1890607204=magicon2,31\n-1073910849=mirror\n-988841056=still night\n-1857025509=sunburn\n-468596910=easter jig\n796868952=major miner\n-1066798491=trawler\n1640556978=wonderous\n-1624274920=emperor\n740093634=find my way\n1890607238=magicon2,44\n1890607239=magicon2,45\n3559837=tick\n-1839713245=sideicons\n1029455878=hells bells\n1890607234=magicon2,40\n1890607235=magicon2,41\n1890607236=magicon2,42\n1890607237=magicon2,43\n1503566841=forbidden\n-895763669=spooky\n-276138668=ham attack\n500433071=combaticons,5\n-1021014225=catch me if you can\n500433070=combaticons,4\n500433073=combaticons,7\n500433072=combaticons,6\n500433075=combaticons,9\n1533565119=mind over matter\n500433074=combaticons,8\n2025958358=emotes_locked,4\n500433066=combaticons,0\n2025958357=emotes_locked,3\n862821975=far away\n-1228392498=artistry\n2025958356=emotes_locked,2\n500433068=combaticons,2\n2025958355=emotes_locked,1\n500433067=combaticons,1\n1085444827=refresh\n500433069=combaticons,3\n2025958359=emotes_locked,5\n1427043851=on the up\n2025958354=emotes_locked,0\n1316697938=whistle\n347955347=venture\n1959211608=mapfunction,77\n1959211609=mapfunction,78\n881850881=the chosen\n1959211601=mapfunction,70\n1959211602=mapfunction,71\n584643951=lost soul\n582140282=rising damp\n1959211603=mapfunction,72\n1740872686=soulfall\n1959211604=mapfunction,73\n1959211605=mapfunction,74\n-119984248=combaticons2,19\n1959211606=mapfunction,75\n-119984249=combaticons2,18\n1609255038=slither and thither\n82917947=sarim's vermin\n1959211607=mapfunction,76\n1728911401=natural\n-1189743137=duel arena\n108698078=roof2\n214634021=head to head\n2025958361=emotes_locked,7\n-448773288=isle of everywhere\n2025958360=emotes_locked,6\n-1869996941=titlebox\n-338347745=showdown\n-2075972251=long ago\n2025958363=emotes_locked,9\n2025958362=emotes_locked,8\n-1487589606=7th realm\n-1253087691=garden\n-2133902017=zeah_farming\n-492926285=impetuous\n3314014=lair\n907815588=the desert\n-1960860275=barbarianism\n1890607241=magicon2,47\n1890607242=magicon2,48\n1890607243=magicon2,49\n-919642451=jungle bells\n795515487=underground\n561438836=fountain\n-1418827919=illusive\n-634763748=fruits de mer\n1890607240=magicon2,46\n1694458038=large_button\n1393517697=bandit camp\n1959211632=mapfunction,80\n1884773718=magicoff2,2\n1884773719=magicoff2,3\n1884773716=magicoff2,0\n2121900771=backtop1\n1884773717=magicoff2,1\n-103077377=gnomeball\n-1947119982=blistering barnacles\n828650857=autumn voyage\n92909147=alone\n1691516951=undead dungeon\n122265833=expecting\n-1320617626=dunjun\n1959211633=mapfunction,81\n1959211634=mapfunction,82\n1959211635=mapfunction,83\n1959211636=mapfunction,84\n1959211637=mapfunction,85\n777534707=army of darkness\n1959211638=mapfunction,86\n1959211639=mapfunction,87\n1884773721=magicoff2,5\n1884773722=magicoff2,6\n1884773720=magicoff2,4\n1959211640=mapfunction,88\n1959211641=mapfunction,89\n-327707013=anywhere\n1884773725=magicoff2,9\n1884773723=magicoff2,7\n1884773724=magicoff2,8\n1116844876=incantation\n-728886272=temple of light\n685934899=in the clink\n-1237289460=grumpy\n1945133711=inferno\n466902883=strange place\n-418223472=phasmatys\n817472004=zombiism\n106578554=zeah_book,0\n106578555=zeah_book,1\n1509070203=eagle peak\n-485932799=expedition\n1171923143=emotes,8\n-675357975=attack1\n1171923144=emotes,9\n1959211610=mapfunction,79\n-675357974=attack2\n-675357973=attack3\n-675357972=attack4\n-675357971=attack5\n40246002=masquerade\n-675357970=attack6\n-734206983=arrival\n-1980407601=sea shanty xmas\n284435223=pharoah's tomb\n-148552909=down below\n1171923141=emotes,6\n1171923142=emotes,7\n1171923140=emotes,5\n-1077789440=mellow\n-710537653=kingdom\n1171923138=emotes,3\n1171923139=emotes,4\n1171923136=emotes,1\n-2098286081=venture2\n1171923137=emotes,2\n1171923135=emotes,0\n-1094248165=sigmunds showdown\n-271106892=rat a tat tat\n3288564=keys\n-143163121=ham fisted\n-900633031=medieval\n944208821=life's a beach\\!\n-1228279453=riverside\n-1666444059=combaticons,10\n825919125=options_icons,24\n825919126=options_icons,25\n1179379180=the trade parade\n-1666444057=combaticons,12\n825919123=options_icons,22\n-1666444058=combaticons,11\n825919124=options_icons,23\n825919121=options_icons,20\n1884768169=magicoff,32\n825919122=options_icons,21\n1884768167=magicoff,30\n1884768168=magicoff,31\n1318818808=chainmail\n582031337=intrepid\n783525419=beetle juice\n432605856=untouchable\n-969918857=neverland\n79789174=narnode's theme\n-705938181=zealot\n117588=web\n-1666444051=combaticons,18\n-1666444052=combaticons,17\n1687654733=troubled\n-1666444050=combaticons,19\n-1666444055=combaticons,14\n825919129=options_icons,28\n-1666444056=combaticons,13\n-1666444053=combaticons,16\n825919127=options_icons,26\n-1666444054=combaticons,15\n825919128=options_icons,27\n1320694328=magical journey\n364185053=roll the bones\n-1254483584=jungly1\n981183822=right on track\n-1254483583=jungly2\n-1254483582=jungly3\n3075958=dark\n-2038936746=deep down\n1512143976=everlasting fire\n-1392319985=beyond\n46273615=tale of keldagrim\n-651951461=goblin game\n3522941=save\n104084791=mossy\n1250935993=the monsters below\n794539501=garden of summer\n1814277765=elven mist\n2110556093=the golem\n-1475251658=where eagles lair\n1884768143=magicoff,27\n1884768144=magicoff,28\n1884768141=magicoff,25\n1529837717=bubble and squeak\n1884768142=magicoff,26\n-1679325940=technology\n1884768145=magicoff,29\n-826562194=troubled_waters\n1884768140=magicoff,24\n-1359348243=painting1\n1267356434=the power of tears\n-1359348242=painting2\n-860755690=jungle hunt\n1884768138=magicoff,22\n-1197347961=magic magic magic\n1884768139=magicoff,23\n1134405764=hypnotized\n1959211539=mapfunction,50\n1381363755=my arms journey\n-1644401602=complication\n1959211540=mapfunction,51\n1959211541=mapfunction,52\n1884768136=magicoff,20\n1959211542=mapfunction,53\n2111304827=warning_icons,0\n1884768137=magicoff,21\n-440187560=zogre dance\n1959211543=mapfunction,54\n2111304828=warning_icons,1\n-2002535437=corridors of power\n825919130=options_icons,29\n580384095=jungle troubles\n1301622585=slice of station\n1959211544=mapfunction,55\n2111304829=warning_icons,2\n-1294172031=escape\n-1309477156=expanse\n-1526067851=alternative root\n2124773424=dynasty\n1743765602=leftarrow\n-1482676188=romancing the crone\n-1891851953=island of the trolls\n736457293=small_button_pressed\n-1106172890=letter\n986170990=dreamstate\n1959211545=mapfunction,56\n1959211546=mapfunction,57\n1765722413=spirits of elid\n1959211547=mapfunction,58\n1959211548=mapfunction,59\n-2075333010=lonesome\n3314400=lava\n1355033875=worldmap_icon,1\n1814357716=knightmare\n1690742645=nox_irae\n94935104=cross\n-1249495153=frogland\n-1642689926=athletes foot\n107944162=quest\n1355033874=worldmap_icon,0\n-2130741313=joy of the hunt\n-28982081=labyrinth\n250959119=marooned\n-1522984472=altar_ego\n1326424637=the lost melody\n-1779111734=arabique\n-398925062=sea shanty2\n1884768110=magicoff,15\n1884768111=magicoff,16\n1817249074=woe of the wyvern\n1884768114=magicoff,19\n1884768112=magicoff,17\n-1624760229=emotion\n1884768113=magicoff,18\n-353951458=attention\n279431252=garden of autumn\n422652266=small_button\n1884768107=magicoff,12\n1884768108=magicoff,13\n375695247=the far side\n1884768105=magicoff,10\n1884768106=magicoff,11\n-528864109=crystal sword\n1884768109=magicoff,14\n1959211570=mapfunction,60\n-158141423=prayeron,7\n-158141424=prayeron,6\n1959211571=mapfunction,61\n-158141421=prayeron,9\n1959211572=mapfunction,62\n-158141422=prayeron,8\n1959211573=mapfunction,63\n1959211574=mapfunction,64\n688840255=piscarilius_sigil\n1959211575=mapfunction,65\n1959211576=mapfunction,66\n1170407052=headicons_prayer\n1959211577=mapfunction,67\n-324496873=soundscape\n-1418445703=tex_red\n1961540869=wornicons,10\n-1028580907=that_sullen_hall\n397136995=elfpainting\n-2092714094=haunted mine\n1959211578=mapfunction,68\n1959211579=mapfunction,69\n-158141429=prayeron,1\n-158141427=prayeron,3\n-140492390=bunny_sugar_rush\n-158141428=prayeron,2\n-158141425=prayeron,5\n1827366203=righteousness\n-649601274=darkness_in_the_depths\n-158141426=prayeron,4\n1961540870=wornicons,11\n-158141430=prayeron,0\n910299584=principality\n-734028978=arrow,1\n-734028979=arrow,0\n790067275=garden of spring\n35762567=workshop\n-1095396929=competition\n96463963=egypt\n-1154441378=jollyr\n-1685231711=cave background\n-2078908549=time out\n-1172405897=wildwood\n-170561624=spookyjungle\n2110231453=mod_icons\n2061491048=shining\n-1063411723=tremble\n94627585=chest\n-520702427=ice melody\n1346720899=backleft1\n-607416919=prayeron,25\n-607416918=prayeron,26\n900197712=staticons,6\n-607416917=prayeron,27\n900197713=staticons,7\n-607416916=prayeron,28\n900197710=staticons,4\n-607416915=prayeron,29\n900197711=staticons,5\n957931606=courage\n900197714=staticons,8\n900197715=staticons,9\n-720253066=the other side\n-1655721374=prayeroff,31\n-607416921=prayeron,23\n-1655721375=prayeroff,30\n-607416920=prayeron,24\n-710515142=the mad mole\n-1350228392=stratosphere\n-1666438445=combaticons2,3\n-1666438446=combaticons2,2\n-1666438443=combaticons2,5\n-1666438444=combaticons2,4\n-1106570438=legion\n-1666438441=combaticons2,7\n-1666438442=combaticons2,6\n1398587265=flute salad\n-1666438440=combaticons2,8\n837223705=mapedge\n900197709=staticons,3\n-243680393=peng_emotes,7\n900197707=staticons,1\n378300078=everlasting\n900197708=staticons,2\n-944748869=witching\n-243680396=peng_emotes,4\n-1335336992=logo_deadman_mode\n-1368714737=small_button_blue\n-795140435=wander\n-243680397=peng_emotes,3\n-243680394=peng_emotes,6\n-1666438447=combaticons2,1\n-243680395=peng_emotes,5\n-1666438448=combaticons2,0\n-243680398=peng_emotes,2\n-243680399=peng_emotes,1\n-607416924=prayeron,20\n900197706=staticons,0\n-607416923=prayeron,21\n-607416922=prayeron,22\n3016376=bark\n-89244313=romper chomper\n346288985=dorgeshun deep\n1585002399=magicon,21\n-1725263140=chef surprize\n1585002398=magicon,20\n-993528987=making waves\n-628963539=ham and seek\n-1666438439=combaticons2,9\n-333224315=baroque\n-1236252722=prime time\n280241284=waking dream\n-564582358=distant land\n115411843=castlewars\n1513246078=al kharid\n-1377700863=unknown land\n1264132816=miles away\n1185785872=barbassault_icons,3\n1185785873=barbassault_icons,4\n-1655721397=prayeroff,29\n-1655721398=prayeroff,28\n1185785874=barbassault_icons,5\n-1655721399=prayeroff,27\n1185785875=barbassault_icons,6\n1711341885=fight or flight\n1185785870=barbassault_icons,1\n1185785871=barbassault_icons,2\n3530505=sire\n-309570839=pick_and_shovel\n647234089=distillery hilarity\n-127408236=gnome_village_party\n1337378554=backbase2\n109757537=stars\n1337378553=backbase1\n109757538=start\n819884325=wilderness3\n1328851775=close_buttons,1\n658759958=side_background\n1328851774=close_buttons,0\n-782211141=wonder\n1328851777=close_buttons,3\n-1995718284=wall_white\n819884324=wilderness2\n1328851776=close_buttons,2\n1328851779=close_buttons,5\n-943885542=scape hunter\n1328851778=close_buttons,4\n1185785869=barbassault_icons,0\n-607416949=prayeron,16\n-607416948=prayeron,17\n-1779684630=rune essence\n-607416947=prayeron,18\n-607416946=prayeron,19\n1119460311=bandos battalion\n-967559823=creature cruelty\n-1904094243=zeah_fishing\n-1913214770=wilderness\n460367020=village\n825919161=options_icons,39\n1585002375=magicon,18\n94839810=coins\n1585002376=magicon,19\n825919160=options_icons,38\n-1282090556=faerie\n-521895311=the adventurer\n788399136=tree spirits\n-1902858744=beneath_the_stronghold\n-356730043=pirates of penance\n1185785876=barbassault_icons,7\n825919158=options_icons,36\n825919159=options_icons,37\n825919156=options_icons,34\n-1455241861=victory is mine\n825919157=options_icons,35\n-1333874720=side_icons,17\n825919154=options_icons,32\n825919155=options_icons,33\n825919152=options_icons,30\n825919153=options_icons,31\n1437805631=chatback\n-1623296531=ground scape\n685190118=in the brine\n1366257555=nightfall\n110327241=theme\n-1333874725=side_icons,12\n-1333874726=side_icons,11\n-1333874727=side_icons,10\n-1333874721=side_icons,16\n-1333874722=side_icons,15\n-1124681475=darkly_altared\n-8976533=throne of the demon\n-1333874723=side_icons,14\n-1333874724=side_icons,13\n-1989106719=assault and battery\n1958759012=greatness\n1057075019=b12_full\n1143353537=chain of command\n-51091830=desert voyage\n-1073927447=mirage\n-91048728=number_button\n3165239=gaol\n-1380919269=breeze\n445640248=rugged_terrain\n106079=key\n-655784411=overlay_multiway\n-1025835715=backright2\n-1025835716=backright1\n1120933843=scape main\n3225350=iban\n-956253112=title fight\n-123912401=la mort\n1585002367=magicon,10\n-2128736428=startgame\n1585002368=magicon,11\n-925031874=royale\n1585002369=magicon,12\n1585002370=magicon,13\n1585002373=magicon,16\n1585002374=magicon,17\n-1307116191=superstition\n1585002371=magicon,14\n1585002372=magicon,15\n-587569902=path of peril\n3392903=null\n-1601127242=inadequacy\n2136330800=staticons2,0\n2136330801=staticons2,1\n344336468=grip of the talon\n2136330804=staticons2,4\n2136330805=staticons2,5\n2136330802=staticons2,2\n2136330803=staticons2,3\n1960215130=barking mad\n-43712789=scape original\n621171714=cellar song\n1585002461=magicon,41\n1585002462=magicon,42\n1585002460=magicon,40\n111485446=upass\n2136330808=staticons2,8\n694847251=in the manor\n2136330809=staticons2,9\n2136330806=staticons2,6\n2136330807=staticons2,7\n-1385847955=rightarrow\n1343200077=the slayer\n1585002465=magicon,45\n1585002466=magicon,46\n1585002463=magicon,43\n1585002464=magicon,44\n1585002469=magicon,49\n-95571520=volcanic vikings\n1585002467=magicon,47\n1585002468=magicon,48\n-1032629963=shipwrecked\n93330745=aztec\n-881372797=tabs,1\n1377351472=oriental\n121641580=headicons_hint\n-881372798=tabs,0\n1585002438=magicon,39\n-1661605940=elfwood\n-607416893=prayeron,30\n-822106577=jungle island xmas\n-607416892=prayeron,31\n332368736=mad eadgar\n1585002432=magicon,33\n-143368781=side_background_right\n1585002433=magicon,34\n-1081494434=malady\n1585002430=magicon,31\n1585002431=magicon,32\n1585002436=magicon,37\n1585002437=magicon,38\n1585002434=magicon,35\n1585002435=magicon,36\n-1938171360=miscgraphics3,0\n-1059680853=trinity\n781557721=dies_irae\n-1938171359=miscgraphics3,1\n497375231=stillness\n-1938171358=miscgraphics3,2\n-1938171357=miscgraphics3,3\n-960709976=dogs of war\n2129339089=magicon,1\n2129339088=magicon,0\n755433248=headicons_pk\n108392383=regal\n-213632750=waterfall\n-1367706280=canvas\n73828649=settlement\n848123561=into the abyss\n478781900=last stand\n1339486127=the shadow\n-1055503808=roc and roll\n837204902=mapdots\n950484242=compass\n-1082154559=fanfare\n747848680=nether_realm\n788224888=dead quiet\n1532279978=monarch waltz\n-149029727=side_background_left1\n812947089=fanfare2\n-149029726=side_background_left2\n1006643748=high spirits\n-2136059388=starlight\n2122572442=the tower\n-1998869913=spooky2\n1411067174=gnome village2\n-2065077267=wild side\n812947090=fanfare3\n1585002429=magicon,30\n1294629755=on the wing\n2097127567=monkey badness\n-2032107216=sojourn\n1020264019=pest control\n3237038=info\n1473393027=fe fi fo fum\n-1686202291=upper_depths\n3540994=stop\n1742080803=darkwood\n740392969=little cave of horrors\n-158379532=prayerglow\n-691855347=in between\n-200702983=the noble rodent\n1652745754=forgotten\n-1895307673=hitmark,3\n-1895307674=hitmark,2\n-1895307675=hitmark,1\n-1895307676=hitmark,0\n-1895307670=hitmark,6\n-1895307671=hitmark,5\n-1895307672=hitmark,4\n1968917071=bone dry\n-850506182=trawler minor\n197029040=mapscene\n-808772318=in the pits\n-1165315580=looking back\n-1763090403=scape_ape\n-1938177931=miscgraphics,11\n-1938177932=miscgraphics,10\n1936130561=thrall_of_the_serpent\n1258863383=yesteryear\n1994744000=slice of silent movie\n-1691854169=dead can dance\n1585002407=magicon,29\n1585002405=magicon,27\n1585002406=magicon,28\n271319484=frostbite\n-499867199=meridian\n1585002400=magicon,22\n-1938177930=miscgraphics,12\n-84626226=mudskipper melody\n1585002403=magicon,25\n1585002404=magicon,26\n3641802=wall\n1585002401=magicon,23\n1585002402=magicon,24\n-606457701=wolf mountain\n1276599785=button_brown\n1969878899=emotes,13\n1969878897=emotes,11\n1969878898=emotes,12\n1969878896=emotes,10\n-1938177928=miscgraphics,14\n-907669678=brew hoo hoo\n-1938177929=miscgraphics,13\n72999866=subterranea\n619237947=the galleon\n-1764950404=scape sad\n295831445=heart and mind\n908430134=dangerous road\n738888631=tradebacking\n-174800339=verdana_11pt_regular\n686705631=lightwalk\n-601591436=side_background_bottom\n-1479412376=the navigator\n-359173459=zamorak zoo\n744536246=null and void\n-1701556798=magicoff2,39\n-1701556799=magicoff2,38\n-1396384012=bamboo\n-200388662=lighthouse\n133626717=suspicious\n-810515425=voyage\n3061973=crag\n1802291895=big chords\n-1661619479=elfwall\n113315621=wood2\n1813041183=steelborder2,0\n429244831=slug a bug ball\n1813041184=steelborder2,1\n-1658386264=shining_spirit\n738909086=chamber\n526264239=verdana_13pt_regular\n-877351859=temple\n2142215577=the mollusc menace\n1124498189=warpath\n-2136649922=no way out\n-339706871=grimly_fiendish\n547534551=wrath and ruin\n544229147=lore and order\n3327206=load\n1610073470=lovakengj_sigil\n-419218284=long way home\n-662489856=food for thought\n1306461568=stagnant\n-1662171955=elfdoor\n-1043985601=meddling kids\n947464074=titlebutton\n-1309055712=exposed\n-1487348923=ambient jungle\n-1829469821=lament of meiyerditch\n233203434=leftarrow_small\n-1216167350=dangerous\n114464611=railings\n-1106574323=legend\n-1701556767=magicoff2,49\n-999707515=time to mine\n-1701556768=magicoff2,48\n2129339097=magicon,9\n2129339096=magicon,8\n1959211510=mapfunction,42\n2129339095=magicon,7\n1959211511=mapfunction,43\n2129339094=magicon,6\n2129339093=magicon,5\n2129339092=magicon,4\n2129339091=magicon,3\n2129339090=magicon,2\n3522472=saga\n-1701556769=magicoff2,47\n-544722449=rellekka\n1033441676=tribal background\n1915718129=the desolate isle\n1890607150=magicon2,19\n1787618597=stranded\n1717999087=forgettable melody\n1959211512=mapfunction,44\n-243680400=peng_emotes,0\n1959211513=mapfunction,45\n1959211514=mapfunction,46\n1959211515=mapfunction,47\n1959211516=mapfunction,48\n1959211517=mapfunction,49\n1705947058=the cellar dwellers\n1216634785=landlubber\n1884768198=magicoff,40\n-1588113323=the rogues den\n1884768199=magicoff,41\n-905842564=serene\n-607599698=prayeroff,2\n-607599699=prayeroff,1\n1890607142=magicon2,11\n1389384362=monkey trouble\n1890607143=magicon2,12\n1890607144=magicon2,13\n1890607145=magicon2,14\n1890607146=magicon2,15\n1890607147=magicon2,16\n1890607148=magicon2,17\n1966766798=mausoleum\n1890607149=magicon2,18\n1808345541=armadyl alliance\n290391725=options_slider,7\n1890607141=magicon2,10\n290391722=options_slider,4\n-607599696=prayeroff,4\n290391721=options_slider,3\n-607599697=prayeroff,3\n290391724=options_slider,6\n-607599694=prayeroff,6\n290391723=options_slider,5\n-607599695=prayeroff,5\n-607599692=prayeroff,8\n-607599693=prayeroff,7\n290391720=options_slider,2\n-607599691=prayeroff,9\n1334775925=chat_background\n-1779127378=arabian2\n-1779127377=arabian3\n528722471=island life\n1890607175=magicon2,23\n1890607176=magicon2,24\n949634504=mouse trap\n1890607177=magicon2,25\n1890607178=magicon2,26\n1890607179=magicon2,27\n3327403=logo\n290391719=options_slider,1\n1092249049=storm brew\n290391718=options_slider,0\n404357804=everywhere\n1890607172=magicon2,20\n1890607173=magicon2,21\n951530772=contest\n1890607174=magicon2,22\n-1701556776=magicoff2,40\n1884768176=magicoff,39\n-395250469=corporal punishment\n1749113330=newbie melody\n1884768174=magicoff,37\n1884768175=magicoff,38\n-1701556772=magicoff2,44\n-1701556773=magicoff2,43\n-1701556774=magicoff2,42\n-1701556775=magicoff2,41\n-858121616=tzhaar\n1884768172=magicoff,35\n1884768173=magicoff,36\n666772244=combat_shield\n1884768170=magicoff,33\n1884768171=magicoff,34\n1959211509=mapfunction,41\n1639695510=mapmarker\n-1661748240=friends_icons\n-552301350=knightly\n-1918044851=mastermindless\n-1701556770=magicoff2,46\n-1701556771=magicoff2,45\n201526300=corporealbeast\n1959211508=mapfunction,40\n105001967=nomad\n-70910145=clickcross,3\n-1737914947=mapfunction,5\n1801745440=staticons2,11\n-70910146=clickcross,2\n-865479038=tribal\n-1737914946=mapfunction,6\n1801745441=staticons2,12\n-70910147=clickcross,1\n-1737914945=mapfunction,7\n1801745442=staticons2,13\n-70910148=clickcross,0\n-1737914944=mapfunction,8\n1801745443=staticons2,14\n-1737914943=mapfunction,9\n1801745444=staticons2,15\n1801745445=staticons2,16\n1801745446=staticons2,17\n-1877545169=land down under\n93921962=books\n-1737914949=mapfunction,3\n-1737914948=mapfunction,4\n-1655721428=prayeroff,19\n-2136884405=title.jpg\n-1655721429=prayeroff,18\n1584819628=magicoff,6\n1584819629=magicoff,7\n437480876=voodoo cult\n1584819624=magicoff,2\n1584819625=magicoff,3\n1584819626=magicoff,4\n-1737914952=mapfunction,0\n1584819627=magicoff,5\n-1737914951=mapfunction,1\n124995564=harmony2\n1584819622=magicoff,0\n1801745439=staticons2,10\n1584819623=magicoff,1\n346263512=dorgeshun city\n-1665011705=down and out\n1890607180=magicon2,28\n1890607181=magicon2,29\n1417471781=titlescroll\n1959211446=mapfunction,20\n1956141536=options_radio_buttons,0\n736568812=ballad of enchantment\n-1737914950=mapfunction,2\n1959211447=mapfunction,21\n1959211448=mapfunction,22\n1959211449=mapfunction,23\n-1890130256=morytania\n-70910141=clickcross,7\n-70910142=clickcross,6\n1956141539=options_radio_buttons,3\n-70910143=clickcross,5\n1956141538=options_radio_buttons,2\n284766976=splendour\n-70910144=clickcross,4\n1956141537=options_radio_buttons,1\n196677638=the quizmaster\n530068296=overture\n-1123094568=sl_button\n-700552779=hosidius_sigil\n-614076819=sad meadow\n1956141543=options_radio_buttons,7\n1956141542=options_radio_buttons,6\n1956141541=options_radio_buttons,5\n1956141540=options_radio_buttons,4\n1584819631=magicoff,9\n1846633612=gnome village\n-2128560371=sl_back\n1969878905=emotes,19\n306819362=crystal castle\n1584819630=magicoff,8\n1969878903=emotes,17\n303737220=options_icons,7\n1969878904=emotes,18\n1969878901=emotes,15\n-78220817=devils_may_care\n1969878902=emotes,16\n-40521666=dimension x\n1969878900=emotes,14\n673424924=the lunar isle\n789609582=brimstail's scales\n303737222=options_icons,9\n303737221=options_icons,8\n1959211415=mapfunction,10\n3059343=coil\n1959211416=mapfunction,11\n1959211417=mapfunction,12\n-1256560486=last_man_standing\n1959211418=mapfunction,13\n336238005=rightarrow_small\n1959211419=mapfunction,14\n-677662361=forever\n-1655721430=prayeroff,17\n-1655721431=prayeroff,16\n-1655721432=prayeroff,15\n1959211420=mapfunction,15\n1959211663=mapfunction,90\n1959211421=mapfunction,16\n1959211422=mapfunction,17\n1959211423=mapfunction,18\n303737217=options_icons,4\n303737216=options_icons,3\n303737215=options_icons,2\n-1665005042=funny bunnies\n303737214=options_icons,1\n303737219=options_icons,6\n303737218=options_icons,5\n303737213=options_icons,0\n95997798=we are the fairies\n2001751835=desert heat\n1959211424=mapfunction,19\n-1655721437=prayeroff,10\n687938017=clanwars\n-1776024210=desolate_mage\n-650944128=strength of saradomin\n-1655721433=prayeroff,14\n1160873524=aye car rum ba\n-1655721434=prayeroff,13\n-1655721435=prayeroff,12\n-1081314499=marble\n-1655721436=prayeroff,11\n1097075475=reset,0\n1959211477=mapfunction,30\n-693313916=warriors guild\n3506388=roof\n1959211478=mapfunction,31\n1097075476=reset,1\n-2134967800=dagannoth dawn\n-985763247=planks\n1959211479=mapfunction,32\n1999746381=fenkenstrain's refrain\n898010371=garden of winter\n359174830=rat hunt\n1959211482=mapfunction,35\n686441581=lightness\n1959211483=mapfunction,36\n1959211484=mapfunction,37\n1959211485=mapfunction,38\n1959211486=mapfunction,39\n2023201035=dwarf theme\n1959211480=mapfunction,33\n1959211481=mapfunction,34\n-1065532022=combatboxes,1\n-1065532021=combatboxes,2\n-1065532020=combatboxes,3\n1867160429=old_tiles\n394756979=scape santa\n25205919=elfroof2\n-663428071=dangerous way\n-1065532023=combatboxes,0\n1959211450=mapfunction,24\n-895939599=spirit\n1959211451=mapfunction,25\n1959211452=mapfunction,26\n1959211453=mapfunction,27\n1959211454=mapfunction,28\n1959211455=mapfunction,29\n-275310687=undercurrent\n212205923=goblin village\n-303898981=faithless\n-1381531001=tomb raider\n260940912=marzipan\n1343267530=backhmid1\n1343267531=backhmid2\n1097468315=horizon\n-1655721404=prayeroff,22\n623451622=kourend_the_magnificent\n-1655721405=prayeroff,21\n-1655721406=prayeroff,20\n-1655721400=prayeroff,26\n-313384067=p12_full\n-1655721401=prayeroff,25\n-1655721402=prayeroff,24\n-1655721403=prayeroff,23\n95848451=dream\n1966781751=maws_jaws_claws\n-995428255=parade\n95734525=method of madness\n-1308064877=hitmarks\n1030045177=mutant medley\n1333034828=blackmark\n851641665=davy jones locker\n417793574=scrollbar\n1346720900=backleft2\n1884768206=magicoff,48\n1884768207=magicoff,49\n1801140808=fangs for the memory\n1884768204=magicoff,46\n1345432055=pinball wizard\n1884768205=magicoff,47\n-783693496=dance of the undead\n1274780903=chompy hunt\n465278529=the lost tribe\n-1666437481=combaticons3,6\n2032696205=cabin fever\n-1666437480=combaticons3,7\n1825640471=borderland\n415928477=zeah_mining\n1884768202=magicoff,44\n-607599700=prayeroff,0\n1884768203=magicoff,45\n1884768200=magicoff,42\n1884768201=magicoff,43\n-1666437487=combaticons3,0\n-1666437486=combaticons3,1\n813726263=crystal cave\n1235442953=pathways\n518814479=lullaby\n-1666437483=combaticons3,4\n1585007985=magicon2,7\n-1666437482=combaticons3,5\n1585007986=magicon2,8\n-1666437485=combaticons3,2\n1585007987=magicon2,9\n-1666437484=combaticons3,3\n104080482=moody\n1969878967=emotes,39\n-665666447=work work work\n1364992651=evil bobs island\n1969878965=emotes,37\n1227328817=verdana_15pt_regular\n1969878966=emotes,38\n1969878963=emotes,35\n1581724013=monkey business\n1969878964=emotes,36\n-74307138=miscellania\n1969878961=emotes,33\n1969878962=emotes,34\n1969878960=emotes,32\n1131171307=wayward\n-1154558416=sl_arrows\n529929957=overpass\n1258058669=huffman\n-694094064=adventure\n1171698653=orb_xp,1\n1171698652=orb_xp,0\n1171698655=orb_xp,3\n1171698654=orb_xp,2\n1802171733=arceuus_sigil\n221109227=tears of guthix\n-1661754893=elfroof\n-1666437478=combaticons3,9\n1124565314=warrior\n1969878958=emotes,30\n1969878959=emotes,31\n-934797897=reggae\n2110260221=the genie\n-1666437479=combaticons3,8\n104257585=mummy\n-1923924724=sworddecor,0\n2136325196=staticons,17\n-1332194002=background\n2136164423=homescape\n-1923924721=sworddecor,3\n2136325194=staticons,15\n-1923924722=sworddecor,2\n-873564465=tiptoe\n2136325195=staticons,16\n-1923924723=sworddecor,1\n123560953=espionage\n2136325192=staticons,13\n2136325193=staticons,14\n1038911415=gnome king\n2136325190=staticons,11\n2136325191=staticons,12\n1854274741=karamja jam\n1969878989=emotes,40\n-1242708793=glyphs\n563269755=the terrible tower\n1650323088=twilight\n-12868552=sea shanty\n289742397=book of spells\n1880989696=dragontooth island\n2136325189=staticons,10\n110873=pen\n-454421102=out of the deep\n825919099=options_icons,19\n825919097=options_icons,17\n825919098=options_icons,18\n-1349119470=cursed\n1585007978=magicon2,0\n825919095=options_icons,15\n1585007979=magicon2,1\n825919096=options_icons,16\n825919093=options_icons,13\n825919094=options_icons,14\n825919091=options_icons,11\n825919092=options_icons,12\n-895977880=sphinx\n-874529881=city of the dead\n825919090=options_icons,10\n1585007981=magicon2,3\n1585007982=magicon2,4\n1585007983=magicon2,5\n-1618729246=body parts\n1585007984=magicon2,6\n1585007980=magicon2,2\n1086075866=shayzien_sigil\n2103661451=jester minute\n-911346307=steelborder,2\n-1809781334=button_brown_big\n-911346306=steelborder,3\n-816227352=vision\n-911346309=steelborder,0\n109407595=shine\n-911346308=steelborder,1\n-119954464=combaticons3,12\n-119954465=combaticons3,11\n-1846853118=armageddon\n-119954462=combaticons3,14\n-119954463=combaticons3,13\n-119954460=combaticons3,16\n-908183966=scarab\n-119954461=combaticons3,15\n-282886672=home sweet home\n103666243=march\n1969878929=emotes,22\n1643875326=fire and brimstone\n1969878927=emotes,20\n-119954466=combaticons3,10\n1969878928=emotes,21\n-839455633=close quarters\n941457503=way of the enchanter\n-1453405761=mor-ul-rek\n-415134015=have a blast\n-1658514874=floating free\n1213477442=chickened out\n-1619800349=emotes_locked,27\n-599680631=fear and loathing\n-1081041422=insect queen\n-1268786147=forest\n-119954459=combaticons3,17\n-119954457=combaticons3,19\n-119954458=combaticons3,18\n-850395529=trouble brewing\n-1773920521=cave of the goblins\n-771284962=claustrophobia\n1310729739=bankbuttons,2\n1306691868=upcoming\n1465443077=over to nardah\n1310729744=bankbuttons,7\n1310729743=bankbuttons,6\n1310729742=bankbuttons,5\n1234827707=deep wildy\n-90350772=xenophobe\n-750127868=arabian\n1310729741=bankbuttons,4\n1310729740=bankbuttons,3\n1041911129=waterlogged\n108875897=runes\n1447063382=barb wire\n-378865792=magic dance\n285466503=overlay_duel\n1814287296=zeah_magic\n1976894499=down to earth\n1969878936=emotes,29\n1969878934=emotes,27\n-1567437308=deadlands\n1969878935=emotes,28\n1310729738=bankbuttons,1\n1969878932=emotes,25\n-957019274=too many cooks\n1310729737=bankbuttons,0\n2092627105=silence\n1969878933=emotes,26\n1969878930=emotes,23\n1969878931=emotes,24\n-901674570=well of voyage\n1787935731=quill_oblique_large\n-365283881=quill_caps_large\n1157777820=lunar_alphabet\n24702590=lunar_alphabet_lrg\n"
  },
  {
    "path": "config/file-names.properties",
    "content": "-1863637185=miscgraphics,4\n-1863637184=miscgraphics,5\n-1863637187=miscgraphics,2\n-1863637186=miscgraphics,3\n-1863637181=miscgraphics,8\n-1863637180=miscgraphics,9\n-440204630=alls fairy in love n war\n-1863637183=miscgraphics,6\n-1253085654=scorpia_dances\n-1863637182=miscgraphics,7\n1356196826=backvmid3\n-751102526=high seas\n1356196825=backvmid2\n-1863637189=miscgraphics,0\n549358875=camelot\n-1863637188=miscgraphics,1\n-1619800378=emotes_locked,19\n-1619800379=emotes_locked,18\n792536868=understanding\n1356196824=backvmid1\n-1773559904=title_mute\n-342013218=p11_full\n1523653533=pirates of peril\n696768774=harmony\n-1701556831=magicoff2,27\n281586976=orb_icon,3\n286265996=ready for battle\n-1701556832=magicoff2,26\n281586977=orb_icon,4\n-645977478=scape scared\n-1701556833=magicoff2,25\n281586978=orb_icon,5\n-1701556834=magicoff2,24\n-1701556830=magicoff2,28\n-1052794696=dance of death\n-1619800383=emotes_locked,14\n-1619800384=emotes_locked,13\n-1619800385=emotes_locked,12\n-1556842207=sl_flags\n-1619800386=emotes_locked,11\n1614826739=mapletree\n-1619800387=emotes_locked,10\n-1701556835=magicoff2,23\n-1701556836=magicoff2,22\n1363656441=serenade\n-1701556837=magicoff2,21\n3089326=door\n-1701556838=magicoff2,20\n-1619800380=emotes_locked,17\n-1619800381=emotes_locked,16\n-1619800382=emotes_locked,15\n-1037172987=tomorrow\n825974316=melodrama\n281586973=orb_icon,0\n281586974=orb_icon,1\n281586975=orb_icon,2\n-1701556864=magicoff2,15\n-1701556865=magicoff2,14\n-1701556866=magicoff2,13\n1509400204=sarcophagus\n-1701556867=magicoff2,12\n-1701556860=magicoff2,19\n-1701556861=magicoff2,18\n-1701556862=magicoff2,17\n1868377358=lower_depths\n-1701556863=magicoff2,16\n1086036315=reggae2\n-1220755677=hermit\n-1544597765=sl_stars\n-1619800350=emotes_locked,26\n-1619800351=emotes_locked,25\n-1636062434=tex_brown\n-1619800352=emotes_locked,24\n-1619800353=emotes_locked,23\n-2122174648=back to life\n-1619800354=emotes_locked,22\n-1701556868=magicoff2,11\n-1619800355=emotes_locked,21\n-1701556869=magicoff2,10\n-1619800356=emotes_locked,20\n1619539773=side_icons,7\n1121239524=scape wild\n368271413=diango's little helpers\n1619539772=side_icons,6\n1619539775=side_icons,9\n1619539774=side_icons,8\n-1938172321=miscgraphics2,0\n1619539771=side_icons,5\n1619539770=side_icons,4\n-926977577=the enchanter\n1202794514=doorways\n1343649581=schools out\n-1097177625=q8_full\n756012174=wornicons,4\n756012173=wornicons,3\n756012176=wornicons,6\n756012175=wornicons,5\n756012170=wornicons,0\n827249681=ogre the top\n756012172=wornicons,2\n756012171=wornicons,1\n-1741764817=poles apart\n756012178=wornicons,8\n-1938172320=miscgraphics2,1\n756012177=wornicons,7\n-43136286=the last shanty\n756012179=wornicons,9\n837131331=mapback\n-1938172313=miscgraphics2,8\n907740319=the depths\n-1938172312=miscgraphics2,9\n-2099722614=cave of beasts\n-1938172317=miscgraphics2,4\n-1938172316=miscgraphics2,5\n-1938172315=miscgraphics2,6\n-1938172314=miscgraphics2,7\n1619539769=side_icons,3\n-1367483767=cavern\n-1938172319=miscgraphics2,2\n50474489=treestump\n-1938172318=miscgraphics2,3\n1619539766=side_icons,0\n1619539768=side_icons,2\n1619539767=side_icons,1\n-607416954=prayeron,11\n1837251043=melzars maze\n-607416953=prayeron,12\n1318893900=have an ice day\n-607416952=prayeron,13\n-1701556800=magicoff2,37\n-607416951=prayeron,14\n-1701556801=magicoff2,36\n-56804840=woodland\n-607416950=prayeron,15\n1120636327=scape cave\n358884868=button_red\n-1701556806=magicoff2,31\n-1701556807=magicoff2,30\n-1701556802=magicoff2,35\n-1701556803=magicoff2,34\n-1701556804=magicoff2,33\n-1858265682=monster melee\n-1701556805=magicoff2,32\n-649484675=land of the dwarves\n-1789903512=golden touch\n1328851780=close_buttons,6\n1080306793=shayzien_march\n-1339126929=damage\n4820960=monkey sadness\n-1951786153=bone dance\n1328851781=close_buttons,7\n939546513=forlorn_homestead\n-1019905269=shadowland\n-607416955=prayeron,10\n-2048535896=pheasant peasant\n-1025233830=monkey madness\n1121125956=scape soft\n-1562452687=etcetera\n1969878996=emotes,47\n1969878994=emotes,45\n-1228279872=ge_icons,3\n1969878995=emotes,46\n-1228279871=ge_icons,4\n1969878992=emotes,43\n-1228279870=ge_icons,5\n1969878993=emotes,44\n1959803992=invback\n399048409=mage arena\n1969878990=emotes,41\n-47057524=lasting\n1969878991=emotes,42\n-1228279875=ge_icons,0\n-1228279874=ge_icons,1\n-1228279873=ge_icons,2\n-1860080918=inspiration\n1997757502=redstone2\n1997757503=redstone3\n-1060046352=tribal2\n1997757501=redstone1\n-1701556829=magicoff2,29\n1890607210=magicon2,37\n1890607211=magicon2,38\n1890607212=magicon2,39\n-1408684838=ascent\n-848436598=fishing\n-119984250=combaticons2,17\n-119984251=combaticons2,16\n1769177816=jungle island\n-119984252=combaticons2,15\n-119984253=combaticons2,14\n-119984254=combaticons2,13\n-119984255=combaticons2,12\n-119984256=combaticons2,11\n-119984257=combaticons2,10\n1890607205=magicon2,32\n1890607206=magicon2,33\n1890607207=magicon2,34\n1890607208=magicon2,35\n135141185=zeah_combat\n1890607209=magicon2,36\n-180851958=norse code\n112903447=water\n922007495=talking forest\n-672706748=miracle dance\n-1110089645=lament\n-1237461365=grotto\n1890607203=magicon2,30\n1890607204=magicon2,31\n-1073910849=mirror\n-988841056=still night\n-1857025509=sunburn\n-468596910=easter jig\n796868952=major miner\n-1066798491=trawler\n1640556978=wonderous\n-1624274920=emperor\n740093634=find my way\n1890607238=magicon2,44\n1890607239=magicon2,45\n3559837=tick\n-1839713245=sideicons\n1029455878=hells bells\n1890607234=magicon2,40\n1890607235=magicon2,41\n1890607236=magicon2,42\n1890607237=magicon2,43\n1503566841=forbidden\n-895763669=spooky\n-276138668=ham attack\n500433071=combaticons,5\n-1021014225=catch me if you can\n500433070=combaticons,4\n500433073=combaticons,7\n500433072=combaticons,6\n500433075=combaticons,9\n1533565119=mind over matter\n500433074=combaticons,8\n2025958358=emotes_locked,4\n500433066=combaticons,0\n2025958357=emotes_locked,3\n862821975=far away\n-1228392498=artistry\n2025958356=emotes_locked,2\n500433068=combaticons,2\n2025958355=emotes_locked,1\n500433067=combaticons,1\n1085444827=refresh\n500433069=combaticons,3\n2025958359=emotes_locked,5\n1427043851=on the up\n2025958354=emotes_locked,0\n1316697938=whistle\n347955347=venture\n1959211608=mapfunction,77\n1959211609=mapfunction,78\n881850881=the chosen\n1959211601=mapfunction,70\n1959211602=mapfunction,71\n584643951=lost soul\n582140282=rising damp\n1959211603=mapfunction,72\n1740872686=soulfall\n1959211604=mapfunction,73\n1959211605=mapfunction,74\n-119984248=combaticons2,19\n1959211606=mapfunction,75\n-119984249=combaticons2,18\n1609255038=slither and thither\n82917947=sarim's vermin\n1959211607=mapfunction,76\n1728911401=natural\n-1189743137=duel arena\n108698078=roof2\n214634021=head to head\n2025958361=emotes_locked,7\n-448773288=isle of everywhere\n2025958360=emotes_locked,6\n-1869996941=titlebox\n-338347745=showdown\n-2075972251=long ago\n2025958363=emotes_locked,9\n2025958362=emotes_locked,8\n-1487589606=7th realm\n-1253087691=garden\n-2133902017=zeah_farming\n-492926285=impetuous\n3314014=lair\n907815588=the desert\n-1960860275=barbarianism\n1890607241=magicon2,47\n1890607242=magicon2,48\n1890607243=magicon2,49\n-919642451=jungle bells\n795515487=underground\n561438836=fountain\n-1418827919=illusive\n-634763748=fruits de mer\n1890607240=magicon2,46\n1694458038=large_button\n1393517697=bandit camp\n1959211632=mapfunction,80\n1884773718=magicoff2,2\n1884773719=magicoff2,3\n1884773716=magicoff2,0\n2121900771=backtop1\n1884773717=magicoff2,1\n-103077377=gnomeball\n-1947119982=blistering barnacles\n828650857=autumn voyage\n92909147=alone\n1691516951=undead dungeon\n122265833=expecting\n-1320617626=dunjun\n1959211633=mapfunction,81\n1959211634=mapfunction,82\n1959211635=mapfunction,83\n1959211636=mapfunction,84\n1959211637=mapfunction,85\n777534707=army of darkness\n1959211638=mapfunction,86\n1959211639=mapfunction,87\n1884773721=magicoff2,5\n1884773722=magicoff2,6\n1884773720=magicoff2,4\n1959211640=mapfunction,88\n1959211641=mapfunction,89\n-327707013=anywhere\n1884773725=magicoff2,9\n1884773723=magicoff2,7\n1884773724=magicoff2,8\n1116844876=incantation\n-728886272=temple of light\n685934899=in the clink\n-1237289460=grumpy\n1945133711=inferno\n466902883=strange place\n-418223472=phasmatys\n817472004=zombiism\n106578554=zeah_book,0\n106578555=zeah_book,1\n1509070203=eagle peak\n-485932799=expedition\n1171923143=emotes,8\n-675357975=attack1\n1171923144=emotes,9\n1959211610=mapfunction,79\n-675357974=attack2\n-675357973=attack3\n-675357972=attack4\n-675357971=attack5\n40246002=masquerade\n-675357970=attack6\n-734206983=arrival\n-1980407601=sea shanty xmas\n284435223=pharoah's tomb\n-148552909=down below\n1171923141=emotes,6\n1171923142=emotes,7\n1171923140=emotes,5\n-1077789440=mellow\n-710537653=kingdom\n1171923138=emotes,3\n1171923139=emotes,4\n1171923136=emotes,1\n-2098286081=venture2\n1171923137=emotes,2\n1171923135=emotes,0\n-1094248165=sigmunds showdown\n-271106892=rat a tat tat\n3288564=keys\n-143163121=ham fisted\n-900633031=medieval\n944208821=life's a beach\\!\n-1228279453=riverside\n-1666444059=combaticons,10\n825919125=options_icons,24\n825919126=options_icons,25\n1179379180=the trade parade\n-1666444057=combaticons,12\n825919123=options_icons,22\n-1666444058=combaticons,11\n825919124=options_icons,23\n825919121=options_icons,20\n1884768169=magicoff,32\n825919122=options_icons,21\n1884768167=magicoff,30\n1884768168=magicoff,31\n1318818808=chainmail\n582031337=intrepid\n783525419=beetle juice\n432605856=untouchable\n-969918857=neverland\n79789174=narnode's theme\n-705938181=zealot\n117588=web\n-1666444051=combaticons,18\n-1666444052=combaticons,17\n1687654733=troubled\n-1666444050=combaticons,19\n-1666444055=combaticons,14\n825919129=options_icons,28\n-1666444056=combaticons,13\n-1666444053=combaticons,16\n825919127=options_icons,26\n-1666444054=combaticons,15\n825919128=options_icons,27\n1320694328=magical journey\n364185053=roll the bones\n-1254483584=jungly1\n981183822=right on track\n-1254483583=jungly2\n-1254483582=jungly3\n3075958=dark\n-2038936746=deep down\n1512143976=everlasting fire\n-1392319985=beyond\n46273615=tale of keldagrim\n-651951461=goblin game\n3522941=save\n104084791=mossy\n1250935993=the monsters below\n794539501=garden of summer\n1814277765=elven mist\n2110556093=the golem\n-1475251658=where eagles lair\n1884768143=magicoff,27\n1884768144=magicoff,28\n1884768141=magicoff,25\n1529837717=bubble and squeak\n1884768142=magicoff,26\n-1679325940=technology\n1884768145=magicoff,29\n-826562194=troubled_waters\n1884768140=magicoff,24\n-1359348243=painting1\n1267356434=the power of tears\n-1359348242=painting2\n-860755690=jungle hunt\n1884768138=magicoff,22\n-1197347961=magic magic magic\n1884768139=magicoff,23\n1134405764=hypnotized\n1959211539=mapfunction,50\n1381363755=my arms journey\n-1644401602=complication\n1959211540=mapfunction,51\n1959211541=mapfunction,52\n1884768136=magicoff,20\n1959211542=mapfunction,53\n2111304827=warning_icons,0\n1884768137=magicoff,21\n-440187560=zogre dance\n1959211543=mapfunction,54\n2111304828=warning_icons,1\n-2002535437=corridors of power\n825919130=options_icons,29\n580384095=jungle troubles\n1301622585=slice of station\n1959211544=mapfunction,55\n2111304829=warning_icons,2\n-1294172031=escape\n-1309477156=expanse\n-1526067851=alternative root\n2124773424=dynasty\n1743765602=leftarrow\n-1482676188=romancing the crone\n-1891851953=island of the trolls\n736457293=small_button_pressed\n-1106172890=letter\n986170990=dreamstate\n1959211545=mapfunction,56\n1959211546=mapfunction,57\n1765722413=spirits of elid\n1959211547=mapfunction,58\n1959211548=mapfunction,59\n-2075333010=lonesome\n3314400=lava\n1355033875=worldmap_icon,1\n1814357716=knightmare\n1690742645=nox_irae\n94935104=cross\n-1249495153=frogland\n-1642689926=athletes foot\n107944162=quest\n1355033874=worldmap_icon,0\n-2130741313=joy of the hunt\n-28982081=labyrinth\n250959119=marooned\n-1522984472=altar_ego\n1326424637=the lost melody\n-1779111734=arabique\n-398925062=sea shanty2\n1884768110=magicoff,15\n1884768111=magicoff,16\n1817249074=woe of the wyvern\n1884768114=magicoff,19\n1884768112=magicoff,17\n-1624760229=emotion\n1884768113=magicoff,18\n-353951458=attention\n279431252=garden of autumn\n422652266=small_button\n1884768107=magicoff,12\n1884768108=magicoff,13\n375695247=the far side\n1884768105=magicoff,10\n1884768106=magicoff,11\n-528864109=crystal sword\n1884768109=magicoff,14\n1959211570=mapfunction,60\n-158141423=prayeron,7\n-158141424=prayeron,6\n1959211571=mapfunction,61\n-158141421=prayeron,9\n1959211572=mapfunction,62\n-158141422=prayeron,8\n1959211573=mapfunction,63\n1959211574=mapfunction,64\n688840255=piscarilius_sigil\n1959211575=mapfunction,65\n1959211576=mapfunction,66\n1170407052=headicons_prayer\n1959211577=mapfunction,67\n-324496873=soundscape\n-1418445703=tex_red\n1961540869=wornicons,10\n-1028580907=that_sullen_hall\n397136995=elfpainting\n-2092714094=haunted mine\n1959211578=mapfunction,68\n1959211579=mapfunction,69\n-158141429=prayeron,1\n-158141427=prayeron,3\n-140492390=bunny_sugar_rush\n-158141428=prayeron,2\n-158141425=prayeron,5\n1827366203=righteousness\n-649601274=darkness_in_the_depths\n-158141426=prayeron,4\n1961540870=wornicons,11\n-158141430=prayeron,0\n910299584=principality\n-734028978=arrow,1\n-734028979=arrow,0\n790067275=garden of spring\n35762567=workshop\n-1095396929=competition\n96463963=egypt\n-1154441378=jollyr\n-1685231711=cave background\n-2078908549=time out\n-1172405897=wildwood\n-170561624=spookyjungle\n2110231453=mod_icons\n2061491048=shining\n-1063411723=tremble\n94627585=chest\n-520702427=ice melody\n1346720899=backleft1\n-607416919=prayeron,25\n-607416918=prayeron,26\n900197712=staticons,6\n-607416917=prayeron,27\n900197713=staticons,7\n-607416916=prayeron,28\n900197710=staticons,4\n-607416915=prayeron,29\n900197711=staticons,5\n957931606=courage\n900197714=staticons,8\n900197715=staticons,9\n-720253066=the other side\n-1655721374=prayeroff,31\n-607416921=prayeron,23\n-1655721375=prayeroff,30\n-607416920=prayeron,24\n-710515142=the mad mole\n-1350228392=stratosphere\n-1666438445=combaticons2,3\n-1666438446=combaticons2,2\n-1666438443=combaticons2,5\n-1666438444=combaticons2,4\n-1106570438=legion\n-1666438441=combaticons2,7\n-1666438442=combaticons2,6\n1398587265=flute salad\n-1666438440=combaticons2,8\n837223705=mapedge\n900197709=staticons,3\n-243680393=peng_emotes,7\n900197707=staticons,1\n378300078=everlasting\n900197708=staticons,2\n-944748869=witching\n-243680396=peng_emotes,4\n-1335336992=logo_deadman_mode\n-1368714737=small_button_blue\n-795140435=wander\n-243680397=peng_emotes,3\n-243680394=peng_emotes,6\n-1666438447=combaticons2,1\n-243680395=peng_emotes,5\n-1666438448=combaticons2,0\n-243680398=peng_emotes,2\n-243680399=peng_emotes,1\n-607416924=prayeron,20\n900197706=staticons,0\n-607416923=prayeron,21\n-607416922=prayeron,22\n3016376=bark\n-89244313=romper chomper\n346288985=dorgeshun deep\n1585002399=magicon,21\n-1725263140=chef surprize\n1585002398=magicon,20\n-993528987=making waves\n-628963539=ham and seek\n-1666438439=combaticons2,9\n-333224315=baroque\n-1236252722=prime time\n280241284=waking dream\n-564582358=distant land\n115411843=castlewars\n1513246078=al kharid\n-1377700863=unknown land\n1264132816=miles away\n1185785872=barbassault_icons,3\n1185785873=barbassault_icons,4\n-1655721397=prayeroff,29\n-1655721398=prayeroff,28\n1185785874=barbassault_icons,5\n-1655721399=prayeroff,27\n1185785875=barbassault_icons,6\n1711341885=fight or flight\n1185785870=barbassault_icons,1\n1185785871=barbassault_icons,2\n3530505=sire\n-309570839=pick_and_shovel\n647234089=distillery hilarity\n-127408236=gnome_village_party\n1337378554=backbase2\n109757537=stars\n1337378553=backbase1\n109757538=start\n819884325=wilderness3\n1328851775=close_buttons,1\n658759958=side_background\n1328851774=close_buttons,0\n-782211141=wonder\n1328851777=close_buttons,3\n-1995718284=wall_white\n819884324=wilderness2\n1328851776=close_buttons,2\n1328851779=close_buttons,5\n-943885542=scape hunter\n1328851778=close_buttons,4\n1185785869=barbassault_icons,0\n-607416949=prayeron,16\n-607416948=prayeron,17\n-1779684630=rune essence\n-607416947=prayeron,18\n-607416946=prayeron,19\n1119460311=bandos battalion\n-967559823=creature cruelty\n-1904094243=zeah_fishing\n-1913214770=wilderness\n460367020=village\n825919161=options_icons,39\n1585002375=magicon,18\n94839810=coins\n1585002376=magicon,19\n825919160=options_icons,38\n-1282090556=faerie\n-521895311=the adventurer\n788399136=tree spirits\n-1902858744=beneath_the_stronghold\n-356730043=pirates of penance\n1185785876=barbassault_icons,7\n825919158=options_icons,36\n825919159=options_icons,37\n825919156=options_icons,34\n-1455241861=victory is mine\n825919157=options_icons,35\n-1333874720=side_icons,17\n825919154=options_icons,32\n825919155=options_icons,33\n825919152=options_icons,30\n825919153=options_icons,31\n1437805631=chatback\n-1623296531=ground scape\n685190118=in the brine\n1366257555=nightfall\n110327241=theme\n-1333874725=side_icons,12\n-1333874726=side_icons,11\n-1333874727=side_icons,10\n-1333874721=side_icons,16\n-1333874722=side_icons,15\n-1124681475=darkly_altared\n-8976533=throne of the demon\n-1333874723=side_icons,14\n-1333874724=side_icons,13\n-1989106719=assault and battery\n1958759012=greatness\n1057075019=b12_full\n1143353537=chain of command\n-51091830=desert voyage\n-1073927447=mirage\n-91048728=number_button\n3165239=gaol\n-1380919269=breeze\n445640248=rugged_terrain\n106079=key\n-655784411=overlay_multiway\n-1025835715=backright2\n-1025835716=backright1\n1120933843=scape main\n3225350=iban\n-956253112=title fight\n-123912401=la mort\n1585002367=magicon,10\n-2128736428=startgame\n1585002368=magicon,11\n-925031874=royale\n1585002369=magicon,12\n1585002370=magicon,13\n1585002373=magicon,16\n1585002374=magicon,17\n-1307116191=superstition\n1585002371=magicon,14\n1585002372=magicon,15\n-587569902=path of peril\n3392903=null\n-1601127242=inadequacy\n2136330800=staticons2,0\n2136330801=staticons2,1\n344336468=grip of the talon\n2136330804=staticons2,4\n2136330805=staticons2,5\n2136330802=staticons2,2\n2136330803=staticons2,3\n1960215130=barking mad\n-43712789=scape original\n621171714=cellar song\n1585002461=magicon,41\n1585002462=magicon,42\n1585002460=magicon,40\n111485446=upass\n2136330808=staticons2,8\n694847251=in the manor\n2136330809=staticons2,9\n2136330806=staticons2,6\n2136330807=staticons2,7\n-1385847955=rightarrow\n1343200077=the slayer\n1585002465=magicon,45\n1585002466=magicon,46\n1585002463=magicon,43\n1585002464=magicon,44\n1585002469=magicon,49\n-95571520=volcanic vikings\n1585002467=magicon,47\n1585002468=magicon,48\n-1032629963=shipwrecked\n93330745=aztec\n-881372797=tabs,1\n1377351472=oriental\n121641580=headicons_hint\n-881372798=tabs,0\n1585002438=magicon,39\n-1661605940=elfwood\n-607416893=prayeron,30\n-822106577=jungle island xmas\n-607416892=prayeron,31\n332368736=mad eadgar\n1585002432=magicon,33\n-143368781=side_background_right\n1585002433=magicon,34\n-1081494434=malady\n1585002430=magicon,31\n1585002431=magicon,32\n1585002436=magicon,37\n1585002437=magicon,38\n1585002434=magicon,35\n1585002435=magicon,36\n-1938171360=miscgraphics3,0\n-1059680853=trinity\n781557721=dies_irae\n-1938171359=miscgraphics3,1\n497375231=stillness\n-1938171358=miscgraphics3,2\n-1938171357=miscgraphics3,3\n-960709976=dogs of war\n2129339089=magicon,1\n2129339088=magicon,0\n755433248=headicons_pk\n108392383=regal\n-213632750=waterfall\n-1367706280=canvas\n73828649=settlement\n848123561=into the abyss\n478781900=last stand\n1339486127=the shadow\n-1055503808=roc and roll\n837204902=mapdots\n950484242=compass\n-1082154559=fanfare\n747848680=nether_realm\n788224888=dead quiet\n1532279978=monarch waltz\n-149029727=side_background_left1\n812947089=fanfare2\n-149029726=side_background_left2\n1006643748=high spirits\n-2136059388=starlight\n2122572442=the tower\n-1998869913=spooky2\n1411067174=gnome village2\n-2065077267=wild side\n812947090=fanfare3\n1585002429=magicon,30\n1294629755=on the wing\n2097127567=monkey badness\n-2032107216=sojourn\n1020264019=pest control\n3237038=info\n1473393027=fe fi fo fum\n-1686202291=upper_depths\n3540994=stop\n1742080803=darkwood\n740392969=little cave of horrors\n-158379532=prayerglow\n-691855347=in between\n-200702983=the noble rodent\n1652745754=forgotten\n-1895307673=hitmark,3\n-1895307674=hitmark,2\n-1895307675=hitmark,1\n-1895307676=hitmark,0\n-1895307670=hitmark,6\n-1895307671=hitmark,5\n-1895307672=hitmark,4\n1968917071=bone dry\n-850506182=trawler minor\n197029040=mapscene\n-808772318=in the pits\n-1165315580=looking back\n-1763090403=scape_ape\n-1938177931=miscgraphics,11\n-1938177932=miscgraphics,10\n1936130561=thrall_of_the_serpent\n1258863383=yesteryear\n1994744000=slice of silent movie\n-1691854169=dead can dance\n1585002407=magicon,29\n1585002405=magicon,27\n1585002406=magicon,28\n271319484=frostbite\n-499867199=meridian\n1585002400=magicon,22\n-1938177930=miscgraphics,12\n-84626226=mudskipper melody\n1585002403=magicon,25\n1585002404=magicon,26\n3641802=wall\n1585002401=magicon,23\n1585002402=magicon,24\n-606457701=wolf mountain\n1276599785=button_brown\n1969878899=emotes,13\n1969878897=emotes,11\n1969878898=emotes,12\n1969878896=emotes,10\n-1938177928=miscgraphics,14\n-907669678=brew hoo hoo\n-1938177929=miscgraphics,13\n72999866=subterranea\n619237947=the galleon\n-1764950404=scape sad\n295831445=heart and mind\n908430134=dangerous road\n738888631=tradebacking\n-174800339=verdana_11pt_regular\n686705631=lightwalk\n-601591436=side_background_bottom\n-1479412376=the navigator\n-359173459=zamorak zoo\n744536246=null and void\n-1701556798=magicoff2,39\n-1701556799=magicoff2,38\n-1396384012=bamboo\n-200388662=lighthouse\n133626717=suspicious\n-810515425=voyage\n3061973=crag\n1802291895=big chords\n-1661619479=elfwall\n113315621=wood2\n1813041183=steelborder2,0\n429244831=slug a bug ball\n1813041184=steelborder2,1\n-1658386264=shining_spirit\n738909086=chamber\n526264239=verdana_13pt_regular\n-877351859=temple\n2142215577=the mollusc menace\n1124498189=warpath\n-2136649922=no way out\n-339706871=grimly_fiendish\n547534551=wrath and ruin\n544229147=lore and order\n3327206=load\n1610073470=lovakengj_sigil\n-419218284=long way home\n-662489856=food for thought\n1306461568=stagnant\n-1662171955=elfdoor\n-1043985601=meddling kids\n947464074=titlebutton\n-1309055712=exposed\n-1487348923=ambient jungle\n-1829469821=lament of meiyerditch\n233203434=leftarrow_small\n-1216167350=dangerous\n114464611=railings\n-1106574323=legend\n-1701556767=magicoff2,49\n-999707515=time to mine\n-1701556768=magicoff2,48\n2129339097=magicon,9\n2129339096=magicon,8\n1959211510=mapfunction,42\n2129339095=magicon,7\n1959211511=mapfunction,43\n2129339094=magicon,6\n2129339093=magicon,5\n2129339092=magicon,4\n2129339091=magicon,3\n2129339090=magicon,2\n3522472=saga\n-1701556769=magicoff2,47\n-544722449=rellekka\n1033441676=tribal background\n1915718129=the desolate isle\n1890607150=magicon2,19\n1787618597=stranded\n1717999087=forgettable melody\n1959211512=mapfunction,44\n-243680400=peng_emotes,0\n1959211513=mapfunction,45\n1959211514=mapfunction,46\n1959211515=mapfunction,47\n1959211516=mapfunction,48\n1959211517=mapfunction,49\n1705947058=the cellar dwellers\n1216634785=landlubber\n1884768198=magicoff,40\n-1588113323=the rogues den\n1884768199=magicoff,41\n-905842564=serene\n-607599698=prayeroff,2\n-607599699=prayeroff,1\n1890607142=magicon2,11\n1389384362=monkey trouble\n1890607143=magicon2,12\n1890607144=magicon2,13\n1890607145=magicon2,14\n1890607146=magicon2,15\n1890607147=magicon2,16\n1890607148=magicon2,17\n1966766798=mausoleum\n1890607149=magicon2,18\n1808345541=armadyl alliance\n290391725=options_slider,7\n1890607141=magicon2,10\n290391722=options_slider,4\n-607599696=prayeroff,4\n290391721=options_slider,3\n-607599697=prayeroff,3\n290391724=options_slider,6\n-607599694=prayeroff,6\n290391723=options_slider,5\n-607599695=prayeroff,5\n-607599692=prayeroff,8\n-607599693=prayeroff,7\n290391720=options_slider,2\n-607599691=prayeroff,9\n1334775925=chat_background\n-1779127378=arabian2\n-1779127377=arabian3\n528722471=island life\n1890607175=magicon2,23\n1890607176=magicon2,24\n949634504=mouse trap\n1890607177=magicon2,25\n1890607178=magicon2,26\n1890607179=magicon2,27\n3327403=logo\n290391719=options_slider,1\n1092249049=storm brew\n290391718=options_slider,0\n404357804=everywhere\n1890607172=magicon2,20\n1890607173=magicon2,21\n951530772=contest\n1890607174=magicon2,22\n-1701556776=magicoff2,40\n1884768176=magicoff,39\n-395250469=corporal punishment\n1749113330=newbie melody\n1884768174=magicoff,37\n1884768175=magicoff,38\n-1701556772=magicoff2,44\n-1701556773=magicoff2,43\n-1701556774=magicoff2,42\n-1701556775=magicoff2,41\n-858121616=tzhaar\n1884768172=magicoff,35\n1884768173=magicoff,36\n666772244=combat_shield\n1884768170=magicoff,33\n1884768171=magicoff,34\n1959211509=mapfunction,41\n1639695510=mapmarker\n-1661748240=friends_icons\n-552301350=knightly\n-1918044851=mastermindless\n-1701556770=magicoff2,46\n-1701556771=magicoff2,45\n201526300=corporealbeast\n1959211508=mapfunction,40\n105001967=nomad\n-70910145=clickcross,3\n-1737914947=mapfunction,5\n1801745440=staticons2,11\n-70910146=clickcross,2\n-865479038=tribal\n-1737914946=mapfunction,6\n1801745441=staticons2,12\n-70910147=clickcross,1\n-1737914945=mapfunction,7\n1801745442=staticons2,13\n-70910148=clickcross,0\n-1737914944=mapfunction,8\n1801745443=staticons2,14\n-1737914943=mapfunction,9\n1801745444=staticons2,15\n1801745445=staticons2,16\n1801745446=staticons2,17\n-1877545169=land down under\n93921962=books\n-1737914949=mapfunction,3\n-1737914948=mapfunction,4\n-1655721428=prayeroff,19\n-2136884405=title.jpg\n-1655721429=prayeroff,18\n1584819628=magicoff,6\n1584819629=magicoff,7\n437480876=voodoo cult\n1584819624=magicoff,2\n1584819625=magicoff,3\n1584819626=magicoff,4\n-1737914952=mapfunction,0\n1584819627=magicoff,5\n-1737914951=mapfunction,1\n124995564=harmony2\n1584819622=magicoff,0\n1801745439=staticons2,10\n1584819623=magicoff,1\n346263512=dorgeshun city\n-1665011705=down and out\n1890607180=magicon2,28\n1890607181=magicon2,29\n1417471781=titlescroll\n1959211446=mapfunction,20\n1956141536=options_radio_buttons,0\n736568812=ballad of enchantment\n-1737914950=mapfunction,2\n1959211447=mapfunction,21\n1959211448=mapfunction,22\n1959211449=mapfunction,23\n-1890130256=morytania\n-70910141=clickcross,7\n-70910142=clickcross,6\n1956141539=options_radio_buttons,3\n-70910143=clickcross,5\n1956141538=options_radio_buttons,2\n284766976=splendour\n-70910144=clickcross,4\n1956141537=options_radio_buttons,1\n196677638=the quizmaster\n530068296=overture\n-1123094568=sl_button\n-700552779=hosidius_sigil\n-614076819=sad meadow\n1956141543=options_radio_buttons,7\n1956141542=options_radio_buttons,6\n1956141541=options_radio_buttons,5\n1956141540=options_radio_buttons,4\n1584819631=magicoff,9\n1846633612=gnome village\n-2128560371=sl_back\n1969878905=emotes,19\n306819362=crystal castle\n1584819630=magicoff,8\n1969878903=emotes,17\n303737220=options_icons,7\n1969878904=emotes,18\n1969878901=emotes,15\n-78220817=devils_may_care\n1969878902=emotes,16\n-40521666=dimension x\n1969878900=emotes,14\n673424924=the lunar isle\n789609582=brimstail's scales\n303737222=options_icons,9\n303737221=options_icons,8\n1959211415=mapfunction,10\n3059343=coil\n1959211416=mapfunction,11\n1959211417=mapfunction,12\n-1256560486=last_man_standing\n1959211418=mapfunction,13\n336238005=rightarrow_small\n1959211419=mapfunction,14\n-677662361=forever\n-1655721430=prayeroff,17\n-1655721431=prayeroff,16\n-1655721432=prayeroff,15\n1959211420=mapfunction,15\n1959211663=mapfunction,90\n1959211421=mapfunction,16\n1959211422=mapfunction,17\n1959211423=mapfunction,18\n303737217=options_icons,4\n303737216=options_icons,3\n303737215=options_icons,2\n-1665005042=funny bunnies\n303737214=options_icons,1\n303737219=options_icons,6\n303737218=options_icons,5\n303737213=options_icons,0\n95997798=we are the fairies\n2001751835=desert heat\n1959211424=mapfunction,19\n-1655721437=prayeroff,10\n687938017=clanwars\n-1776024210=desolate_mage\n-650944128=strength of saradomin\n-1655721433=prayeroff,14\n1160873524=aye car rum ba\n-1655721434=prayeroff,13\n-1655721435=prayeroff,12\n-1081314499=marble\n-1655721436=prayeroff,11\n1097075475=reset,0\n1959211477=mapfunction,30\n-693313916=warriors guild\n3506388=roof\n1959211478=mapfunction,31\n1097075476=reset,1\n-2134967800=dagannoth dawn\n-985763247=planks\n1959211479=mapfunction,32\n1999746381=fenkenstrain's refrain\n898010371=garden of winter\n359174830=rat hunt\n1959211482=mapfunction,35\n686441581=lightness\n1959211483=mapfunction,36\n1959211484=mapfunction,37\n1959211485=mapfunction,38\n1959211486=mapfunction,39\n2023201035=dwarf theme\n1959211480=mapfunction,33\n1959211481=mapfunction,34\n-1065532022=combatboxes,1\n-1065532021=combatboxes,2\n-1065532020=combatboxes,3\n1867160429=old_tiles\n394756979=scape santa\n25205919=elfroof2\n-663428071=dangerous way\n-1065532023=combatboxes,0\n1959211450=mapfunction,24\n-895939599=spirit\n1959211451=mapfunction,25\n1959211452=mapfunction,26\n1959211453=mapfunction,27\n1959211454=mapfunction,28\n1959211455=mapfunction,29\n-275310687=undercurrent\n212205923=goblin village\n-303898981=faithless\n-1381531001=tomb raider\n260940912=marzipan\n1343267530=backhmid1\n1343267531=backhmid2\n1097468315=horizon\n-1655721404=prayeroff,22\n623451622=kourend_the_magnificent\n-1655721405=prayeroff,21\n-1655721406=prayeroff,20\n-1655721400=prayeroff,26\n-313384067=p12_full\n-1655721401=prayeroff,25\n-1655721402=prayeroff,24\n-1655721403=prayeroff,23\n95848451=dream\n1966781751=maws_jaws_claws\n-995428255=parade\n95734525=method of madness\n-1308064877=hitmarks\n1030045177=mutant medley\n1333034828=blackmark\n851641665=davy jones locker\n417793574=scrollbar\n1346720900=backleft2\n1884768206=magicoff,48\n1884768207=magicoff,49\n1801140808=fangs for the memory\n1884768204=magicoff,46\n1345432055=pinball wizard\n1884768205=magicoff,47\n-783693496=dance of the undead\n1274780903=chompy hunt\n465278529=the lost tribe\n-1666437481=combaticons3,6\n2032696205=cabin fever\n-1666437480=combaticons3,7\n1825640471=borderland\n415928477=zeah_mining\n1884768202=magicoff,44\n-607599700=prayeroff,0\n1884768203=magicoff,45\n1884768200=magicoff,42\n1884768201=magicoff,43\n-1666437487=combaticons3,0\n-1666437486=combaticons3,1\n813726263=crystal cave\n1235442953=pathways\n518814479=lullaby\n-1666437483=combaticons3,4\n1585007985=magicon2,7\n-1666437482=combaticons3,5\n1585007986=magicon2,8\n-1666437485=combaticons3,2\n1585007987=magicon2,9\n-1666437484=combaticons3,3\n104080482=moody\n1969878967=emotes,39\n-665666447=work work work\n1364992651=evil bobs island\n1969878965=emotes,37\n1227328817=verdana_15pt_regular\n1969878966=emotes,38\n1969878963=emotes,35\n1581724013=monkey business\n1969878964=emotes,36\n-74307138=miscellania\n1969878961=emotes,33\n1969878962=emotes,34\n1969878960=emotes,32\n1131171307=wayward\n-1154558416=sl_arrows\n529929957=overpass\n1258058669=huffman\n-694094064=adventure\n1171698653=orb_xp,1\n1171698652=orb_xp,0\n1171698655=orb_xp,3\n1171698654=orb_xp,2\n1802171733=arceuus_sigil\n221109227=tears of guthix\n-1661754893=elfroof\n-1666437478=combaticons3,9\n1124565314=warrior\n1969878958=emotes,30\n1969878959=emotes,31\n-934797897=reggae\n2110260221=the genie\n-1666437479=combaticons3,8\n104257585=mummy\n-1923924724=sworddecor,0\n2136325196=staticons,17\n-1332194002=background\n2136164423=homescape\n-1923924721=sworddecor,3\n2136325194=staticons,15\n-1923924722=sworddecor,2\n-873564465=tiptoe\n2136325195=staticons,16\n-1923924723=sworddecor,1\n123560953=espionage\n2136325192=staticons,13\n2136325193=staticons,14\n1038911415=gnome king\n2136325190=staticons,11\n2136325191=staticons,12\n1854274741=karamja jam\n1969878989=emotes,40\n-1242708793=glyphs\n563269755=the terrible tower\n1650323088=twilight\n-12868552=sea shanty\n289742397=book of spells\n1880989696=dragontooth island\n2136325189=staticons,10\n110873=pen\n-454421102=out of the deep\n825919099=options_icons,19\n825919097=options_icons,17\n825919098=options_icons,18\n-1349119470=cursed\n1585007978=magicon2,0\n825919095=options_icons,15\n1585007979=magicon2,1\n825919096=options_icons,16\n825919093=options_icons,13\n825919094=options_icons,14\n825919091=options_icons,11\n825919092=options_icons,12\n-895977880=sphinx\n-874529881=city of the dead\n825919090=options_icons,10\n1585007981=magicon2,3\n1585007982=magicon2,4\n1585007983=magicon2,5\n-1618729246=body parts\n1585007984=magicon2,6\n1585007980=magicon2,2\n1086075866=shayzien_sigil\n2103661451=jester minute\n-911346307=steelborder,2\n-1809781334=button_brown_big\n-911346306=steelborder,3\n-816227352=vision\n-911346309=steelborder,0\n109407595=shine\n-911346308=steelborder,1\n-119954464=combaticons3,12\n-119954465=combaticons3,11\n-1846853118=armageddon\n-119954462=combaticons3,14\n-119954463=combaticons3,13\n-119954460=combaticons3,16\n-908183966=scarab\n-119954461=combaticons3,15\n-282886672=home sweet home\n103666243=march\n1969878929=emotes,22\n1643875326=fire and brimstone\n1969878927=emotes,20\n-119954466=combaticons3,10\n1969878928=emotes,21\n-839455633=close quarters\n941457503=way of the enchanter\n-1453405761=mor-ul-rek\n-415134015=have a blast\n-1658514874=floating free\n1213477442=chickened out\n-1619800349=emotes_locked,27\n-599680631=fear and loathing\n-1081041422=insect queen\n-1268786147=forest\n-119954459=combaticons3,17\n-119954457=combaticons3,19\n-119954458=combaticons3,18\n-850395529=trouble brewing\n-1773920521=cave of the goblins\n-771284962=claustrophobia\n1310729739=bankbuttons,2\n1306691868=upcoming\n1465443077=over to nardah\n1310729744=bankbuttons,7\n1310729743=bankbuttons,6\n1310729742=bankbuttons,5\n1234827707=deep wildy\n-90350772=xenophobe\n-750127868=arabian\n1310729741=bankbuttons,4\n1310729740=bankbuttons,3\n1041911129=waterlogged\n108875897=runes\n1447063382=barb wire\n-378865792=magic dance\n285466503=overlay_duel\n1814287296=zeah_magic\n1976894499=down to earth\n1969878936=emotes,29\n1969878934=emotes,27\n-1567437308=deadlands\n1969878935=emotes,28\n1310729738=bankbuttons,1\n1969878932=emotes,25\n-957019274=too many cooks\n1310729737=bankbuttons,0\n2092627105=silence\n1969878933=emotes,26\n1969878930=emotes,23\n1969878931=emotes,24\n-901674570=well of voyage\n1787935731=quill_oblique_large\n-365283881=quill_caps_large\n1157777820=lunar_alphabet\n24702590=lunar_alphabet_lrg\n"
  },
  {
    "path": "config/server-config.example.json",
    "content": "{\n    \"configDir\": \"./config\",\n    \"cacheDir\": \"./cache\",\n\n    \"host\": \"0.0.0.0\",\n    \"port\": 43594,\n\n    \"updateServerHost\": \"0.0.0.0\",\n    \"updateServerPort\": 43592,\n\n    \"loginServerHost\": \"0.0.0.0\",\n    \"loginServerPort\": 43591,\n    \"rsaMod\": \"119568088839203297999728368933573315070738693395974011872885408638642676871679245723887367232256427712869170521351089799352546294030059890127723509653145359924771433131004387212857375068629466435244653901851504845054452735390701003613803443469723435116497545687393297329052988014281948392136928774011011998343\",\n    \"rsaExp\": \"12747337179295870166838611986189126026507945904720545965726999254744592875817063488911622974072289858092633084100280214658532446654378876853112046049506789703022033047774294965255097838909779899992870910011426403494610880634275141204442441976355383839981584149269550057129306515912021704593400378690444280161\",\n    \"encryptionEnabled\": true,\n    \"playerSavePath\": \"./data/saves\",\n\n    \"showWelcome\": true,\n    \"expRate\": 1,\n    \"giveAchievements\": true,\n    \"checkCredentials\": true,\n    \"tutorialEnabled\": false,\n    \"adminDropsEnabled\": true,\n    \"bypassTeleportRequirements\": false\n}\n"
  },
  {
    "path": "data/config/combat-styles.json",
    "content": "{\n    \"unarmed\": [\n        {\n            \"type\": \"crush\",\n            \"exp\": \"attack\",\n            \"anim\": \"punch\",\n            \"button_id\": 2\n        },\n        {\n            \"type\": \"crush\",\n            \"exp\": \"strength\",\n            \"anim\": \"kick\",\n            \"button_id\": 3\n        },\n        {\n            \"type\": \"crush\",\n            \"exp\": \"defence\",\n            \"anim\": \"punch\",\n            \"button_id\": 4\n        }\n    ],\n    \"axe\": [\n        {\n            \"type\": \"slash\",\n            \"exp\": \"attack\",\n            \"anim\": \"slash\",\n            \"button_id\": 2\n        },\n        {\n            \"type\": \"slash\",\n            \"exp\": \"strength\",\n            \"anim\": \"slash\",\n            \"button_id\": 5\n        },\n        {\n            \"type\": \"crush\",\n            \"exp\": \"strength\",\n            \"anim\": \"slash\",\n            \"button_id\": 4\n        },\n        {\n            \"type\": \"slash\",\n            \"exp\": \"defence\",\n            \"anim\": \"slash\",\n            \"button_id\": 3\n        }\n    ],\n    \"dagger\": [\n        {\n            \"type\": \"stab\",\n            \"exp\": \"attack\",\n            \"anim\": \"stab\",\n            \"button_id\": 2\n        },\n        {\n            \"type\": \"stab\",\n            \"exp\": \"strength\",\n            \"anim\": \"stab\",\n            \"button_id\": 3\n        },\n        {\n            \"type\": \"slash\",\n            \"exp\": \"strength\",\n            \"anim\": \"slash\",\n            \"button_id\": 4\n        },\n        {\n            \"type\": \"stab\",\n            \"exp\": \"defence\",\n            \"anim\": \"stab\",\n            \"button_id\": 5\n        }\n    ]\n}\n"
  },
  {
    "path": "data/config/examine-item-data.yaml",
    "content": "- id: 0 # Dwarf remains\n  examine: \"The body of a Dwarf savaged by Goblins.\"\n- id: 1 # Toolkit\n  examine: \"Good for repairing broken cannons.\"\n- id: 2 # Cannonball\n  examine: \"Ammo for the Dwarf Cannon.\"\n- id: 3 # Nulodion's notes\n  examine: \"Construction notes for Dwarf cannon ammo.\"\n- id: 4 # Ammo mould\n  examine: \"Used to make cannon ammunition.\"\n- id: 5 # Instruction manual\n  examine: \"An old note book.\"\n- id: 6 # Cannon base\n  examine: \"The cannon is built on this.\"\n- id: 8 # Cannon stand\n  examine: \"The mounting for the multicannon.\"\n- id: 10 # Cannon barrels\n  examine: \"The barrels of the multicannon.\"\n- id: 12 # Cannon furnace\n  examine: \"This powers the multicannon.\"\n- id: 14 # Railing\n  examine: \"A metal railing replacement.\"\n- id: 15 # Holy table napkin\n  examine: \"A cloth given to me by Sir Galahad.\"\n- id: 16 # Magic whistle\n  examine: \"A small tin whistle.\"\n- id: 17 # Grail bell\n  examine: \"I wonder what happens when I ring it?\"\n- id: 18 # Magic gold feather\n  examine: \"It will point the way for me.\"\n- id: 19 # Holy grail\n  examine: \"A holy and powerful artefact.\"\n- id: 20 # White cog\n  examine: \"A cog from some machinery.\"\n- id: 21 # Black cog\n  examine: \"A cog from some machinery.\"\n- id: 22 # Blue cog\n  examine: \"A cog from some machinery.\"\n- id: 23 # Red cog\n  examine: \"A cog from some machinery.\"\n- id: 24 # Rat poison\n  examine: \"Doesn't look very tasty.\"\n- id: 25 # Red vine worm\n  examine: \"Wormy.\"\n- id: 26 # Fishing trophy\n  examine: \"Hemenster fishing contest trophy.\"\n- id: 27 # Fishing pass\n  examine: \"Pass to the Hemenster fishing contest.\"\n- id: 28 # Insect repellent\n  examine: \"Drives away all known 6 legged creatures.\"\n- id: 30 # Bucket of wax\n  examine: \"It's a bucket of wax.\"\n- id: 32 # Lit black candle\n  examine: \"A lit spooky candle.\"\n- id: 33 # Lit candle\n  examine: \"A lit candle.\"\n- id: 35 # Excalibur\n  examine: \"This used to belong to King Arthur.\"\n- id: 36 # Candle\n  examine: \"A candle.\"\n- id: 38 # Black candle\n  examine: \"A spooky candle.\"\n- id: 39 # Bronze arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 40 # Iron arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 41 # Steel arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 42 # Mithril arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 43 # Adamant arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 44 # Rune arrowtips\n  examine: \"I can make some arrows with these.\"\n- id: 45 # Opal bolt tips\n  examine: \"Opal bolt tips.\"\n- id: 46 # Pearl bolt tips\n  examine: \"Pearl bolt tips.\"\n- id: 47 # Barb bolttips\n  examine: \"I can make bolts with these.\"\n- id: 48 # Longbow (u)\n  examine: \"I need to find a string for this.\"\n- id: 50 # Shortbow (u)\n  examine: \"I need to find a string for this.\"\n- id: 52 # Arrow shaft\n  examine: \"A wooden arrow shaft.\"\n- id: 53 # Headless arrow\n  examine: \"A wooden arrow shaft with flights attached.\"\n- id: 54 # Oak shortbow (u)\n  examine: \"An unstrung oak shortbow; I need a bowstring for this.\"\n- id: 56 # Oak longbow (u)\n  examine: \"An unstrung oak longbow; I need a bowstring for this.\"\n- id: 58 # Willow longbow (u)\n  examine: \"An unstrung willow longbow; I need a bowstring for this.\"\n- id: 60 # Willow shortbow (u)\n  examine: \"An unstrung willow shortbow; I need a bowstring for this.\"\n- id: 62 # Maple longbow (u)\n  examine: \"An unstrung maple longbow; I need a bowstring for this.\"\n- id: 64 # Maple shortbow (u)\n  examine: \"An unstrung maple shortbow; I need a bowstring for this.\"\n- id: 66 # Yew longbow (u)\n  examine: \"An unstrung yew longbow; I need a bowstring for this.\"\n- id: 68 # Yew shortbow (u)\n  examine: \"An unstrung yew shortbow; I need a bowstring for this.\"\n- id: 70 # Magic longbow (u)\n  examine: \"An unstrung magic longbow; I need a bowstring for this.\"\n- id: 72 # Magic shortbow (u)\n  examine: \"An unstrung magic shortbow; I need a bowstring for this.\"\n- id: 74 # Khazard helmet\n  examine: \"A helmet, as worn by the minions of General Khazard.\"\n- id: 75 # Khazard armour\n  examine: \"Armour, as worn by the minions of General Khazard.\"\n- id: 76 # Khazard cell keys\n  examine: \"These keys open the cells at the Khazard fight arena.\"\n- id: 77 # Khali brew\n  examine: \"A bottle of Khazard's worst brew.\"\n- id: 78 # Ice arrows\n  examine: \"Can only be fired from yew, magic, dark or twisted bows.\"\n- id: 83 # Lever\n  examine: \"A lever to open something perhaps?\"\n- id: 84 # Staff of armadyl\n  examine: \"The power in this staff causes it to vibrate gently.\"\n- id: 85 # Shiny key\n  examine: \"It catches the light!.\"\n- id: 86 # Pendant of lucien\n  examine: \"An amulet made by Lucien.\"\n- id: 87 # Armadyl pendant\n  examine: \"Worn by followers of Armadyl.\"\n- id: 88 # Boots of lightness\n  examine: \"Magic boots that make you lighter than normal.\"\n- id: 89 # Boots of lightness\n  examine: \"Magic boots that make you lighter than normal.\"\n- id: 90 # Child's blanket\n  examine: \"It's very soft!\"\n- id: 113 # Strength potion(4)\n  examine: \"4 doses of Strength potion.\"\n- id: 115 # Strength potion(3)\n  examine: \"3 doses of Strength potion.\"\n- id: 117 # Strength potion(2)\n  examine: \"2 doses of Strength potion.\"\n- id: 119 # Strength potion(1)\n  examine: \"1 dose of Strength potion.\"\n- id: 121 # Attack potion(3)\n  examine: \"3 doses of Attack potion.\"\n- id: 123 # Attack potion(2)\n  examine: \"2 doses of Attack potion.\"\n- id: 125 # Attack potion(1)\n  examine: \"1 dose of Attack potion.\"\n- id: 127 # Restore potion(3)\n  examine: \"3 doses of restore potion.\"\n- id: 129 # Restore potion(2)\n  examine: \"2 doses of restore potion.\"\n- id: 131 # Restore potion(1)\n  examine: \"1 dose of restore potion.\"\n- id: 133 # Defence potion(3)\n  examine: \"3 doses of Defence potion.\"\n- id: 135 # Defence potion(2)\n  examine: \"2 doses of Defence potion.\"\n- id: 137 # Defence potion(1)\n  examine: \"1 dose of Defence potion.\"\n- id: 139 # Prayer potion(3)\n  examine: \"3 doses of Prayer restore potion.\"\n- id: 141 # Prayer potion(2)\n  examine: \"2 doses of Prayer restore potion.\"\n- id: 143 # Prayer potion(1)\n  examine: \"1 dose of Prayer restore potion.\"\n- id: 145 # Super attack(3)\n  examine: \"3 doses of super Attack potion.\"\n- id: 147 # Super attack(2)\n  examine: \"2 doses of super Attack potion.\"\n- id: 149 # Super attack(1)\n  examine: \"1 dose of super Attack potion.\"\n- id: 151 # Fishing potion(3)\n  examine: \"3 doses of Fishing potion.\"\n- id: 153 # Fishing potion(2)\n  examine: \"2 doses of Fishing potion.\"\n- id: 155 # Fishing potion(1)\n  examine: \"1 dose of Fishing potion.\"\n- id: 157 # Super strength(3)\n  examine: \"3 doses of super Strength potion.\"\n- id: 159 # Super strength(2)\n  examine: \"2 doses of super Strength potion.\"\n- id: 161 # Super strength(1)\n  examine: \"1 dose of super Strength potion.\"\n- id: 163 # Super defence(3)\n  examine: \"3 doses of super Defence potion.\"\n- id: 165 # Super defence(2)\n  examine: \"2 doses of super Defence potion.\"\n- id: 167 # Super defence(1)\n  examine: \"1 dose of super Defence potion.\"\n- id: 169 # Ranging potion(3)\n  examine: \"3 doses of ranging potion.\"\n- id: 171 # Ranging potion(2)\n  examine: \"2 doses of ranging potion.\"\n- id: 173 # Ranging potion(1)\n  examine: \"1 dose of ranging potion.\"\n- id: 175 # Antipoison(3)\n  examine: \"3 doses of antipoison potion.\"\n- id: 177 # Antipoison(2)\n  examine: \"2 doses of antipoison potion.\"\n- id: 179 # Antipoison(1)\n  examine: \"1 dose of antipoison potion.\"\n- id: 181 # Superantipoison(3)\n  examine: \"3 doses of super antipoison potion.\"\n- id: 183 # Superantipoison(2)\n  examine: \"2 doses of super antipoison potion.\"\n- id: 185 # Superantipoison(1)\n  examine: \"1 dose of super antipoison potion.\"\n- id: 187 # Weapon poison\n  examine: \"For use on daggers and projectiles.\"\n- id: 189 # Zamorak brew(3)\n  examine: \"3 doses of Zamorak brew.\"\n- id: 191 # Zamorak brew(2)\n  examine: \"2 doses of Zamorak brew.\"\n- id: 193 # Zamorak brew(1)\n  examine: \"1 dose of Zamorak brew.\"\n- id: 195 # Potion\n  examine: \"This is meant to be good for spots.\"\n- id: 197 # Poison chalice\n  examine: \"A cup of a strange brew...\"\n- id: 221 # Eye of newt\n  examine: \"It seems to be looking at me.\"\n- id: 223 # Red spiders' eggs\n  examine: \"Ewww!\"\n- id: 225 # Limpwurt root\n  examine: \"The root of a limpwurt plant.\"\n- id: 227 # Vial of water\n  examine: \"A glass vial containing water.\"\n- id: 229 # Vial\n  examine: \"An empty glass vial.\"\n- id: 231 # Snape grass\n  examine: \"Strange spiky grass.\"\n- id: 233 # Pestle and mortar\n  examine: \"I can grind things for potions in this.\"\n- id: 235 # Unicorn horn dust\n  examine: \"Finely ground horn of Unicorn.\"\n- id: 237 # Unicorn horn\n  examine: \"This horn has restorative properties.\"\n- id: 239 # White berries\n  examine: \"Sour berries, used in potions.\"\n- id: 241 # Dragon scale dust\n  examine: \"Finely ground scale of Dragon.\"\n- id: 243 # Blue dragon scale\n  examine: \"A large shiny scale.\"\n- id: 245 # Wine of zamorak\n  examine: \"An evil wine for an evil god.\"\n- id: 247 # Jangerberries\n  examine: \"They don't look very ripe.\"\n- id: 249 # Guam leaf\n  examine: \"A bitter green herb.\"\n- id: 251 # Marrentill\n  examine: \"A herb used in poison cures.\"\n- id: 253 # Tarromin\n  examine: \"A useful herb.\"\n- id: 255 # Harralander\n  examine: \"A useful herb.\"\n- id: 257 # Ranarr weed\n  examine: \"A useful herb.\"\n- id: 259 # Irit leaf\n  examine: \"A useful herb.\"\n- id: 261 # Avantoe\n  examine: \"A useful herb.\"\n- id: 263 # Kwuarm\n  examine: \"A powerful herb.\"\n- id: 265 # Cadantine\n  examine: \"A powerful herb.\"\n- id: 267 # Dwarf weed\n  examine: \"A powerful herb.\"\n- id: 269 # Torstol\n  examine: \"A powerful herb.\"\n- id: 271 # Pressure gauge\n  examine: \"It looks like part of a machine.\"\n- id: 272 # Fish food\n  examine: \"Keeps your pet fish strong and healthy.\"\n- id: 273 # Poison\n  examine: \"This stuff looks nasty.\"\n- id: 274 # Poisoned fish food\n  examine: \"Doesn't seem very nice to the poor fishes.\"\n- id: 275 # Key\n  examine: \"A slightly smelly key.\"\n- id: 276 # Rubber tube\n  examine: \"It's slightly charred.\"\n- id: 277 # Oil can\n  examine: \"It's pretty full.\"\n- id: 278 # Cattleprod\n  examine: \"A sharp cattleprod.\"\n- id: 279 # Sheep feed\n  examine: \"Councillor Halgrive gave me this to kill some sheep.\"\n- id: 280 # Sheep bones (1)\n  examine: \"The suspicious-looking remains of a suspicious-looking sheep.\"\n- id: 281 # Sheep bones (2)\n  examine: \"The suspicious-looking remains of a suspicious-looking sheep.\"\n- id: 282 # Sheep bones (3)\n  examine: \"The suspicious-looking remains of a suspicious-looking sheep.\"\n- id: 283 # Sheep bones (4)\n  examine: \"The suspicious-looking remains of a suspicious-looking sheep.\"\n- id: 284 # Plague jacket\n  examine: \"This should protect me from the plague, I hope!\"\n- id: 285 # Plague trousers\n  examine: \"These should protect me from the plague, I hope!\"\n- id: 286 # Orange goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 287 # Blue goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 288 # Goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 290 # Research package\n  examine: \"This contains some vital research results.\"\n- id: 291 # Notes\n  examine: \"It seems to be written in some kind of code.\"\n- id: 292 # Book on baxtorian\n  examine: \"A book on elven history in northern Gielinor.\"\n- id: 294 # Glarial's pebble\n  examine: \"A small pebble with elven inscription.\"\n- id: 295 # Glarial's amulet\n  examine: \"A bright green gem set in a necklace.\"\n- id: 296 # Glarial's urn\n  examine: \"An urn containing Glarial's ashes.\"\n- id: 299 # Mithril seeds\n  examine: \"Magical seeds in a mithril case.\"\n- id: 300 # Rat's tail\n  examine: \"A bit of rat.\"\n- id: 301 # Lobster pot\n  examine: \"Useful for catching lobsters.\"\n- id: 303 # Small fishing net\n  examine: \"Useful for catching small fish.\"\n- id: 305 # Big fishing net\n  examine: \"Useful for catching lots of fish.\"\n- id: 307 # Fishing rod\n  examine: \"Useful for catching sardine or herring.\"\n- id: 309 # Fly fishing rod\n  examine: \"Useful for catching salmon or trout.\"\n- id: 311 # Harpoon\n  examine: \"Useful for catching really big fish.\"\n- id: 313 # Fishing bait\n  examine: \"For use with a fishing rod.\"\n- id: 314 # Feather\n  examine: \"Used for fly fishing.\"\n- id: 315 # Shrimps\n  examine: \"Some nicely cooked shrimp.\"\n- id: 317 # Raw shrimps\n  examine: \"I should try cooking this.\"\n- id: 319 # Anchovies\n  examine: \"Some nicely cooked anchovies.\"\n- id: 321 # Raw anchovies\n  examine: \"I should try cooking this.\"\n- id: 323 # Burnt fish\n  examine: \"Oops!\"\n- id: 325 # Sardine\n  examine: \"Some nicely cooked sardines.\"\n- id: 327 # Raw sardine\n  examine: \"I should try cooking this.\"\n- id: 329 # Salmon\n  examine: \"Some nicely cooked salmon.\"\n- id: 331 # Raw salmon\n  examine: \"I should try cooking this.\"\n- id: 333 # Trout\n  examine: \"Some nicely cooked trout.\"\n- id: 335 # Raw trout\n  examine: \"I should try cooking this.\"\n- id: 337 # Giant carp\n  examine: \"Some nicely cooked giant carp.\"\n- id: 338 # Raw giant carp\n  examine: \"I should try cooking this.\"\n- id: 339 # Cod\n  examine: \"Some nicely cooked cod.\"\n- id: 341 # Raw cod\n  examine: \"I should try cooking this.\"\n- id: 343 # Burnt fish\n  examine: \"Oops!\"\n- id: 345 # Raw herring\n  examine: \"I should try cooking this.\"\n- id: 347 # Herring\n  examine: \"Some nicely cooked herring.\"\n- id: 349 # Raw pike\n  examine: \"I should try cooking this.\"\n- id: 351 # Pike\n  examine: \"Some nicely cooked pike.\"\n- id: 353 # Raw mackerel\n  examine: \"I should try cooking this.\"\n- id: 355 # Mackerel\n  examine: \"Some nicely cooked mackerel.\"\n- id: 357 # Burnt fish\n  examine: \"Oops!\"\n- id: 359 # Raw tuna\n  examine: \"I should try cooking this.\"\n- id: 361 # Tuna\n  examine: \"Wow, this is a big fish.\"\n- id: 363 # Raw bass\n  examine: \"I should try cooking this.\"\n- id: 365 # Bass\n  examine: \"Wow, this is a big fish.\"\n- id: 367 # Burnt fish\n  examine: \"Oops!\"\n- id: 369 # Burnt fish\n  examine: \"Oops!\"\n- id: 371 # Raw swordfish\n  examine: \"I should try cooking this.\"\n- id: 373 # Swordfish\n  examine: \"I'd better be careful eating this!\"\n- id: 375 # Burnt swordfish\n  examine: \"Oops!\"\n- id: 377 # Raw lobster\n  examine: \"I should try cooking this.\"\n- id: 379 # Lobster\n  examine: \"This looks tricky to eat.\"\n- id: 381 # Burnt lobster\n  examine: \"Oops!\"\n- id: 383 # Raw shark\n  examine: \"I should try cooking this.\"\n- id: 385 # Shark\n  examine: \"I'd better be careful eating this.\"\n- id: 387 # Burnt shark\n  examine: \"Oops!\"\n- id: 389 # Raw manta ray\n  examine: \"A rare catch.\"\n- id: 391 # Manta ray\n  examine: \"A rare catch.\"\n- id: 393 # Burnt manta ray\n  examine: \"Oops!\"\n- id: 395 # Raw sea turtle\n  examine: \"A rare catch.\"\n- id: 397 # Sea turtle\n  examine: \"Tasty!\"\n- id: 399 # Burnt sea turtle\n  examine: \"Oops!\"\n- id: 401 # Seaweed\n  examine: \"Slightly damp seaweed.\"\n- id: 403 # Edible seaweed\n  examine: \"Slightly damp seaweed.\"\n- id: 405 # Casket\n  examine: \"I hope there's treasure in it.\"\n- id: 407 # Oyster\n  examine: \"Maybe there are pearls inside?\"\n- id: 409 # Empty oyster\n  examine: \"Aww, it's empty.\"\n- id: 411 # Oyster pearl\n  examine: \"I could work wonders with a chisel on this pearl.\"\n- id: 413 # Oyster pearls\n  examine: \"I could work wonders with a chisel on these pearls.\"\n- id: 415 # Ethenea\n  examine: \"An expensive colourless liquid.\"\n- id: 416 # Liquid honey\n  examine: \"This isn't worth much.\"\n- id: 417 # Sulphuric broline\n  examine: \"It's highly poisonous.\"\n- id: 418 # Plague sample\n  examine: \"Probably best I don't keep this too long.\"\n- id: 419 # Touch paper\n  examine: \"A special kind of paper.\"\n- id: 420 # Distillator\n  examine: \"Apparently it distills.\"\n- id: 422 # Bird feed\n  examine: \"Birds love this stuff!\"\n- id: 423 # Key\n  examine: \"Opens things.\"\n- id: 424 # Pigeon cage\n  examine: \"It's full of pigeons.\"\n- id: 425 # Pigeon cage\n  examine: \"It's empty...\"\n- id: 426 # Priest gown\n  examine: \"Top half of a priest suit.\"\n- id: 428 # Priest gown\n  examine: \"Bottom half of a priest suit.\"\n- id: 431 # Karamjan rum\n  examine: \"A very strong spirit brewed in Karamja.\"\n- id: 432 # Chest key\n  examine: \"A key to One-Eyed Hector's chest.\"\n- id: 433 # Pirate message\n  examine: \"Pirates don't have the best handwriting...\"\n- id: 434 # Clay\n  examine: \"Some hard dry clay.\"\n- id: 436 # Copper ore\n  examine: \"This needs refining.\"\n- id: 438 # Tin ore\n  examine: \"This needs refining.\"\n- id: 440 # Iron ore\n  examine: \"This needs refining.\"\n- id: 442 # Silver ore\n  examine: \"This needs refining.\"\n- id: 444 # Gold ore\n  examine: \"This needs refining.\"\n- id: 446 # 'perfect' gold ore\n  examine: \"This needs refining.\"\n- id: 447 # Mithril ore\n  examine: \"This needs refining.\"\n- id: 449 # Adamantite ore\n  examine: \"This needs refining.\"\n- id: 451 # Runite ore\n  examine: \"This needs refining.\"\n- id: 453 # Coal\n  examine: \"Hmm a non-renewable energy source!\"\n- id: 455 # Barcrawl card\n  examine: \"The official Alfred Grimhand bar crawl card.\"\n- id: 456 # Scorpion cage\n  examine: \"It's empty!\"\n- id: 457 # Scorpion cage\n  examine: \"There is 1 scorpion inside.\"\n- id: 458 # Scorpion cage\n  examine: \"There are 2 scorpions inside.\"\n- id: 459 # Scorpion cage\n  examine: \"There are 2 scorpions inside.\"\n- id: 460 # Scorpion cage\n  examine: \"There is 1 scorpion inside.\"\n- id: 461 # Scorpion cage\n  examine: \"There are 2 scorpions inside.\"\n- id: 462 # Scorpion cage\n  examine: \"There is 1 scorpion inside.\"\n- id: 463 # Scorpion cage\n  examine: \"There are 3 scorpions inside.\"\n- id: 464 # Strange fruit\n  examine: \"I wonder what this tastes like?\"\n- id: 466 # Pickaxe handle\n  examine: \"Useless without the head.\"\n- id: 468 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 470 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 472 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 474 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 476 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 478 # Broken pickaxe\n  examine: \"Nurmof can fix this for me.\"\n- id: 480 # Bronze pick head\n  examine: \"It's missing a handle.\"\n- id: 482 # Iron pick head\n  examine: \"It's missing a handle.\"\n- id: 484 # Steel pick head\n  examine: \"It's missing a handle.\"\n- id: 486 # Mithril pick head\n  examine: \"It's missing a handle.\"\n- id: 488 # Adamant pick head\n  examine: \"It's missing a handle.\"\n- id: 490 # Rune pick head\n  examine: \"It's missing a handle.\"\n- id: 492 # Axe handle\n  examine: \"Useless without the head.\"\n- id: 494 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 496 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 498 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 500 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 502 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 504 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 506 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 522 # Enchanted beef\n  examine: \"I don't fancy eating this now.\"\n- id: 523 # Enchanted rat\n  examine: \"I don't fancy eating this now.\"\n- id: 524 # Enchanted bear\n  examine: \"I don't fancy eating this now.\"\n- id: 525 # Enchanted chicken\n  examine: \"I don't fancy eating this now.\"\n- id: 526 # Bones\n  examine: \"Bones are for burying!\"\n- id: 528 # Burnt bones\n  examine: \"Bones are for burying!\"\n- id: 530 # Bat bones\n  examine: \"Ew it's a pile of bones.\"\n- id: 532 # Big bones\n  examine: \"Ew it's a pile of bones.\"\n- id: 534 # Babydragon bones\n  examine: \"Ew it's a pile of bones.\"\n- id: 536 # Dragon bones\n  examine: \"These would feed a dog for months!\"\n- id: 538 # Druid's robe\n  examine: \"Keeps a druid's knees nice and warm.\"\n- id: 542 # Monk's robe\n  examine: \"Keeps a monk's knees nice and warm.\"\n- id: 548 # Shade robe\n  examine: \"If a shade had knees, this would keep them nice and warm.\"\n- id: 550 # Newcomer map\n  examine: \"Issued to all new citizens of Gielinor.\"\n- id: 552 # Ghostspeak amulet\n  examine: \"It lets me talk to ghosts.\"\n- id: 553 # Ghost's skull\n  examine: \"Ooooh spooky!\"\n- id: 554 # Fire rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 555 # Water rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 556 # Air rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 557 # Earth rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 558 # Mind rune\n  examine: \"Used for basic level missile spells.\"\n- id: 559 # Body rune\n  examine: \"Used for curse spells.\"\n- id: 560 # Death rune\n  examine: \"Used for medium level missile spells.\"\n- id: 561 # Nature rune\n  examine: \"Used for alchemy spells.\"\n- id: 562 # Chaos rune\n  examine: \"Used for low level missile spells.\"\n- id: 563 # Law rune\n  examine: \"Used for teleport spells.\"\n- id: 564 # Cosmic rune\n  examine: \"Used for enchant spells.\"\n- id: 565 # Blood rune\n  examine: \"Used for high level missile spells.\"\n- id: 566 # Soul rune\n  examine: \"Used for high level curse spells.\"\n- id: 567 # Unpowered orb\n  examine: \"I'd prefer it if it was powered.\"\n- id: 569 # Fire orb\n  examine: \"A magic glowing orb.\"\n- id: 571 # Water orb\n  examine: \"A magic glowing orb.\"\n- id: 573 # Air orb\n  examine: \"A magic glowing orb.\"\n- id: 575 # Earth orb\n  examine: \"A magic glowing orb.\"\n- id: 581 # Black robe\n  examine: \"I can do magic better in this.\"\n- id: 583 # Bailing bucket\n  examine: \"It's a bailing bucket.\"\n- id: 585 # Bailing bucket\n  examine: \"It's a bailing bucket full of salty water.\"\n- id: 587 # Orb of protection\n  examine: \"A strange glowing green orb.\"\n- id: 588 # Orbs of protection\n  examine: \"Two strange glowing green orbs.\"\n- id: 589 # Gnome amulet\n  examine: \"It's an amulet of protection given to me by the gnomes.\"\n- id: 590 # Tinderbox\n  examine: \"Useful for lighting a fire.\"\n- id: 592 # Ashes\n  examine: \"A heap of ashes.\"\n- id: 594 # Lit torch\n  examine: \"A lit home-made torch.\"\n- id: 595 # Torch\n  examine: \"An unlit home-made torch.\"\n- id: 596 # Unlit torch\n  examine: \"An unlit home-made torch.\"\n- id: 602 # Lens mould\n  examine: \"An unusual mould in the shape of a disc.\"\n- id: 604 # Bone shard\n  examine: \"A slender bone shard given to you by Zadimus.\"\n- id: 605 # Bone key\n  examine: \"A bone key fashioned from a shard of bone.\"\n- id: 606 # Stone-plaque\n  examine: \"A stone plaque with carved letters in it.\"\n- id: 607 # Tattered scroll\n  examine: \"An ancient tattered scroll.\"\n- id: 608 # Crumpled scroll\n  examine: \"An ancient crumpled scroll.\"\n- id: 609 # Rashiliyia corpse\n  examine: \"The remains of the Zombie Queen.\"\n- id: 610 # Zadimus corpse\n  examine: \"The remains of Zadimus.\"\n- id: 611 # Locating crystal\n  examine: \"A magical crystal sphere.\"\n- id: 612 # Locating crystal\n  examine: \"A magical crystal sphere.\"\n- id: 613 # Locating crystal\n  examine: \"A magical crystal sphere.\"\n- id: 614 # Locating crystal\n  examine: \"A magical crystal sphere.\"\n- id: 615 # Locating crystal\n  examine: \"A magical crystal sphere.\"\n- id: 616 # Beads of the dead\n  examine: \"A curious looking neck ornament.\"\n- id: 617 # Coins\n  examine: \"Lovely money!\"\n- id: 618 # Bone beads\n  examine: \"Beads carved out of a bone.\"\n- id: 619 # Paramaya ticket\n  examine: \"Allows you to rest in the luxurious Paramayer Inn.\"\n- id: 621 # Ship ticket\n  examine: \"Allows you passage on the 'Lady of the waves' ship.\"\n- id: 623 # Sword pommel\n  examine: \"An ivory sword pommel.\"\n- id: 624 # Bervirius notes\n  examine: \"Notes taken from the tomb of Bervirius.\"\n- id: 625 # Wampum belt\n  examine: \"A decorated belt used to trade information between distant villages.\"\n- id: 666 # Portrait\n  examine: \"Picture of a posing Paladin.\"\n- id: 667 # Blurite sword\n  examine: \"A Faladian Knight's sword.\"\n- id: 668 # Blurite ore\n  examine: \"Definitely blue.\"\n- id: 669 # Specimen jar\n  examine: \"A receptacle for specimens!\"\n- id: 670 # Specimen brush\n  examine: \"A small brush used to clean rock samples.\"\n- id: 671 # Animal skull\n  examine: \"A carefully-kept-safe skull sample.\"\n- id: 672 # Special cup\n  examine: \"A special cup.\"\n- id: 673 # Teddy\n  examine: \"A lucky mascot.\"\n- id: 674 # Cracked sample\n  examine: \"A roughly shaped piece of rock.\"\n- id: 675 # Rock pick\n  examine: \"A small pick for digging.\"\n- id: 676 # Trowel\n  examine: \"Used for digging!\"\n- id: 677 # Panning tray\n  examine: \"An empty tray for panning.\"\n- id: 678 # Panning tray\n  examine: \"This tray contains gold.\"\n- id: 679 # Panning tray\n  examine: \"This tray contains mud.\"\n- id: 680 # Nuggets\n  examine: \"Pure, lovely gold!\"\n- id: 681 # Ancient talisman\n  examine: \"An unusual symbol as yet unidentified by the archaeological expert.\"\n- id: 682 # Unstamped letter\n  examine: \"A letter waiting to be stamped.\"\n- id: 683 # Sealed letter\n  examine: \"A sealed letter of recommendation.\"\n- id: 684 # Belt buckle\n  examine: \"Used to hold up trousers!\"\n- id: 685 # Old boot\n  examine: \"Phew!\"\n- id: 686 # Rusty sword\n  examine: \"A decent-enough weapon gone rusty.\"\n- id: 687 # Broken arrow\n  examine: \"This must have been shot at high speed.\"\n- id: 688 # Buttons\n  examine: \"Not Dick Whittington's helper at all!\"\n- id: 689 # Broken staff\n  examine: \"I pity the poor person beaten with this!\"\n- id: 690 # Broken glass\n  examine: \"Smashed glass.\"\n- id: 691 # Level 1 certificate\n  examine: \"The owner has passed the Earth Sciences level 1 exam.\"\n- id: 692 # Level 2 certificate\n  examine: \"The owner has passed Earth Sciences level 2 exam.\"\n- id: 693 # Level 3 certificate\n  examine: \"The owner has passed Earth Sciences level 3 exam.\"\n- id: 694 # Ceramic remains\n  examine: \"Smashing!\"\n- id: 695 # Old tooth\n  examine: \"Now, if I can just find a tooth fairy to sell this to...\"\n- id: 696 # Invitation letter\n  examine: \"A letter inviting me to use the private dig shafts.\"\n- id: 697 # Damaged armour\n  examine: \"It would be hard to repair this!\"\n- id: 698 # Broken armour\n  examine: \"No use to me in this state...\"\n- id: 699 # Stone tablet\n  examine: \"An old stone slab with writing on it.\"\n- id: 700 # Chemical powder\n  examine: \"An acrid chemical.\"\n- id: 701 # Ammonium nitrate\n  examine: \"An acrid chemical.\"\n- id: 702 # Unidentified liquid\n  examine: \"A strong chemical.\"\n- id: 703 # Nitroglycerin\n  examine: \"A strong chemical.\"\n- id: 704 # Ground charcoal\n  examine: \"Charcoal - crushed to small pieces!\"\n- id: 705 # Mixed chemicals\n  examine: \"A mixture of strong chemicals.\"\n- id: 706 # Mixed chemicals\n  examine: \"A mixture of strong chemicals.\"\n- id: 707 # Chemical compound\n  examine: \"A mixture of strong chemicals.\"\n- id: 708 # Arcenia root\n  examine: \"The root of an arcenia plant.\"\n- id: 709 # Chest key\n  examine: \"This fits a chest.\"\n- id: 710 # Vase\n  examine: \"A vessel for holding plants.\"\n- id: 711 # Book on chemicals\n  examine: \"It's about chemicals, judging from its cover.\"\n- id: 712 # Cup of tea\n  examine: \"A nice cup of tea.\"\n- id: 714 # Radimus notes\n  examine: \"Notes given to you by Radimus Erkle, it includes a partially completed map.\"\n- id: 715 # Radimus notes\n  examine: \"Notes given to you by Radimus Erkle, it includes a partially completed map.\"\n- id: 716 # Bull roarer\n  examine: \"It makes a loud but interesting sound when swung in the air.\"\n- id: 717 # Scrawled note\n  examine: \"A scrawled note with spidery writing on it.\"\n- id: 718 # A scribbled note\n  examine: \"A scrawled note with spidery writing on it.\"\n- id: 719 # Scrumpled note\n  examine: \"A scrawled note with spidery writing on it.\"\n- id: 720 # Sketch\n  examine: \"A rough sketch of a bowl shaped vessel given to you by Gujuo.\"\n- id: 721 # Gold bowl\n  examine: \"A specially made bowl constructed out of pure gold.\"\n- id: 722 # Blessed gold bowl\n  examine: \"A specially made bowl constructed out of pure gold and blessed.\"\n- id: 723 # Golden bowl\n  examine: \"A specially made golden bowl with water.\"\n- id: 724 # Golden bowl\n  examine: \"A specially made bowl constructed out of pure gold. It has pure water in it.\"\n- id: 725 # Golden bowl\n  examine: \"A blessed golden bowl. It has water in it.\"\n- id: 726 # Golden bowl\n  examine: \"A blessed golden bowl. It has pure sacred water in it.\"\n- id: 727 # Hollow reed\n  examine: \"One of nature's pipes.\"\n- id: 729 # Shamans tome\n  examine: \"It looks like the Shamans personal notes...\"\n- id: 731 # Enchanted vial\n  examine: \"An enchanted empty glass vial.\"\n- id: 732 # Holy water\n  examine: \"A vial of holy water, good against certain demons.\"\n- id: 733 # Smashed glass\n  examine: \"Fragments of a broken container.\"\n- id: 735 # Yommi tree seeds\n  examine: \"These need to be germinated before they can be used.\"\n- id: 736 # Yommi tree seeds\n  examine: \"These are germinated and ready to be planted in fertile soil.\"\n- id: 737 # Snakeweed mixture\n  examine: \"It's a mixture of Snakeweed and water. Needs another ingredient.\"\n- id: 738 # Ardrigal mixture\n  examine: \"It's a mixture of Ardrigal and water. Needs another ingredient.\"\n- id: 739 # Bravery potion\n  examine: \"A bravery potion for which Gujuo gave you the details, let's hope it works.\"\n- id: 740 # Blue hat\n  examine: \"A strange blue wizards hat.\"\n- id: 741 # Chunk of crystal\n  examine: \"It looks like it's been snapped off of something.\"\n- id: 742 # Hunk of crystal\n  examine: \"It looks like it's been snapped off of something.\"\n- id: 743 # Lump of crystal\n  examine: \"It looks like it's been snapped off of something.\"\n- id: 744 # Heart crystal\n  examine: \"A heart shaped crystal.\"\n- id: 745 # Heart crystal\n  examine: \"A heart shaped crystal.\"\n- id: 746 # Dark dagger\n  examine: \"A black obsidian dagger, it has a strange aura about it.\"\n- id: 747 # Glowing dagger\n  examine: \"A black obsidian dagger, it has a strange aura about it - it seems to be glowing.\"\n- id: 748 # Holy force\n  examine: \"A powerful spell for good.\"\n- id: 749 # Yommi totem\n  examine: \"A well carved totem pole made from the trunk of a Yommi tree.\"\n- id: 750 # Gilded totem\n  examine: \"A gilded totem pole from the Kharazi tribe.\"\n- id: 751 # Gnomeball\n  examine: \"A ball used in Gnomeball.\"\n- id: 753 # Cadava berries\n  examine: \"Poisonous berries.\"\n- id: 755 # Message\n  examine: \"A message from Juliet to Romeo.\"\n- id: 756 # Cadava potion\n  examine: \"I'm meant to give this to Juliet.\"\n- id: 757 # Book\n  examine: \"The Shield of Arrav by A R Wright.\"\n- id: 763 # Broken shield\n  examine: \"Half of the Shield of Arrav.\"\n- id: 765 # Broken shield\n  examine: \"Half of the Shield of Arrav.\"\n- id: 767 # Phoenix crossbow\n  examine: \"Former property of the Phoenix Gang.\"\n- id: 769 # Certificate\n  examine: \"I can use this to claim a reward from the King.\"\n- id: 771 # Dramen branch\n  examine: \"A limb of the fabled Dramen tree.\"\n- id: 772 # Dramen staff\n  examine: \"Crafted from a Dramen tree branch.\"\n- id: 773 # 'perfect' ring\n  examine: \"A perfect ruby ring.\"\n- id: 774 # 'perfect' necklace\n  examine: \"A perfect ruby necklace.\"\n- id: 775 # Cooking gauntlets\n  examine: \"These gauntlets empower with a greater ability to cook fish.\"\n- id: 777 # Chaos gauntlets\n  examine: \"These gauntlets empower spell casters.\"\n- id: 778 # Steel gauntlets\n  examine: \"My reward for assisting the Fitzharmon family.\"\n- id: 779 # Crest part\n  examine: \"A fragment of the Fitzharmon family crest.\"\n- id: 780 # Crest part\n  examine: \"A fragment of the Fitzharmon family crest.\"\n- id: 781 # Crest part\n  examine: \"A fragment of the Fitzharmon family crest.\"\n- id: 782 # Family crest\n  examine: \"The Fitzharmon family crest.\"\n- id: 783 # Bark sample\n  examine: \"A sample of the bark from the Grand Tree.\"\n- id: 784 # Translation book\n  examine: \"A book to translate the ancient gnome language into English.\"\n- id: 785 # Glough's journal\n  examine: \"Perhaps I should read it and see what Glough is up to!\"\n- id: 786 # Hazelmere's scroll\n  examine: \"Hazelmere wrote something down on this scroll.\"\n- id: 787 # Lumber order\n  examine: \"An order from the Karamja shipyard.\"\n- id: 788 # Glough's key\n  examine: \"The key to Glough's chest.\"\n- id: 789 # Twigs\n  examine: \"Twigs bound together in the shape of a T.\"\n- id: 790 # Twigs\n  examine: \"Twigs bound together in the shape of a U.\"\n- id: 791 # Twigs\n  examine: \"Twigs bound together in the shape of a Z.\"\n- id: 792 # Twigs\n  examine: \"Twigs bound together in the shape of a O.\"\n- id: 793 # Daconia rock\n  examine: \"An ancient rock with strange magical properties.\"\n- id: 794 # Invasion plans\n  examine: \"These are plans for an invasion!\"\n- id: 795 # War ship\n  examine: \"A model of a Karamja warship.\"\n- id: 800 # Bronze thrownaxe\n  examine: \"A finely balanced throwing axe.\"\n- id: 801 # Iron thrownaxe\n  examine: \"A finely balanced throwing axe.\"\n- id: 802 # Steel thrownaxe\n  examine: \"A finely balanced throwing axe.\"\n- id: 803 # Mithril thrownaxe\n  examine: \"A finely balanced throwing axe.\"\n- id: 805 # Rune thrownaxe\n  examine: \"A finely balanced throwing axe.\"\n- id: 806 # Bronze dart\n  examine: \"A deadly throwing dart with a bronze tip.\"\n- id: 807 # Iron dart\n  examine: \"A deadly throwing dart with an iron tip.\"\n- id: 808 # Steel dart\n  examine: \"A deadly throwing dart with a steel tip.\"\n- id: 809 # Mithril dart\n  examine: \"A deadly throwing dart with a mithril tip.\"\n- id: 810 # Adamant dart\n  examine: \"A deadly throwing dart with an adamant tip.\"\n- id: 811 # Rune dart\n  examine: \"A deadly throwing dart with a rune tip.\"\n- id: 812 # Bronze dart(p)\n  examine: \"A deadly poisoned dart with a bronze tip.\"\n- id: 813 # Iron dart(p)\n  examine: \"A deadly poisoned dart with an iron tip.\"\n- id: 814 # Steel dart(p)\n  examine: \"A deadly poisoned dart with a steel tip.\"\n- id: 815 # Mithril dart(p)\n  examine: \"A deadly poisoned dart with a mithril tip.\"\n- id: 816 # Adamant dart(p)\n  examine: \"A deadly poisoned dart with an adamant tip.\"\n- id: 817 # Rune dart(p)\n  examine: \"A deadly poisoned dart with a rune tip.\"\n- id: 819 # Bronze dart tip\n  examine: \"A deadly looking dart tip made of bronze - needs feathers for flight.\"\n- id: 820 # Iron dart tip\n  examine: \"A deadly looking dart tip made of iron - needs feathers for flight.\"\n- id: 821 # Steel dart tip\n  examine: \"A deadly-looking dart tip made of steel - needs feathers for flight.\"\n- id: 822 # Mithril dart tip\n  examine: \"A deadly looking dart tip made of mithril - needs feathers for flight.\"\n- id: 823 # Adamant dart tip\n  examine: \"A deadly-looking dart tip made of adamant - needs feathers for flight.\"\n- id: 824 # Rune dart tip\n  examine: \"A deadly-looking dart tip made of runite - needs feathers for flight.\"\n- id: 825 # Bronze javelin\n  examine: \"A bronze tipped javelin.\"\n- id: 826 # Iron javelin\n  examine: \"An iron tipped javelin.\"\n- id: 827 # Steel javelin\n  examine: \"A steel tipped javelin.\"\n- id: 828 # Mithril javelin\n  examine: \"A mithril tipped javelin.\"\n- id: 829 # Adamant javelin\n  examine: \"An adamant tipped javelin.\"\n- id: 830 # Rune javelin\n  examine: \"A rune tipped javelin.\"\n- id: 831 # Bronze javelin(p)\n  examine: \"A bronze tipped javelin.\"\n- id: 832 # Iron javelin(p)\n  examine: \"An iron tipped javelin.\"\n- id: 833 # Steel javelin(p)\n  examine: \"A steel tipped javelin.\"\n- id: 834 # Mithril javelin(p)\n  examine: \"A mithril tipped javelin.\"\n- id: 835 # Adamant javelin(p)\n  examine: \"An adamant tipped javelin.\"\n- id: 836 # Rune javelin(p)\n  examine: \"A rune tipped javelin.\"\n- id: 837 # Crossbow\n  examine: \"This fires crossbow bolts.\"\n- id: 839 # Longbow\n  examine: \"A nice sturdy bow.\"\n- id: 841 # Shortbow\n  examine: \"Short but effective.\"\n- id: 843 # Oak shortbow\n  examine: \"A shortbow made out of oak, still effective.\"\n- id: 845 # Oak longbow\n  examine: \"A nice sturdy bow made out of oak.\"\n- id: 847 # Willow longbow\n  examine: \"A nice sturdy bow made out of willow.\"\n- id: 849 # Willow shortbow\n  examine: \"A shortbow made out of willow, still effective.\"\n- id: 851 # Maple longbow\n  examine: \"A nice sturdy bow made out of Maple.\"\n- id: 853 # Maple shortbow\n  examine: \"A shortbow made out of Maple, still effective.\"\n- id: 855 # Yew longbow\n  examine: \"A nice sturdy bow made out of yew.\"\n- id: 857 # Yew shortbow\n  examine: \"A shortbow made out of yew, still effective.\"\n- id: 859 # Magic longbow\n  examine: \"A nice sturdy magical bow.\"\n- id: 861 # Magic shortbow\n  examine: \"Short and magical, but still effective.\"\n- id: 863 # Iron knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 864 # Bronze knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 865 # Steel knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 866 # Mithril knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 867 # Adamant knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 868 # Rune knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 869 # Black knife\n  examine: \"A finely balanced throwing knife.\"\n- id: 870 # Bronze knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 871 # Iron knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 872 # Steel knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 873 # Mithril knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 874 # Black knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 875 # Adamant knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 876 # Rune knife(p)\n  examine: \"A finely balanced throwing knife.\"\n- id: 877 # Bronze bolts\n  examine: \"Bronze crossbow bolts.\"\n- id: 879 # Opal bolts\n  examine: \"Opal tipped Bronze crossbow bolts.\"\n- id: 880 # Pearl bolts\n  examine: \"Pearl tipped Iron crossbow bolts.\"\n- id: 881 # Barbed bolts\n  examine: \"Great if you have a crossbow!\"\n- id: 882 # Bronze arrow\n  examine: \"Arrows with bronze heads.\"\n- id: 883 # Bronze arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 884 # Iron arrow\n  examine: \"Arrows with iron heads.\"\n- id: 885 # Iron arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 886 # Steel arrow\n  examine: \"Arrows with steel heads.\"\n- id: 887 # Steel arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 888 # Mithril arrow\n  examine: \"Arrows with mithril heads.\"\n- id: 889 # Mithril arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 890 # Adamant arrow\n  examine: \"Arrows with adamant heads.\"\n- id: 891 # Adamant arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 892 # Rune arrow\n  examine: \"Arrows with rune heads.\"\n- id: 893 # Rune arrow(p)\n  examine: \"Venomous-looking arrows.\"\n- id: 946 # Knife\n  examine: \"A dangerous looking knife.\"\n- id: 948 # Bear fur\n  examine: \"This would make warm clothing.\"\n- id: 950 # Silk\n  examine: \"It's a sheet of silk.\"\n- id: 952 # Spade\n  examine: \"A slightly muddy spade.\"\n- id: 954 # Rope\n  examine: \"A coil of rope.\"\n- id: 956 # Flier\n  examine: \"Get your axes from Bob's Axes.\"\n- id: 958 # Grey wolf fur\n  examine: \"This would make warm clothing.\"\n- id: 960 # Plank\n  examine: \"A plank of wood!\"\n- id: 962 # Christmas cracker\n  examine: \"I need to pull this.\"\n- id: 964 # Skull\n  examine: \"Ooooh spooky!\"\n- id: 966 # Tile\n  examine: \"A fraction of a roof.\"\n- id: 968 # Rock\n  examine: \"A rock\"\n- id: 970 # Papyrus\n  examine: \"Used for making notes.\"\n- id: 972 # Papyrus\n  examine: \"Used for making notes.\"\n- id: 973 # Charcoal\n  examine: \"A lump of charcoal.\"\n- id: 975 # Machete\n  examine: \"A jungle specific slashing device.\"\n- id: 981 # Disk of returning\n  examine: \"Used to get out of Thordur's blackhole.\"\n- id: 983 # Brass key\n  examine: \"Opens a door that leads into a dungeon.\"\n- id: 989 # Crystal key\n  examine: \"A mysterious key for a mysterious chest.\"\n- id: 991 # Muddy key\n  examine: \"It looks like the key to a chest.\"\n- id: 993 # Sinister key\n  examine: \"You get a sense of dread from this key.\"\n- id: 995 # Coins\n  examine: \"Lovely money!\"\n- id: 1005 # White apron\n  examine: \"A mostly clean apron.\"\n- id: 1009 # Brass necklace\n  examine: \"I'd prefer a gold one.\"\n- id: 1011 # Blue skirt\n  examine: \"Leg covering favoured by women and wizards.\"\n- id: 1013 # Pink skirt\n  examine: \"A ladies skirt.\"\n- id: 1015 # Black skirt\n  examine: \"Clothing favoured by women and dark wizards.\"\n- id: 1017 # Wizard hat\n  examine: \"A silly pointed hat.\"\n- id: 1037 # Bunny ears\n  examine: \"A rabbit-like adornment.\"\n- id: 1038 # Red partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1040 # Yellow partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1042 # Blue partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1044 # Green partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1046 # Purple partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1048 # White partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 1050 # Santa hat\n  examine: \"It's a Santa hat.\"\n- id: 1052 # Cape of legends\n  examine: \"The cape worn by members of the Legends Guild.\"\n- id: 1059 # Leather gloves\n  examine: \"These will keep my hands warm!\"\n- id: 1061 # Leather boots\n  examine: \"Comfortable leather boots.\"\n- id: 1063 # Leather vambraces\n  examine: \"Better than no armour!\"\n- id: 1067 # Iron platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1069 # Steel platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1071 # Mithril platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1073 # Adamant platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1075 # Bronze platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1077 # Black platelegs\n  examine: \"Big, black and heavy looking.\"\n- id: 1079 # Rune platelegs\n  examine: \"These look pretty heavy.\"\n- id: 1081 # Iron plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1083 # Steel plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1085 # Mithril plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1087 # Bronze plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1089 # Black plateskirt\n  examine: \"Big, black and heavy looking.\"\n- id: 1091 # Adamant plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1093 # Rune plateskirt\n  examine: \"Designer leg protection.\"\n- id: 1095 # Leather chaps\n  examine: \"Better than no armour!\"\n- id: 1097 # Studded chaps\n  examine: \"Those studs should provide a bit more protection.\"\n- id: 1099 # Green d'hide chaps\n  examine: \"Made from 100% real dragonhide.\"\n- id: 1101 # Iron chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1103 # Bronze chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1105 # Steel chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1107 # Black chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1109 # Mithril chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1111 # Adamant chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1113 # Rune chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 1115 # Iron platebody\n  examine: \"Provides excellent protection.\"\n- id: 1117 # Bronze platebody\n  examine: \"Provides excellent protection.\"\n- id: 1119 # Steel platebody\n  examine: \"Provides excellent protection.\"\n- id: 1121 # Mithril platebody\n  examine: \"Provides excellent protection.\"\n- id: 1123 # Adamant platebody\n  examine: \"Provides excellent protection.\"\n- id: 1125 # Black platebody\n  examine: \"Provides excellent protection.\"\n- id: 1127 # Rune platebody\n  examine: \"Provides excellent protection.\"\n- id: 1129 # Leather body\n  examine: \"Better than no armour!\"\n- id: 1131 # Hardleather body\n  examine: \"Harder than normal leather.\"\n- id: 1133 # Studded body\n  examine: \"Those studs should provide a bit more protection.\"\n- id: 1135 # Green d'hide body\n  examine: \"Made from 100% real dragonhide.\"\n- id: 1137 # Iron med helm\n  examine: \"A medium sized helmet.\"\n- id: 1139 # Bronze med helm\n  examine: \"A medium sized helmet.\"\n- id: 1141 # Steel med helm\n  examine: \"A medium sized helmet.\"\n- id: 1143 # Mithril med helm\n  examine: \"A medium sized helmet.\"\n- id: 1145 # Adamant med helm\n  examine: \"A medium sized helmet.\"\n- id: 1147 # Rune med helm\n  examine: \"A medium sized helmet.\"\n- id: 1149 # Dragon med helm\n  examine: \"Makes the wearer pretty intimidating.\"\n- id: 1151 # Black med helm\n  examine: \"A medium sized helmet.\"\n- id: 1153 # Iron full helm\n  examine: \"A full face helmet.\"\n- id: 1155 # Bronze full helm\n  examine: \"A full face helmet.\"\n- id: 1157 # Steel full helm\n  examine: \"A full face helmet.\"\n- id: 1159 # Mithril full helm\n  examine: \"A full face helmet.\"\n- id: 1161 # Adamant full helm\n  examine: \"A full face helmet.\"\n- id: 1163 # Rune full helm\n  examine: \"A full face helmet.\"\n- id: 1165 # Black full helm\n  examine: \"A full face helmet.\"\n- id: 1167 # Leather cowl\n  examine: \"Better than no armour!\"\n- id: 1169 # Coif\n  examine: \"Light weight head protection.\"\n- id: 1171 # Wooden shield\n  examine: \"A solid wooden shield.\"\n- id: 1173 # Bronze sq shield\n  examine: \"A medium square shield.\"\n- id: 1175 # Iron sq shield\n  examine: \"A medium square shield.\"\n- id: 1177 # Steel sq shield\n  examine: \"A medium square shield.\"\n- id: 1179 # Black sq shield\n  examine: \"A medium square shield.\"\n- id: 1181 # Mithril sq shield\n  examine: \"A medium square shield.\"\n- id: 1183 # Adamant sq shield\n  examine: \"A medium square shield.\"\n- id: 1185 # Rune sq shield\n  examine: \"A medium square shield.\"\n- id: 1187 # Dragon sq shield\n  examine: \"An ancient and powerful looking Dragon Square shield.\"\n- id: 1189 # Bronze kiteshield\n  examine: \"A large metal shield.\"\n- id: 1191 # Iron kiteshield\n  examine: \"A large metal shield.\"\n- id: 1193 # Steel kiteshield\n  examine: \"A large metal shield.\"\n- id: 1195 # Black kiteshield\n  examine: \"A large metal shield.\"\n- id: 1197 # Mithril kiteshield\n  examine: \"A large metal shield.\"\n- id: 1199 # Adamant kiteshield\n  examine: \"A large metal shield.\"\n- id: 1201 # Rune kiteshield\n  examine: \"A large metal shield.\"\n- id: 1203 # Iron dagger\n  examine: \"Short but pointy.\"\n- id: 1205 # Bronze dagger\n  examine: \"Short but pointy.\"\n- id: 1207 # Steel dagger\n  examine: \"Short but pointy.\"\n- id: 1209 # Mithril dagger\n  examine: \"A dangerous dagger.\"\n- id: 1211 # Adamant dagger\n  examine: \"Short and deadly.\"\n- id: 1213 # Rune dagger\n  examine: \"A powerful dagger.\"\n- id: 1215 # Dragon dagger\n  examine: \"A powerful dagger.\"\n- id: 1217 # Black dagger\n  examine: \"A vicious black dagger.\"\n- id: 1219 # Iron dagger(p)\n  examine: \"The blade is covered with poison.\"\n- id: 1221 # Bronze dagger(p)\n  examine: \"This dagger is poisoned.\"\n- id: 1223 # Steel dagger(p)\n  examine: \"The blade has been poisoned.\"\n- id: 1225 # Mithril dagger(p)\n  examine: \"A poisoned Mithril dagger.\"\n- id: 1227 # Adamant dagger(p)\n  examine: \"A very dangerous poisoned dagger.\"\n- id: 1229 # Rune dagger(p)\n  examine: \"The blade is covered with a nasty poison.\"\n- id: 1231 # Dragon dagger(p)\n  examine: \"A powerful dagger.\"\n- id: 1233 # Black dagger(p)\n  examine: \"This dagger is poisoned.\"\n- id: 1237 # Bronze spear\n  examine: \"A bronze tipped spear.\"\n- id: 1239 # Iron spear\n  examine: \"An iron tipped spear.\"\n- id: 1241 # Steel spear\n  examine: \"A steel tipped spear.\"\n- id: 1243 # Mithril spear\n  examine: \"A mithril tipped spear.\"\n- id: 1245 # Adamant spear\n  examine: \"An adamantite tipped spear.\"\n- id: 1247 # Rune spear\n  examine: \"A rune tipped spear.\"\n- id: 1249 # Dragon spear\n  examine: \"A dragon tipped spear.\"\n- id: 1251 # Bronze spear(p)\n  examine: \"A poisoned bronze tipped spear.\"\n- id: 1253 # Iron spear(p)\n  examine: \"A poisoned iron tipped spear.\"\n- id: 1255 # Steel spear(p)\n  examine: \"A poisoned steel tipped spear.\"\n- id: 1257 # Mithril spear(p)\n  examine: \"A poisoned mithril tipped spear.\"\n- id: 1259 # Adamant spear(p)\n  examine: \"A poisoned adamantite tipped spear.\"\n- id: 1261 # Rune spear(p)\n  examine: \"A poisoned rune tipped spear.\"\n- id: 1263 # Dragon spear(p)\n  examine: \"A poisoned dragon tipped spear.\"\n- id: 1265 # Bronze pickaxe\n  examine: \"Used for mining.\"\n- id: 1267 # Iron pickaxe\n  examine: \"Used for mining.\"\n- id: 1269 # Steel pickaxe\n  examine: \"Used for mining.\"\n- id: 1271 # Adamant pickaxe\n  examine: \"Used for mining.\"\n- id: 1273 # Mithril pickaxe\n  examine: \"Used for mining.\"\n- id: 1275 # Rune pickaxe\n  examine: \"Used for mining.\"\n- id: 1277 # Bronze sword\n  examine: \"A razor sharp sword.\"\n- id: 1279 # Iron sword\n  examine: \"A razor sharp sword.\"\n- id: 1281 # Steel sword\n  examine: \"A razor sharp sword.\"\n- id: 1283 # Black sword\n  examine: \"A razor sharp sword.\"\n- id: 1285 # Mithril sword\n  examine: \"A razor sharp sword.\"\n- id: 1287 # Adamant sword\n  examine: \"A razor sharp sword.\"\n- id: 1289 # Rune sword\n  examine: \"A razor sharp sword.\"\n- id: 1291 # Bronze longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1293 # Iron longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1295 # Steel longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1297 # Black longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1299 # Mithril longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1301 # Adamant longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1303 # Rune longsword\n  examine: \"A razor sharp longsword.\"\n- id: 1305 # Dragon longsword\n  examine: \"A very powerful sword.\"\n- id: 1307 # Bronze 2h sword\n  examine: \"A two handed sword.\"\n- id: 1309 # Iron 2h sword\n  examine: \"A two handed sword.\"\n- id: 1311 # Steel 2h sword\n  examine: \"A two handed sword.\"\n- id: 1313 # Black 2h sword\n  examine: \"A two handed sword.\"\n- id: 1315 # Mithril 2h sword\n  examine: \"A two handed sword.\"\n- id: 1317 # Adamant 2h sword\n  examine: \"A two handed sword.\"\n- id: 1319 # Rune 2h sword\n  examine: \"A two handed sword.\"\n- id: 1321 # Bronze scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1323 # Iron scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1325 # Steel scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1327 # Black scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1329 # Mithril scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1331 # Adamant scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1333 # Rune scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 1335 # Iron warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1337 # Bronze warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1339 # Steel warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1341 # Black warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1343 # Mithril warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1347 # Rune warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 1349 # Iron axe\n  examine: \"A woodcutter's axe.\"\n- id: 1351 # Bronze axe\n  examine: \"A woodcutter's axe.\"\n- id: 1353 # Steel axe\n  examine: \"A woodcutter's axe.\"\n- id: 1355 # Mithril axe\n  examine: \"A powerful axe.\"\n- id: 1357 # Adamant axe\n  examine: \"A powerful axe.\"\n- id: 1359 # Rune axe\n  examine: \"A powerful axe.\"\n- id: 1361 # Black axe\n  examine: \"A sinister looking axe.\"\n- id: 1363 # Iron battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1365 # Steel battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1367 # Black battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1369 # Mithril battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1371 # Adamant battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1373 # Rune battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1375 # Bronze battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1377 # Dragon battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 1379 # Staff\n  examine: \"It's a slightly magical stick.\"\n- id: 1381 # Staff of air\n  examine: \"A Magical staff.\"\n- id: 1383 # Staff of water\n  examine: \"A Magical staff.\"\n- id: 1385 # Staff of earth\n  examine: \"A Magical staff.\"\n- id: 1387 # Staff of fire\n  examine: \"A Magical staff.\"\n- id: 1389 # Magic staff\n  examine: \"A Magical staff.\"\n- id: 1391 # Battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 1393 # Fire battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 1395 # Water battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 1397 # Air battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 1399 # Earth battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 1401 # Mystic fire staff\n  examine: \"It's a slightly magical stick.\"\n- id: 1403 # Mystic water staff\n  examine: \"It's a slightly magical stick.\"\n- id: 1405 # Mystic air staff\n  examine: \"It's a slightly magical stick.\"\n- id: 1407 # Mystic earth staff\n  examine: \"It's a slightly magical stick.\"\n- id: 1409 # Iban's staff\n  examine: \"An ancient staff, formerly the property of Iban.\"\n- id: 1410 # Iban's staff\n  examine: \"I'll need to get this repaired before I can use it.\"\n- id: 1419 # Scythe\n  examine: \"It's a Scythe.\"\n- id: 1420 # Iron mace\n  examine: \"A spiky mace.\"\n- id: 1422 # Bronze mace\n  examine: \"A spiky mace.\"\n- id: 1424 # Steel mace\n  examine: \"A spiky mace.\"\n- id: 1426 # Black mace\n  examine: \"A spiky mace.\"\n- id: 1428 # Mithril mace\n  examine: \"A spiky mace.\"\n- id: 1430 # Adamant mace\n  examine: \"A spiky mace.\"\n- id: 1432 # Rune mace\n  examine: \"A spiky mace.\"\n- id: 1434 # Dragon mace\n  examine: \"A spiky mace.\"\n- id: 1436 # Rune essence\n  examine: \"An uncharged Rune Stone.\"\n- id: 1438 # Air talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1440 # Earth talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1442 # Fire talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1444 # Water talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1446 # Body talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1448 # Mind talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1452 # Chaos talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1454 # Cosmic talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1456 # Death talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1458 # Law talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1462 # Nature talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 1464 # Archery ticket\n  examine: \"I can exchange this for equipment.\"\n- id: 1465 # Weapon poison\n  examine: \"For use on daggers and projectiles.\"\n- id: 1466 # Sea slug\n  examine: \"A rather nasty looking crustacean.\"\n- id: 1467 # Damp sticks\n  examine: \"Some damp wooden sticks.\"\n- id: 1468 # Dry sticks\n  examine: \"Some dry wooden sticks.\"\n- id: 1469 # Broken glass\n  examine: \"Smashed glass.\"\n- id: 1470 # Red bead\n  examine: \"A small round red bead.\"\n- id: 1472 # Yellow bead\n  examine: \"A small round yellow bead.\"\n- id: 1474 # Black bead\n  examine: \"A small round black bead.\"\n- id: 1476 # White bead\n  examine: \"A small round white bead.\"\n- id: 1478 # Amulet of accuracy\n  examine: \"It increases my aim.\"\n- id: 1480 # Rock\n  examine: \"A chunk of rock.\"\n- id: 1481 # Orb of light\n  examine: \"A magical sphere that glimmers within.\"\n- id: 1482 # Orb of light\n  examine: \"A magical sphere that glimmers within.\"\n- id: 1483 # Orb of light\n  examine: \"A magical sphere that glimmers within.\"\n- id: 1484 # Orb of light\n  examine: \"A magical sphere that glimmers within.\"\n- id: 1486 # Piece of railing\n  examine: \"A broken piece of railing.\"\n- id: 1487 # Unicorn horn\n  examine: \"A withered unicorn horn.\"\n- id: 1488 # Paladin's badge\n  examine: \"A coat of arms of the Ardougne Paladins.\"\n- id: 1489 # Paladin's badge\n  examine: \"A coat of arms of the Ardougne Paladins.\"\n- id: 1490 # Paladin's badge\n  examine: \"A coat of arms of the Ardougne Paladins.\"\n- id: 1491 # Witch's cat\n  examine: \"A cat.\"\n- id: 1492 # Doll of iban\n  examine: \"A simple doll with Iban's likeness.\"\n- id: 1493 # Old journal\n  examine: \"An account of the last times of someone.\"\n- id: 1494 # History of iban\n  examine: \"The tale of Iban.\"\n- id: 1495 # Klank's gauntlets\n  examine: \"Strong dwarvish gloves.\"\n- id: 1496 # Iban's dove\n  examine: \"I thought you only saw these in pairs?\"\n- id: 1497 # Amulet of othanian\n  examine: \"A mystical demonic amulet.\"\n- id: 1498 # Amulet of doomion\n  examine: \"A mystical demonic amulet.\"\n- id: 1499 # Amulet of holthion\n  examine: \"A mystical demonic amulet.\"\n- id: 1500 # Iban's shadow\n  examine: \"A strange dark liquid.\"\n- id: 1501 # Dwarf brew\n  examine: \"Smells stronger than most spirits.\"\n- id: 1502 # Iban's ashes\n  examine: \"The burnt remains of Iban.\"\n- id: 1503 # Warrant\n  examine: \"A search warrant for a house in Ardougne.\"\n- id: 1504 # Hangover cure\n  examine: \"It doesn't look very tasty.\"\n- id: 1506 # Gas mask\n  examine: \"Stops me from breathing nasty stuff!\"\n- id: 1507 # A small key\n  examine: \"Quite a small key.\"\n- id: 1508 # A scruffy note\n  examine: \"It seems to say \\\"hongorer lure\\\"...\"\n- id: 1509 # Book\n  examine: \"Turnip growing for beginners.\"\n- id: 1510 # Picture\n  examine: \"A picture of a lady called Elena.\"\n- id: 1511 # Logs\n  examine: \"A number of wooden logs.\"\n- id: 1513 # Magic logs\n  examine: \"Logs cut from a magic tree.\"\n- id: 1515 # Yew logs\n  examine: \"Logs cut from a yew tree.\"\n- id: 1517 # Maple logs\n  examine: \"Logs cut from a maple tree.\"\n- id: 1519 # Willow logs\n  examine: \"Logs cut from a willow tree.\"\n- id: 1521 # Oak logs\n  examine: \"Logs cut from an oak tree.\"\n- id: 1523 # Lockpick\n  examine: \"For picking tough locks.\"\n- id: 1526 # Snake weed\n  examine: \"This herb is Snake Weed.\"\n- id: 1528 # Ardrigal\n  examine: \"This herb is Ardrigal.\"\n- id: 1530 # Sito foil\n  examine: \"This herb is Sito Foil.\"\n- id: 1532 # Volencia moss\n  examine: \"This herb is Volencia Moss.\"\n- id: 1534 # Rogue's purse\n  examine: \"This herb is Rogue's Purse.\"\n- id: 1535 # Map part\n  examine: \"A piece of the map to Crandor.\"\n- id: 1536 # Map part\n  examine: \"A piece of the map to Crandor.\"\n- id: 1537 # Map part\n  examine: \"A piece of the map to Crandor.\"\n- id: 1538 # Crandor map\n  examine: \"A map of the route to Crandor.\"\n- id: 1539 # Steel nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 1540 # Anti-dragon shield\n  examine: \"This provides partial protection from dragonbreath attacks.\"\n- id: 1542 # Maze key\n  examine: \"A key to Melzar's Maze.\"\n- id: 1543 # Key\n  examine: \"A red key from Melzar's Maze.\"\n- id: 1544 # Key\n  examine: \"An orange key from Melzar's Maze.\"\n- id: 1545 # Key\n  examine: \"A yellow key from Melzar's Maze.\"\n- id: 1546 # Key\n  examine: \"A blue key from Melzar's Maze.\"\n- id: 1547 # Key\n  examine: \"A magenta key from Melzar's Maze.\"\n- id: 1548 # Key\n  examine: \"A green key from Melzar's Maze.\"\n- id: 1549 # Stake\n  examine: \"A very pointy stick.\"\n- id: 1550 # Garlic\n  examine: \"A clove of garlic.\"\n- id: 1552 # Seasoned sardine\n  examine: \"Sardine flavoured with doogle leaves.\"\n- id: 1554 # Fluffs' kitten\n  examine: \"It looks like it's lost.\"\n- id: 1555 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1556 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1557 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1558 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1559 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1560 # Pet kitten\n  examine: \"This kitten seems to like you.\"\n- id: 1561 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1562 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1563 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1564 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1565 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1566 # Pet cat\n  examine: \"This cat definitely likes you.\"\n- id: 1567 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1568 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1569 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1570 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1571 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1572 # Pet cat\n  examine: \"This cat is so well fed it can hardly move.\"\n- id: 1573 # Doogle leaves\n  examine: \"A tasty herb, good for seasoning.\"\n- id: 1575 # Cat training medal\n  examine: \"For feline training expertise.\"\n- id: 1577 # Pete's candlestick\n  examine: \"Scarface Pete's Candlestick.\"\n- id: 1579 # Thieves' armband\n  examine: \"This denotes a Master Thief.\"\n- id: 1580 # Ice gloves\n  examine: \"These will keep my hands cold!\"\n- id: 1581 # Blamish snail slime\n  examine: \"Yuck.\"\n- id: 1582 # Blamish oil\n  examine: \"Made from the finest snail slime.\"\n- id: 1583 # Fire feather\n  examine: \"Firebird feather.\"\n- id: 1584 # Id papers\n  examine: \"Apparently my name is Hartigen.\"\n- id: 1585 # Oily fishing rod\n  examine: \"Useful for catching lava eels.\"\n- id: 1586 # Miscellaneous key\n  examine: \"I wonder what this unlocks?.\"\n- id: 1590 # Dusty key\n  examine: \"Never let your home get like this.\"\n- id: 1591 # Jail key\n  examine: \"Key to a cell.\"\n- id: 1592 # Ring mould\n  examine: \"Used to make gold rings.\"\n- id: 1594 # Unholy mould\n  examine: \"Used to make unholy symbols.\"\n- id: 1595 # Amulet mould\n  examine: \"Used to make gold amulets.\"\n- id: 1597 # Necklace mould\n  examine: \"Used to make gold necklaces.\"\n- id: 1599 # Holy mould\n  examine: \"Used to make holy symbols of Saradomin.\"\n- id: 1601 # Diamond\n  examine: \"This looks valuable.\"\n- id: 1603 # Ruby\n  examine: \"This looks valuable.\"\n- id: 1605 # Emerald\n  examine: \"This looks valuable.\"\n- id: 1607 # Sapphire\n  examine: \"This looks valuable.\"\n- id: 1609 # Opal\n  examine: \"A semi precious stone.\"\n- id: 1611 # Jade\n  examine: \"A semi precious stone.\"\n- id: 1613 # Red topaz\n  examine: \"A semi precious stone.\"\n- id: 1615 # Dragonstone\n  examine: \"This looks valuable.\"\n- id: 1617 # Uncut diamond\n  examine: \"This would be worth more cut.\"\n- id: 1619 # Uncut ruby\n  examine: \"This would be worth more cut.\"\n- id: 1621 # Uncut emerald\n  examine: \"This would be worth more cut.\"\n- id: 1623 # Uncut sapphire\n  examine: \"This would be worth more cut.\"\n- id: 1625 # Uncut opal\n  examine: \"This would be worth more cut.\"\n- id: 1627 # Uncut jade\n  examine: \"This would be worth more cut.\"\n- id: 1629 # Uncut red topaz\n  examine: \"This would be worth more cut.\"\n- id: 1631 # Uncut dragonstone\n  examine: \"This would be worth more cut.\"\n- id: 1635 # Gold ring\n  examine: \"A valuable ring.\"\n- id: 1637 # Sapphire ring\n  examine: \"A valuable ring.\"\n- id: 1639 # Emerald ring\n  examine: \"A valuable ring.\"\n- id: 1641 # Ruby ring\n  examine: \"A valuable ring.\"\n- id: 1643 # Diamond ring\n  examine: \"A valuable ring.\"\n- id: 1645 # Dragonstone ring\n  examine: \"A valuable ring.\"\n- id: 1654 # Gold necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1656 # Sapphire necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1658 # Emerald necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1660 # Ruby necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1662 # Diamond necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1664 # Dragon necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 1692 # Gold amulet\n  examine: \"A plain gold amulet.\"\n- id: 1694 # Sapphire amulet\n  examine: \"I wonder if I can get this enchanted.\"\n- id: 1696 # Emerald amulet\n  examine: \"I wonder if I can get this enchanted.\"\n- id: 1698 # Ruby amulet\n  examine: \"I wonder if I can get this enchanted.\"\n- id: 1700 # Diamond amulet\n  examine: \"I wonder if I can get this enchanted.\"\n- id: 1704 # Amulet of glory\n  examine: \"A very powerful dragonstone amulet.\"\n- id: 1706 # Amulet of glory(1)\n  examine: \"A dragonstone amulet with 1 magic charge.\"\n- id: 1708 # Amulet of glory(2)\n  examine: \"A dragonstone amulet with 2 magic charges.\"\n- id: 1710 # Amulet of glory(3)\n  examine: \"A dragonstone amulet with 3 magic charges.\"\n- id: 1712 # Amulet of glory(4)\n  examine: \"A dragonstone amulet with 4 magic charges.\"\n- id: 1714 # Unstrung symbol\n  examine: \"It needs a string so I can wear it.\"\n- id: 1716 # Unblessed symbol\n  examine: \"A symbol of Saradomin.\"\n- id: 1718 # Holy symbol\n  examine: \"A blessed holy symbol of Saradomin.\"\n- id: 1720 # Unstrung emblem\n  examine: \"It needs a string so I can wear it.\"\n- id: 1722 # Unpowered symbol\n  examine: \"An unblessed symbol of Zamorak.\"\n- id: 1724 # Unholy symbol\n  examine: \"An unholy symbol of Zamorak.\"\n- id: 1725 # Amulet of strength\n  examine: \"An enchanted ruby amulet.\"\n- id: 1727 # Amulet of magic\n  examine: \"An enchanted sapphire amulet of magic.\"\n- id: 1729 # Amulet of defence\n  examine: \"An enchanted emerald amulet of protection.\"\n- id: 1731 # Amulet of power\n  examine: \"An enchanted diamond amulet of power.\"\n- id: 1733 # Needle\n  examine: \"Used with a thread to make clothes.\"\n- id: 1734 # Thread\n  examine: \"Used with a needle to make clothes.\"\n- id: 1735 # Shears\n  examine: \"For shearing sheep.\"\n- id: 1737 # Wool\n  examine: \"I think this came from a sheep.\"\n- id: 1739 # Cowhide\n  examine: \"I should take this to the tannery.\"\n- id: 1741 # Leather\n  examine: \"It's a piece of leather.\"\n- id: 1743 # Hard leather\n  examine: \"It's a piece of hard leather.\"\n- id: 1747 # Black dragonhide\n  examine: \"The scaly rough hide from a Black Dragon.\"\n- id: 1749 # Red dragonhide\n  examine: \"The scaly rough hide from a Red Dragon.\"\n- id: 1751 # Blue dragonhide\n  examine: \"The scaly rough hide from a Blue Dragon.\"\n- id: 1753 # Green dragonhide\n  examine: \"The scaly rough hide from a Green Dragon.\"\n- id: 1755 # Chisel\n  examine: \"Good for detailed Crafting.\"\n- id: 1757 # Brown apron\n  examine: \"A mostly clean apron.\"\n- id: 1759 # Ball of wool\n  examine: \"Spun from sheeps' wool.\"\n- id: 1761 # Soft clay\n  examine: \"Clay soft enough to mould.\"\n- id: 1763 # Red dye\n  examine: \"A little bottle of red dye.\"\n- id: 1765 # Yellow dye\n  examine: \"A little bottle of yellow dye.\"\n- id: 1767 # Blue dye\n  examine: \"A little bottle of blue dye.\"\n- id: 1769 # Orange dye\n  examine: \"A little bottle of orange dye.\"\n- id: 1771 # Green dye\n  examine: \"A little bottle of green dye.\"\n- id: 1773 # Purple dye\n  examine: \"A little bottle of purple dye.\"\n- id: 1775 # Molten glass\n  examine: \"Hot glass ready to be blown into useful objects.\"\n- id: 1777 # Bow string\n  examine: \"I need a bow stave to attach this to.\"\n- id: 1779 # Flax\n  examine: \"I should use this with a spinning wheel.\"\n- id: 1781 # Soda ash\n  examine: \"One of the ingredients for making glass.\"\n- id: 1783 # Bucket of sand\n  examine: \"One of the ingredients for making glass.\"\n- id: 1785 # Glassblowing pipe\n  examine: \"Used to form molten glass into useful items.\"\n- id: 1787 # Unfired pot\n  examine: \"I need to put this in a pottery oven.\"\n- id: 1789 # Unfired pie dish\n  examine: \"I need to put this in a pottery oven.\"\n- id: 1791 # Unfired bowl\n  examine: \"I need to put this in a pottery oven.\"\n- id: 1793 # Woad leaf\n  examine: \"A slightly bluish leaf.\"\n- id: 1794 # Bronze wire\n  examine: \"Useful for Crafting items.\"\n- id: 1796 # Silver necklace\n  examine: \"Anna's shiny silver coated necklace.\"\n- id: 1797 # Silver necklace\n  examine: \"Anna's shiny silver coated necklace coated with a thin layer of flour.\"\n- id: 1798 # Silver cup\n  examine: \"Bob's shiny silver coated tea cup.\"\n- id: 1799 # Silver cup\n  examine: \"Bob's shiny silver coated tea cup coated with a thin layer of flour.\"\n- id: 1800 # Silver bottle\n  examine: \"Carol's shiny silver coated bottle.\"\n- id: 1801 # Silver bottle\n  examine: \"Carol's shiny silver coated bottle coated with a thin layer of flour.\"\n- id: 1802 # Silver book\n  examine: \"David's shiny silver coated book.\"\n- id: 1803 # Silver book\n  examine: \"David's shiny silver coated book coated with a thin layer of flour.\"\n- id: 1804 # Silver needle\n  examine: \"Elizabeth's shiny silver coated needle.\"\n- id: 1805 # Silver needle\n  examine: \"Elizabeth's shiny silver coated needle coated with a thin layer of flour.\"\n- id: 1806 # Silver pot\n  examine: \"Frank's shiny silver coated pot.\"\n- id: 1807 # Silver pot\n  examine: \"Frank's shiny silver coated pot coated with a thin layer of flour.\"\n- id: 1808 # Criminal's thread\n  examine: \"Some red thread found at the murder scene.\"\n- id: 1809 # Criminal's thread\n  examine: \"Some green thread found at the murder scene.\"\n- id: 1810 # Criminal's thread\n  examine: \"Some blue thread found at the murder scene.\"\n- id: 1811 # Flypaper\n  examine: \"A piece of fly paper. It's sticky.\"\n- id: 1812 # Pungent pot\n  examine: \"A pot found at the murder scene, with a sickly odour.\"\n- id: 1813 # Criminal's dagger\n  examine: \"A flimsy-looking dagger found at the crime scene.\"\n- id: 1814 # Criminal's dagger\n  examine: \"A flimsy-looking dagger found at the crime scene coated with a thin layer of flour.\"\n- id: 1815 # Killer's print\n  examine: \"The fingerprints of the murderer.\"\n- id: 1816 # Anna's print\n  examine: \"An imprint of Anna's fingerprint.\"\n- id: 1817 # Bob's print\n  examine: \"An imprint of Bob's fingerprint.\"\n- id: 1818 # Carol's print\n  examine: \"An imprint of Carol's fingerprint.\"\n- id: 1819 # David's print\n  examine: \"An imprint of David's fingerprint.\"\n- id: 1820 # Elizabeth's print\n  examine: \"An imprint of Elizabeth's fingerprint.\"\n- id: 1821 # Frank's print\n  examine: \"An imprint of Frank's fingerprint.\"\n- id: 1822 # Unknown print\n  examine: \"An unidentified fingerprint taken from the murder weapon.\"\n- id: 1823 # Waterskin(4)\n  examine: \"A full waterskin with four portions of water.\"\n- id: 1825 # Waterskin(3)\n  examine: \"A nearly full waterskin with three portions of water.\"\n- id: 1827 # Waterskin(2)\n  examine: \"A half empty waterskin with two portions of water.\"\n- id: 1829 # Waterskin(1)\n  examine: \"A nearly empty waterskin with one portion of water.\"\n- id: 1831 # Waterskin(0)\n  examine: \"A completely empty waterskin - you'll need to fill it up.\"\n- id: 1833 # Desert shirt\n  examine: \"A cool, light desert shirt.\"\n- id: 1835 # Desert robe\n  examine: \"A cool, light desert robe.\"\n- id: 1837 # Desert boots\n  examine: \"Comfortable desert shoes.\"\n- id: 1839 # Metal key\n  examine: \"This key is crudely made. It came from the mining camp Mercenary Captain.\"\n- id: 1840 # Cell door key\n  examine: \"A metallic key, usually used by prison guards.\"\n- id: 1841 # Barrel\n  examine: \"An empty mining barrel.\"\n- id: 1842 # Ana in a barrel\n  examine: \"A mining barrel with Ana in it.\"\n- id: 1843 # Wrought iron key\n  examine: \"This key unlocks a very sturdy gate of some sort. Ana gave me this key.\"\n- id: 1844 # Slave shirt\n  examine: \"A filthy, smelly, flea infested shirt.\"\n- id: 1845 # Slave robe\n  examine: \"A filthy, smelly, flea infested robe.\"\n- id: 1846 # Slave boots\n  examine: \"A set of filthy, smelly, flea infested desert slave boots.\"\n- id: 1847 # Scrumpled paper\n  examine: \"A piece of paper with barely legible writing - looks like a recipe!\"\n- id: 1849 # Prototype dart\n  examine: \"A prototype throwing dart.\"\n- id: 1850 # Technical plans\n  examine: \"Plans of a technical nature.\"\n- id: 1851 # Tenti pineapple\n  examine: \"The most delicious of pineapples.\"\n- id: 1852 # Bedabin key\n  examine: \"A key to the chest in Captain Siad's room.\"\n- id: 1853 # Prototype dart tip\n  examine: \"A prototype dart tip - it looks deadly.\"\n- id: 1854 # Shantay pass\n  examine: \"Allows you to pass through the Shantay pass into the Kharid Desert.\"\n- id: 1855 # Rock\n  examine: \"Looks like a plain rock, must have some ore in it?\"\n- id: 1856 # Guide book\n  examine: \"A Tourist's Guide To Ardougne.\"\n- id: 1857 # Totem\n  examine: \"The Rantuki tribe's totem.\"\n- id: 1858 # Address label\n  examine: \"It says 'To Lord Handelmort, Handelmort Mansion'.\"\n- id: 1859 # Raw ugthanki meat\n  examine: \"I need to cook this first.\"\n- id: 1861 # Ugthanki meat\n  examine: \"Freshly cooked ugthanki meat.\"\n- id: 1863 # Pitta dough\n  examine: \"I need to cook this.\"\n- id: 1865 # Pitta bread\n  examine: \"Nicely baked pitta bread. Needs more ingredients to make a kebab.\"\n- id: 1869 # Chopped tomato\n  examine: \"A mixture of tomatoes in a bowl.\"\n- id: 1871 # Chopped onion\n  examine: \"A mixture of onions in a bowl.\"\n- id: 1873 # Chopped ugthanki\n  examine: \"Strips of ugthanki meat in a bowl.\"\n- id: 1875 # Onion & tomato\n  examine: \"A mixture of chopped onions and tomatoes in a bowl.\"\n- id: 1877 # Ugthanki & onion\n  examine: \"A mixture of chopped onions and ugthanki meat in a bowl.\"\n- id: 1879 # Ugthanki & tomato\n  examine: \"A mixture of chopped tomatoes and ugthanki meat in a bowl.\"\n- id: 1881 # Kebab mix\n  examine: \"A mixture of chopped tomatoes, onions and ugthanki meat in a bowl.\"\n- id: 1883 # Ugthanki kebab\n  examine: \"A strange smelling kebab made from ugthanki meat.\"\n- id: 1885 # Ugthanki kebab\n  examine: \"A fresh kebab made from ugthanki meat.\"\n- id: 1887 # Cake tin\n  examine: \"Useful for baking cakes.\"\n- id: 1889 # Uncooked cake\n  examine: \"Now all I need to do is cook it.\"\n- id: 1891 # Cake\n  examine: \"A plain sponge cake.\"\n- id: 1893 # 2/3 cake\n  examine: \"Someone has eaten a big chunk of this cake.\"\n- id: 1895 # Slice of cake\n  examine: \"I'd rather have a whole cake.\"\n- id: 1897 # Chocolate cake\n  examine: \"This looks very tasty.\"\n- id: 1899 # 2/3 chocolate cake\n  examine: \"Someone has eaten a big chunk of this cake.\"\n- id: 1901 # Chocolate slice\n  examine: \"I'd rather have a whole cake.\"\n- id: 1903 # Burnt cake\n  examine: \"Argh what a mess!\"\n- id: 1905 # Asgarnian ale\n  examine: \"Probably the finest ale in Asgarnia.\"\n- id: 1907 # Wizard's mind bomb\n  examine: \"It's got strange bubbles in it.\"\n- id: 1909 # Greenman's ale\n  examine: \"A glass of frothy ale.\"\n- id: 1911 # Dragon bitter\n  examine: \"A glass of bitter.\"\n- id: 1913 # Dwarven stout\n  examine: \"A pint of thick dark beer.\"\n- id: 1915 # Grog\n  examine: \"A murky glass of some sort of drink.\"\n- id: 1917 # Beer\n  examine: \"A glass of frothy ale.\"\n- id: 1919 # Beer glass\n  examine: \"I need to fill this with beer.\"\n- id: 1921 # Bowl of water\n  examine: \"It's a bowl of water.\"\n- id: 1923 # Bowl\n  examine: \"Useful for mixing things.\"\n- id: 1925 # Bucket\n  examine: \"It's a wooden bucket.\"\n- id: 1927 # Bucket of milk\n  examine: \"It's a bucket of milk.\"\n- id: 1929 # Bucket of water\n  examine: \"It's a bucket of water.\"\n- id: 1931 # Pot\n  examine: \"This pot is empty.\"\n- id: 1933 # Pot of flour\n  examine: \"There is flour in this pot.\"\n- id: 1935 # Jug\n  examine: \"This jug is empty.\"\n- id: 1937 # Jug of water\n  examine: \"It's full of water.\"\n- id: 1939 # Swamp tar\n  examine: \"A foul smelling thick tar-like substance.\"\n- id: 1940 # Raw swamp paste\n  examine: \"A thick tar-like substance mixed with flour.\"\n- id: 1941 # Swamp paste\n  examine: \"A tar-like substance mixed with flour and warmed.\"\n- id: 1942 # Potato\n  examine: \"This could be used to make a good stew.\"\n- id: 1944 # Egg\n  examine: \"A nice fresh egg.\"\n- id: 1947 # Grain\n  examine: \"Some wheat heads.\"\n- id: 1949 # Chef's hat\n  examine: \"What a silly hat.\"\n- id: 1951 # Redberries\n  examine: \"Very bright red berries.\"\n- id: 1953 # Pastry dough\n  examine: \"Potentially pastry.\"\n- id: 1955 # Cooking apple\n  examine: \"Keeps the doctor away.\"\n- id: 1957 # Onion\n  examine: \"A strong smelling onion.\"\n- id: 1959 # Pumpkin\n  examine: \"Happy Halloween.\"\n- id: 1961 # Easter egg\n  examine: \"Happy Easter.\"\n- id: 1963 # Banana\n  examine: \"Mmm this looks tasty.\"\n- id: 1965 # Cabbage\n  examine: \"Yuck I don't like cabbage.\"\n- id: 1967 # Cabbage\n  examine: \"Yuck I don't like cabbage.\"\n- id: 1969 # Spinach roll\n  examine: \"A home made spinach thing.\"\n- id: 1971 # Kebab\n  examine: \"A meaty kebab.\"\n- id: 1973 # Chocolate bar\n  examine: \"Mmmmmmm chocolate.\"\n- id: 1975 # Chocolate dust\n  examine: \"It's ground up chocolate.\"\n- id: 1977 # Chocolatey milk\n  examine: \"Milk with chocolate in it.\"\n- id: 1978 # Cup of tea\n  examine: \"A nice cup of tea.\"\n- id: 1980 # Empty cup\n  examine: \"An empty cup.\"\n- id: 1982 # Tomato\n  examine: \"This would make good ketchup.\"\n- id: 1984 # Rotten apple\n  examine: \"Rotten to the core!\"\n- id: 1985 # Cheese\n  examine: \"It's got holes in it.\"\n- id: 1987 # Grapes\n  examine: \"Good grapes for wine making.\"\n- id: 1989 # Half full wine jug\n  examine: \"An optimist would say it's half full.\"\n- id: 1991 # Jug of bad wine\n  examine: \"Oh dear, this wine is terrible!\"\n- id: 1993 # Jug of wine\n  examine: \"It's full of wine.\"\n- id: 1995 # Unfermented wine\n  examine: \"This wine needs to ferment before it can be drunk.\"\n- id: 1997 # Incomplete stew\n  examine: \"I need to add some meat too.\"\n- id: 1999 # Incomplete stew\n  examine: \"I need to add some potato too.\"\n- id: 2001 # Uncooked stew\n  examine: \"I need to cook this.\"\n- id: 2003 # Stew\n  examine: \"It's a meat and potato stew.\"\n- id: 2005 # Burnt stew\n  examine: \"Eew, it's horribly burnt.\"\n- id: 2007 # Spice\n  examine: \"This could liven up an otherwise bland stew.\"\n- id: 2009 # Uncooked curry\n  examine: \"I need to cook this.\"\n- id: 2011 # Curry\n  examine: \"It's a spicy hot curry.\"\n- id: 2013 # Burnt curry\n  examine: \"Eew, it's horribly burnt.\"\n- id: 2015 # Vodka\n  examine: \"An absolutely clear spirit.\"\n- id: 2017 # Whisky\n  examine: \"A bottle of Draynor Malt.\"\n- id: 2019 # Gin\n  examine: \"A strong spirit that tastes of Juniper.\"\n- id: 2021 # Brandy\n  examine: \"A strong spirit best served in a large glass.\"\n- id: 2023 # Cocktail guide\n  examine: \"A book on tree gnome cocktails.\"\n- id: 2025 # Cocktail shaker\n  examine: \"Used for mixing cocktails.\"\n- id: 2026 # Cocktail glass\n  examine: \"For sipping cocktails.\"\n- id: 2030 # Premade choc s'dy\n  examine: \"A premade Chocolate Saturday.\"\n- id: 2032 # Premade dr' dragon\n  examine: \"A premade Drunk Dragon.\"\n- id: 2034 # Premade fr' blast\n  examine: \"A premade Fruit Blast.\"\n- id: 2036 # Premade p' punch\n  examine: \"A premade Pineapple punch.\"\n- id: 2038 # Premade sgg\n  examine: \"A premade Short Green Guy.\"\n- id: 2040 # Premade wiz blz'd\n  examine: \"A Premade Wizard Blizzard.\"\n- id: 2048 # Pineapple punch\n  examine: \"A fresh healthy fruit mix.\"\n- id: 2054 # Wizard blizzard\n  examine: \"This looks like a strange mix.\"\n- id: 2064 # Blurberry special\n  examine: \"Looks good... smells strong.\"\n- id: 2074 # Choc saturday\n  examine: \"A warm creamy alcoholic beverage.\"\n- id: 2080 # Short green guy\n  examine: \"A Short Green Guy... looks good.\"\n- id: 2084 # Fruit blast\n  examine: \"A cool refreshing fruit mix.\"\n- id: 2092 # Drunk dragon\n  examine: \"A warm creamy alcoholic beverage.\"\n- id: 2102 # Lemon\n  examine: \"A fresh lemon.\"\n- id: 2104 # Lemon chunks\n  examine: \"Fresh chunks of lemon.\"\n- id: 2106 # Lemon slices\n  examine: \"Fresh lemon slices.\"\n- id: 2108 # Orange\n  examine: \"A fresh orange.\"\n- id: 2110 # Orange chunks\n  examine: \"Fresh chunks of orange.\"\n- id: 2112 # Orange slices\n  examine: \"Fresh orange slices.\"\n- id: 2114 # Pineapple\n  examine: \"It can be cut up into something more manageable with a knife.\"\n- id: 2116 # Pineapple chunks\n  examine: \"Fresh chunks of pineapple.\"\n- id: 2118 # Pineapple ring\n  examine: \"Exotic fruit.\"\n- id: 2120 # Lime\n  examine: \"A fresh lime.\"\n- id: 2122 # Lime chunks\n  examine: \"Fresh chunks of lime.\"\n- id: 2124 # Lime slices\n  examine: \"Fresh lime slices.\"\n- id: 2126 # Dwellberries\n  examine: \"Some rather pretty blue berries.\"\n- id: 2128 # Equa leaves\n  examine: \"Small sweet smelling leaves.\"\n- id: 2130 # Pot of cream\n  examine: \"Fresh cream.\"\n- id: 2132 # Raw beef\n  examine: \"I need to cook this first.\"\n- id: 2134 # Raw rat meat\n  examine: \"I need to cook this first.\"\n- id: 2136 # Raw bear meat\n  examine: \"I need to cook this first.\"\n- id: 2138 # Raw chicken\n  examine: \"I need to cook this first.\"\n- id: 2140 # Cooked chicken\n  examine: \"Mmm this looks tasty.\"\n- id: 2142 # Cooked meat\n  examine: \"Mmm this looks tasty.\"\n- id: 2144 # Burnt chicken\n  examine: \"Oh dear, it's totally burnt!\"\n- id: 2146 # Burnt meat\n  examine: \"Oh dear, it's totally burnt!\"\n- id: 2148 # Raw lava eel\n  examine: \"A very strange eel.\"\n- id: 2149 # Lava eel\n  examine: \"Strange, it looks cooler now it's been cooked.\"\n- id: 2150 # Swamp toad\n  examine: \"A slippery little blighter.\"\n- id: 2152 # Toad's legs\n  examine: \"They're a gnome delicacy apparently.\"\n- id: 2162 # King worm\n  examine: \"They're a gnome delicacy apparently.\"\n- id: 2164 # Batta tin\n  examine: \"A deep tin used for baking gnome battas in.\"\n- id: 2165 # Crunchy tray\n  examine: \"A shallow tray used for baking crunchies in.\"\n- id: 2166 # Gnomebowl mould\n  examine: \"A large ovenproof bowl.\"\n- id: 2167 # Gianne's cook book\n  examine: \"Aluft Gianne's favorite dishes.\"\n- id: 2169 # Gnome spice\n  examine: \"It's Aluft Gianne's secret mix of spices.\"\n- id: 2171 # Gianne dough\n  examine: \"It's made from a secret recipe.\"\n- id: 2175 # Burnt gnomebowl\n  examine: \"This gnome bowl has been burnt to a cinder.\"\n- id: 2177 # Half baked bowl\n  examine: \"This gnome bowl is in the early stages of preparation.\"\n- id: 2178 # Raw gnomebowl\n  examine: \"This gnome bowl needs cooking.\"\n- id: 2185 # Chocolate bomb\n  examine: \"Full of creamy, chocolately goodness.\"\n- id: 2187 # Tangled toad's legs\n  examine: \"It actually smells quite good.\"\n- id: 2191 # Worm hole\n  examine: \"It actually smells quite good.\"\n- id: 2195 # Veg ball\n  examine: \"This looks pretty healthy.\"\n- id: 2199 # Burnt crunchies\n  examine: \"These crunchies have been burnt to a cinder.\"\n- id: 2201 # Half baked crunchy\n  examine: \"This crunchy is in the early stages of preparation.\"\n- id: 2202 # Raw crunchies\n  examine: \"These crunchies need cooking.\"\n- id: 2205 # Worm crunchies\n  examine: \"It actually smells quite good.\"\n- id: 2209 # Chocchip crunchies\n  examine: \"Yum... smells good.\"\n- id: 2213 # Spicy crunchies\n  examine: \"Yum... smells spicy.\"\n- id: 2217 # Toad crunchies\n  examine: \"It actually smells quite good.\"\n- id: 2219 # Premade w'm batta\n  examine: \"A premade Worm Batta.\"\n- id: 2221 # Premade t'd batta\n  examine: \"A Premade Toad Batta.\"\n- id: 2223 # Premade c+t batta\n  examine: \"A Premade Cheese and Tomato Batta.\"\n- id: 2225 # Premade fr't batta\n  examine: \"A premade Fruit Batta.\"\n- id: 2227 # Premade veg batta\n  examine: \"A Premade Vegetable Batta.\"\n- id: 2229 # Premade choc bomb\n  examine: \"A premade Chocolate Bomb.\"\n- id: 2231 # Premade ttl\n  examine: \"A premade Tangled Toads Legs.\"\n- id: 2233 # Premade worm hole\n  examine: \"A premade Worm Hole.\"\n- id: 2235 # Premade veg ball\n  examine: \"A premade Vegetable Ball.\"\n- id: 2237 # Premade w'm crun'\n  examine: \"Some Premade Worm Crunchies.\"\n- id: 2239 # Premade ch' crunch\n  examine: \"Some Premade chocchip crunchies.\"\n- id: 2241 # Premade s'y crunch\n  examine: \"Some premade Spicy Crunchies.\"\n- id: 2243 # Premade t'd crunch\n  examine: \"Some premade Toad Crunchies.\"\n- id: 2247 # Burnt batta\n  examine: \"This batta has been burnt to a cinder.\"\n- id: 2249 # Half baked batta\n  examine: \"This gnome batta is in the early stages of preparation.\"\n- id: 2250 # Raw batta\n  examine: \"This gnome batta needs cooking.\"\n- id: 2251 # Unfinished batta\n  examine: \"This batta is just missing those little finishing touches.\"\n- id: 2253 # Worm batta\n  examine: \"It actually smells quite good.\"\n- id: 2255 # Toad batta\n  examine: \"It actually smells quite good.\"\n- id: 2257 # Unfinished batta\n  examine: \"This batta is just missing those little finishing touches.\"\n- id: 2259 # Cheese+tom batta\n  examine: \"This smells really good.\"\n- id: 2277 # Fruit batta\n  examine: \"It actually smells quite good.\"\n- id: 2279 # Unfinished batta\n  examine: \"This batta is just missing those little finishing touches.\"\n- id: 2281 # Vegetable batta\n  examine: \"Well... it looks healthy.\"\n- id: 2283 # Pizza base\n  examine: \"I need to add some tomato next.\"\n- id: 2285 # Incomplete pizza\n  examine: \"I need to add some cheese next.\"\n- id: 2287 # Uncooked pizza\n  examine: \"This needs cooking.\"\n- id: 2289 # Plain pizza\n  examine: \"A cheese and tomato pizza.\"\n- id: 2291 # 1/2 plain pizza\n  examine: \"Half of this plain pizza has been eaten.\"\n- id: 2293 # Meat pizza\n  examine: \"A pizza with bits of meat on it.\"\n- id: 2295 # 1/2 meat pizza\n  examine: \"Half of this meat pizza has been eaten.\"\n- id: 2297 # Anchovy pizza\n  examine: \"A pizza with anchovies.\"\n- id: 2299 # 1/2 anchovy pizza\n  examine: \"Half of this anchovy pizza has been eaten.\"\n- id: 2301 # Pineapple pizza\n  examine: \"A tropicana pizza.\"\n- id: 2305 # Burnt pizza\n  examine: \"Oh dear!\"\n- id: 2307 # Bread dough\n  examine: \"Some uncooked dough.\"\n- id: 2309 # Bread\n  examine: \"Nice crispy bread.\"\n- id: 2311 # Burnt bread\n  examine: \"Nice crispy bread. Possibly too crispy.\"\n- id: 2313 # Pie dish\n  examine: \"Deceptively pie shaped.\"\n- id: 2315 # Pie shell\n  examine: \"I need to find a filling for this pie.\"\n- id: 2317 # Uncooked apple pie\n  examine: \"This would be much tastier cooked.\"\n- id: 2319 # Uncooked meat pie\n  examine: \"This would be much healthier cooked.\"\n- id: 2321 # Uncooked berry pie\n  examine: \"This would be much more appetising cooked.\"\n- id: 2323 # Apple pie\n  examine: \"Mmm Apple pie.\"\n- id: 2325 # Redberry pie\n  examine: \"Looks tasty.\"\n- id: 2327 # Meat pie\n  examine: \"Not for vegetarians.\"\n- id: 2329 # Burnt pie\n  examine: \"I think I left it on the stove too long.\"\n- id: 2331 # Half a meat pie\n  examine: \"Half of it is suitable for vegetarians.\"\n- id: 2333 # Half a redberry pie\n  examine: \"So tasty I kept some for later.\"\n- id: 2335 # Half an apple pie\n  examine: \"Mmm half an apple pie.\"\n- id: 2337 # Raw oomlie\n  examine: \"Raw meat from the oomlie bird.\"\n- id: 2339 # Palm leaf\n  examine: \"A thick green palm leaf used by natives to cook meat.\"\n- id: 2341 # Wrapped oomlie\n  examine: \"Oomlie meat in a palm leaf pouch. It just needs to be cooked.\"\n- id: 2343 # Cooked oomlie wrap\n  examine: \"Deliciously cooked oomlie meat in a palm leaf pouch.\"\n- id: 2347 # Hammer\n  examine: \"Good for hitting things!\"\n- id: 2349 # Bronze bar\n  examine: \"It's a bar of bronze.\"\n- id: 2351 # Iron bar\n  examine: \"It's a bar of iron.\"\n- id: 2353 # Steel bar\n  examine: \"It's a bar of steel.\"\n- id: 2355 # Silver bar\n  examine: \"It's a bar of silver.\"\n- id: 2357 # Gold bar\n  examine: \"It's a bar of gold.\"\n- id: 2359 # Mithril bar\n  examine: \"It's a bar of mithril.\"\n- id: 2361 # Adamantite bar\n  examine: \"It's a bar of adamantite.\"\n- id: 2363 # Runite bar\n  examine: \"It's a bar of runite.\"\n- id: 2365 # 'perfect' gold bar\n  examine: \"It's a bar of 'perfect' gold.\"\n- id: 2366 # Shield left half\n  examine: \"The left half of a dragon square shield.\"\n- id: 2368 # Shield right half\n  examine: \"The right half of a dragon square shield.\"\n- id: 2370 # Steel studs\n  examine: \"A set of studs for leather armour.\"\n- id: 2372 # Ogre relic\n  examine: \"An old statue of an ogre warrior.\"\n- id: 2373 # Relic part 1\n  examine: \"Part of an ogre relic.\"\n- id: 2374 # Relic part 2\n  examine: \"Part of an ogre relic.\"\n- id: 2375 # Relic part 3\n  examine: \"Part of an ogre relic.\"\n- id: 2376 # Skavid map\n  examine: \"It's a map.\"\n- id: 2377 # Ogre tooth\n  examine: \"Very tooth-like.\"\n- id: 2378 # Toban's key\n  examine: \"Formerly the property of the ogre, Toban.\"\n- id: 2379 # Rock cake\n  examine: \"Handy if you want to break all your teeth.\"\n- id: 2380 # Crystal\n  examine: \"A yellow crystal that's meant to power the Watchtower in Yanille.\"\n- id: 2381 # Crystal\n  examine: \"A magenta crystal that's meant to power the Watchtower in Yanille.\"\n- id: 2382 # Crystal\n  examine: \"A cyan crystal that's meant to power the Watchtower in Yanille.\"\n- id: 2383 # Crystal\n  examine: \"A grey crystal that's meant to power the Watchtower in Yanille.\"\n- id: 2385 # Old robe\n  examine: \"I can't wear this old thing.\"\n- id: 2386 # Unusual armour\n  examine: \"Looks kind of useless.\"\n- id: 2387 # Damaged dagger\n  examine: \"Pointy.\"\n- id: 2388 # Tattered eye patch\n  examine: \"Useless as an eye patch.\"\n- id: 2389 # Vial\n  examine: \"An infusion of water and jangerberries.\"\n- id: 2390 # Vial\n  examine: \"A mixture of jangerberries and a guam leaf in a vial.\"\n- id: 2391 # Ground bat bones\n  examine: \"Let's see it fly, now!\"\n- id: 2394 # Potion\n  examine: \"A strange brew.\"\n- id: 2395 # Magic ogre potion\n  examine: \"A dangerous magical liquid.\"\n- id: 2396 # Spell scroll\n  examine: \"A spell is written on this parchment.\"\n- id: 2397 # Shaman robe\n  examine: \"A tattered old robe.\"\n- id: 2399 # Silverlight key\n  examine: \"The Wizard Traiborn gave me this key to Silverlight's case.\"\n- id: 2400 # Silverlight key\n  examine: \"Captain Rovin gave me this key to Silverlight's case.\"\n- id: 2401 # Silverlight key\n  examine: \"Sir Prysin dropped this key down the drain.\"\n- id: 2402 # Silverlight\n  examine: \"The magical sword 'Silverlight'.\"\n- id: 2403 # Hazeel scroll\n  examine: \"Scroll containing a powerful enchantment of restoration.\"\n- id: 2404 # Chest key\n  examine: \"This key opens a chest in the Carnillean household.\"\n- id: 2405 # Carnillean armour\n  examine: \"Decorative armour; an heirloom of the Carnillean family.\"\n- id: 2406 # Hazeel's mark\n  examine: \"A sign of my commitment to Hazeel.\"\n- id: 2407 # Ball\n  examine: \"A child's ball.\"\n- id: 2408 # Diary\n  examine: \"A daily journal.\"\n- id: 2409 # Door key\n  examine: \"A key to the Witch's house's front door.\"\n- id: 2410 # Magnet\n  examine: \"A very attractive magnet.\"\n- id: 2411 # Key\n  examine: \"A key to the Witch's shed.\"\n- id: 2412 # Saradomin cape\n  examine: \"A cape from the almighty god Saradomin.\"\n- id: 2413 # Guthix cape\n  examine: \"A cape from the almighty god Guthix.\"\n- id: 2414 # Zamorak cape\n  examine: \"A cape from the almighty god Zamorak.\"\n- id: 2415 # Saradomin staff\n  examine: \"A magical staff imbued with the power of Saradomin.\"\n- id: 2416 # Guthix staff\n  examine: \"A magical staff imbued with the power of Guthix.\"\n- id: 2417 # Zamorak staff\n  examine: \"A magical staff imbued with the power of Zamorak.\"\n- id: 2418 # Bronze key\n  examine: \"A heavy key made of bronze.\"\n- id: 2419 # Wig\n  examine: \"A wig that has been dyed slightly blonde.\"\n- id: 2421 # Wig\n  examine: \"A grey woollen wig.\"\n- id: 2422 # Blue partyhat\n  examine: \"A nice hat from a cracker.\"\n- id: 2423 # Key print\n  examine: \"An imprint of a key in a lump of clay.\"\n- id: 2424 # Paste\n  examine: \"A bottle of skin coloured paste.\"\n- id: 2426 # Burnt oomlie\n  examine: \"Oh dear, it's totally burnt!\"\n- id: 2428 # Attack potion(4)\n  examine: \"4 doses of Attack potion.\"\n- id: 2430 # Restore potion(4)\n  examine: \"4 doses of restore potion.\"\n- id: 2432 # Defence potion(4)\n  examine: \"4 doses of Defence potion.\"\n- id: 2434 # Prayer potion(4)\n  examine: \"4 doses of Prayer restore potion.\"\n- id: 2436 # Super attack(4)\n  examine: \"4 doses of super Attack potion.\"\n- id: 2438 # Fishing potion(4)\n  examine: \"4 doses of Fishing potion.\"\n- id: 2440 # Super strength(4)\n  examine: \"4 doses of super Strength potion.\"\n- id: 2442 # Super defence(4)\n  examine: \"4 doses of super Defence potion.\"\n- id: 2444 # Ranging potion(4)\n  examine: \"4 doses of ranging potion.\"\n- id: 2446 # Antipoison(4)\n  examine: \"4 doses of antipoison potion.\"\n- id: 2448 # Superantipoison(4)\n  examine: \"4 doses of super antipoison potion.\"\n- id: 2450 # Zamorak brew(4)\n  examine: \"4 doses of Zamorak brew.\"\n- id: 2452 # Antifire potion(4)\n  examine: \"4 doses of anti-firebreath potion.\"\n- id: 2454 # Antifire potion(3)\n  examine: \"3 doses of anti-firebreath potion.\"\n- id: 2456 # Antifire potion(2)\n  examine: \"2 doses of anti-firebreath potion.\"\n- id: 2458 # Antifire potion(1)\n  examine: \"1 dose of anti-firebreath potion.\"\n- id: 2481 # Lantadyme\n  examine: \"A powerful herb.\"\n- id: 2493 # Blue d'hide chaps\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2495 # Red d'hide chaps\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2497 # Black d'hide chaps\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2499 # Blue d'hide body\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2501 # Red d'hide body\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2503 # Black d'hide body\n  examine: \"Made from 100% real dragonhide.\"\n- id: 2507 # Red dragon leather\n  examine: \"It's a piece of prepared red dragonhide.\"\n- id: 2511 # Logs\n  examine: \"A number of wooden logs.\"\n- id: 2513 # Dragon chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 2514 # Raw shrimps\n  examine: \"I should try cooking this.\"\n- id: 2516 # Pot of flour\n  examine: \"There is flour in this pot.\"\n- id: 2518 # Rotten tomato\n  examine: \"Pretty smelly.\"\n- id: 2528 # Lamp\n  examine: \"Wonder what happens if I rub it...\"\n- id: 2530 # Bones\n  examine: \"Bones are for burying!\"\n- id: 2550 # Ring of recoil\n  examine: \"An enchanted ring.\"\n- id: 2552 # Ring of dueling(8)\n  examine: \"An enchanted ring.\"\n- id: 2554 # Ring of dueling(7)\n  examine: \"An enchanted ring.\"\n- id: 2556 # Ring of dueling(6)\n  examine: \"An enchanted ring.\"\n- id: 2558 # Ring of dueling(5)\n  examine: \"An enchanted ring.\"\n- id: 2560 # Ring of dueling(4)\n  examine: \"An enchanted ring.\"\n- id: 2562 # Ring of dueling(3)\n  examine: \"An enchanted ring.\"\n- id: 2564 # Ring of dueling(2)\n  examine: \"An enchanted ring.\"\n- id: 2566 # Ring of dueling(1)\n  examine: \"An enchanted ring.\"\n- id: 2568 # Ring of forging\n  examine: \"An enchanted ring.\"\n- id: 2570 # Ring of life\n  examine: \"An enchanted ring.\"\n- id: 2572 # Ring of wealth\n  examine: \"It can be charged at the Fountain of Rune.\"\n- id: 2574 # Sextant\n  examine: \"Used by navigators to find their position in Gielinor.\"\n- id: 2575 # Watch\n  examine: \"A fine looking time piece.\"\n- id: 2576 # Chart\n  examine: \"A navigator's chart of Gielinor.\"\n- id: 2577 # Ranger boots\n  examine: \"Lightweight boots ideal for rangers.\"\n- id: 2579 # Wizard boots\n  examine: \"Slightly magical boots.\"\n- id: 2581 # Robin hood hat\n  examine: \"Endorsed by Robin Hood.\"\n- id: 2583 # Black platebody (t)\n  examine: \"Black platebody with trim.\"\n- id: 2585 # Black platelegs (t)\n  examine: \"Black platelegs with trim.\"\n- id: 2587 # Black full helm (t)\n  examine: \"Black full helmet with trim.\"\n- id: 2589 # Black kiteshield (t)\n  examine: \"Black kiteshield with trim.\"\n- id: 2591 # Black platebody (g)\n  examine: \"Black platebody with gold trim.\"\n- id: 2593 # Black platelegs (g)\n  examine: \"Black platelegs with gold trim.\"\n- id: 2595 # Black full helm (g)\n  examine: \"Black full helmet with gold trim.\"\n- id: 2597 # Black kiteshield (g)\n  examine: \"Black kiteshield with gold trim.\"\n- id: 2615 # Rune platebody (g)\n  examine: \"Rune platebody with gold trim.\"\n- id: 2617 # Rune platelegs (g)\n  examine: \"Rune platelegs with gold trim.\"\n- id: 2619 # Rune full helm (g)\n  examine: \"Rune full helmet with gold trim.\"\n- id: 2621 # Rune kiteshield (g)\n  examine: \"Rune kiteshield with gold trim.\"\n- id: 2623 # Rune platebody (t)\n  examine: \"Rune platebody with trim.\"\n- id: 2625 # Rune platelegs (t)\n  examine: \"Rune platelegs with trim.\"\n- id: 2627 # Rune full helm (t)\n  examine: \"Rune full helmet with trim.\"\n- id: 2629 # Rune kiteshield (t)\n  examine: \"Rune kiteshield with trim.\"\n- id: 2631 # Highwayman mask\n  examine: \"Your money or your life!\"\n- id: 2633 # Blue beret\n  examine: \"Parlez-vous francais?\"\n- id: 2635 # Black beret\n  examine: \"Parlez-vous francais?\"\n- id: 2637 # White beret\n  examine: \"Parlez-vous francais?\"\n- id: 2639 # Tan cavalier\n  examine: \"All for one and one for all!\"\n- id: 2641 # Dark cavalier\n  examine: \"All for one and one for all!\"\n- id: 2643 # Black cavalier\n  examine: \"All for one and one for all!\"\n- id: 2645 # Red headband\n  examine: \"A minimalist's hat.\"\n- id: 2647 # Black headband\n  examine: \"A minimalist's hat.\"\n- id: 2649 # Brown headband\n  examine: \"A minimalist's hat.\"\n- id: 2651 # Pirate's hat\n  examine: \"Shiver me timbers!\"\n- id: 2653 # Zamorak platebody\n  examine: \"Rune platebody in the colours of Zamorak.\"\n- id: 2655 # Zamorak platelegs\n  examine: \"Rune platelegs in the colours of Zamorak.\"\n- id: 2657 # Zamorak full helm\n  examine: \"Rune full helmet in the colours of Zamorak.\"\n- id: 2659 # Zamorak kiteshield\n  examine: \"Rune kiteshield in the colours of Zamorak.\"\n- id: 2669 # Guthix platebody\n  examine: \"Rune platebody in the colours of Guthix.\"\n- id: 2671 # Guthix platelegs\n  examine: \"Rune plate legs in the colours of Guthix.\"\n- id: 2673 # Guthix full helm\n  examine: \"A rune full face helmet in the colours of Guthix.\"\n- id: 2675 # Guthix kiteshield\n  examine: \"Rune kiteshield in the colours of Guthix.\"\n- id: 2859 # Wolf bones\n  examine: \"Bones of a recently slain wolf.\"\n- id: 2861 # Wolfbone arrowtips\n  examine: \"I can make an ogre arrow with these.\"\n- id: 2862 # Achey tree logs\n  examine: \"These logs are longer than normal.\"\n- id: 2864 # Ogre arrow shaft\n  examine: \"A wooden arrow shaft.\"\n- id: 2865 # Flighted ogre arrow\n  examine: \"A wooden arrow shaft with four flights attached.\"\n- id: 2866 # Ogre arrow\n  examine: \"A large ogre arrow with a bone tip.\"\n- id: 2871 # Ogre bellows\n  examine: \"A large pair of ogre bellows.\"\n- id: 2872 # Ogre bellows (3)\n  examine: \"A large pair of ogre bellows, it has three loads of swamp gas in it.\"\n- id: 2873 # Ogre bellows (2)\n  examine: \"A large pair of ogre bellows, it has two loads of swamp gas in it.\"\n- id: 2874 # Ogre bellows (1)\n  examine: \"A large pair of ogre bellows, it has one load of swamp gas in it.\"\n- id: 2875 # Bloated toad\n  examine: \"An inflated toad.\"\n- id: 2876 # Raw chompy\n  examine: \"I need to cook this first.\"\n- id: 2878 # Cooked chompy\n  examine: \"It might look delicious to an ogre.\"\n- id: 2880 # Ruined chompy\n  examine: \"It's really burnt.\"\n- id: 2882 # Seasoned chompy\n  examine: \"It has been deliciously seasoned to taste wonderful for ogres.\"\n- id: 2883 # Ogre bow\n  examine: \"More powerful than a normal bow, useful against large game birds.\"\n- id: 2886 # Battered book\n  examine: \"Book of the elemental shield.\"\n- id: 2887 # Battered key\n  examine: \"An old battered key.\"\n- id: 2888 # A stone bowl\n  examine: \"This is an empty stone bowl.\"\n- id: 2889 # A stone bowl\n  examine: \"This is a stone bowl full of lava.\"\n- id: 2890 # Elemental shield\n  examine: \"A magic shield.\"\n- id: 2892 # Elemental ore\n  examine: \"This needs refining.\"\n- id: 2893 # Elemental metal\n  examine: \"It's a bar of refined elemental ore.\"\n- id: 2944 # Golden key\n  examine: \"A replica key made of solid gold.\"\n- id: 2945 # Iron key\n  examine: \"A key made of solid Iron.\"\n- id: 2946 # Golden tinderbox\n  examine: \"A replica tinderbox made of solid gold.\"\n- id: 2947 # Golden candle\n  examine: \"A replica candle made of solid gold.\"\n- id: 2948 # Golden pot\n  examine: \"A replica pot made of solid gold.\"\n- id: 2949 # Golden hammer\n  examine: \"A replica hammer made of solid gold.\"\n- id: 2950 # Golden feather\n  examine: \"A replica feather made of solid gold.\"\n- id: 2951 # Golden needle\n  examine: \"A replica needle made of solid gold.\"\n- id: 2952 # Wolfbane\n  examine: \"A silver dagger that can prevent werewolves changing form.\"\n- id: 2955 # Moonlight mead\n  examine: \"A foul smelling brew.\"\n- id: 2957 # Druid pouch\n  examine: \"An empty druid pouch.\"\n- id: 2958 # Druid pouch\n  examine: \"A druid pouch.\"\n- id: 2959 # Rotten food\n  examine: \"Erhhh! It stinks.\"\n- id: 2961 # Silver sickle\n  examine: \"It's a silver sickle.\"\n- id: 2964 # Washing bowl\n  examine: \"Used for washing your face, amongst other things.\"\n- id: 2966 # Mirror\n  examine: \"A small mirror, probably used for grooming.\"\n- id: 2967 # Journal\n  examine: \"This must be Filliman Tarlocks personal journal.\"\n- id: 2968 # Druidic spell\n  examine: \"A druidic spell given to you freely by the spirit of Filliman Tarlock.\"\n- id: 2969 # A used spell\n  examine: \"A used druidic spell given to you freely by the spirit of Filliman Tarlock.\"\n- id: 2972 # Mort myre stem\n  examine: \"A cutting from a budding branch.\"\n- id: 2974 # Mort myre pear\n  examine: \"A pear picked from a dying bush in Mort Myre.\"\n- id: 2976 # Sickle mould\n  examine: \"Used to make sickles.\"\n- id: 2978 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Bowman\"\n- id: 2979 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Bowman\"\n- id: 2980 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Yeoman\"\n- id: 2981 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Yeoman\"\n- id: 2982 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Marksman\"\n- id: 2983 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Marksman\"\n- id: 2984 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Woodsman\"\n- id: 2985 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Woodsman\"\n- id: 2986 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess:Ogre Forester\"\n- id: 2987 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Forester\"\n- id: 2988 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Bowmaster\"\n- id: 2989 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Bowmaster\"\n- id: 2990 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Expert\"\n- id: 2991 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Expert\"\n- id: 2992 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Ogre Dragon Archer\"\n- id: 2993 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Dragon Archer\"\n- id: 2994 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Expert Ogre Dragon Archer\"\n- id: 2995 # Chompy bird hat\n  examine: \"A symbol of your chompy bird hunting prowess: Expert Dragon Archer\"\n- id: 2996 # Agility arena ticket\n  examine: \"I can exchange these for further experience or items.\"\n- id: 2997 # Pirate's hook\n  examine: \"You should see the shark...\"\n- id: 2998 # Toadflax\n  examine: \"A useful herb.\"\n- id: 3000 # Snapdragon\n  examine: \"A powerful herb.\"\n- id: 3008 # Energy potion(4)\n  examine: \"4 doses of energy potion.\"\n- id: 3010 # Energy potion(3)\n  examine: \"3 doses of energy potion.\"\n- id: 3012 # Energy potion(2)\n  examine: \"2 doses of energy potion.\"\n- id: 3014 # Energy potion(1)\n  examine: \"1 dose of energy potion.\"\n- id: 3016 # Super energy(4)\n  examine: \"4 doses of super energy potion.\"\n- id: 3018 # Super energy(3)\n  examine: \"3 doses of super energy potion.\"\n- id: 3020 # Super energy(2)\n  examine: \"2 doses of super energy potion.\"\n- id: 3022 # Super energy(1)\n  examine: \"1 dose of super energy potion.\"\n- id: 3024 # Super restore(4)\n  examine: \"4 doses of super restore potion.\"\n- id: 3026 # Super restore(3)\n  examine: \"3 doses of super restore potion.\"\n- id: 3028 # Super restore(2)\n  examine: \"2 doses of super restore potion.\"\n- id: 3030 # Super restore(1)\n  examine: \"1 dose of super restore potion.\"\n- id: 3032 # Agility potion(4)\n  examine: \"4 doses of Agility potion.\"\n- id: 3034 # Agility potion(3)\n  examine: \"3 doses of Agility potion.\"\n- id: 3036 # Agility potion(2)\n  examine: \"2 doses of Agility potion.\"\n- id: 3038 # Agility potion(1)\n  examine: \"1 dose of Agility potion.\"\n- id: 3040 # Magic potion(4)\n  examine: \"4 doses of Magic potion.\"\n- id: 3042 # Magic potion(3)\n  examine: \"3 doses of Magic potion.\"\n- id: 3044 # Magic potion(2)\n  examine: \"2 doses of Magic potion.\"\n- id: 3046 # Magic potion(1)\n  examine: \"1 dose of Magic potion.\"\n- id: 3053 # Lava battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 3054 # Mystic lava staff\n  examine: \"It's a slightly magical stick.\"\n- id: 3057 # Mime mask\n  examine: \"A mime would wear this.\"\n- id: 3058 # Mime top\n  examine: \"A mime would wear this.\"\n- id: 3059 # Mime legs\n  examine: \"A mime would wear these.\"\n- id: 3060 # Mime gloves\n  examine: \"A mime would wear these.\"\n- id: 3061 # Mime boots\n  examine: \"A mime would wear these.\"\n- id: 3062 # Strange box\n  examine: \"It seems to be humming...\"\n- id: 3093 # Black dart\n  examine: \"A deadly throwing dart with a black tip.\"\n- id: 3094 # Black dart(p)\n  examine: \"A deadly poisoned dart with a black tip.\"\n- id: 3095 # Bronze claws\n  examine: \"A set of fighting claws.\"\n- id: 3096 # Iron claws\n  examine: \"A set of fighting claws.\"\n- id: 3097 # Steel claws\n  examine: \"A set of fighting claws.\"\n- id: 3098 # Black claws\n  examine: \"A set of fighting claws.\"\n- id: 3099 # Mithril claws\n  examine: \"A set of fighting claws.\"\n- id: 3100 # Adamant claws\n  examine: \"A set of fighting claws.\"\n- id: 3101 # Rune claws\n  examine: \"A set of fighting claws.\"\n- id: 3102 # Combination\n  examine: \"The combination to Burthorpe Castle's equipment room.\"\n- id: 3103 # Iou\n  examine: \"The guard wrote the IOU on the back of some paper.\"\n- id: 3104 # Secret way map\n  examine: \"This map shows the secret way up to Death Plateau.\"\n- id: 3105 # Climbing boots\n  examine: \"Boots made for climbing.\"\n- id: 3107 # Spiked boots\n  examine: \"Climbing boots with spikes.\"\n- id: 3109 # Stone ball\n  examine: \"Place on the stone mechanism in the right order to open the door.\"\n- id: 3110 # Stone ball\n  examine: \"Place on the stone mechanism in the right order to open the door.\"\n- id: 3111 # Stone ball\n  examine: \"Place on the stone mechanism in the right order to open the door.\"\n- id: 3112 # Stone ball\n  examine: \"Place on the stone mechanism in the right order to open the door.\"\n- id: 3113 # Stone ball\n  examine: \"Place on the stone mechanism in the right order to open the door.\"\n- id: 3114 # Certificate\n  examine: \"Entrance certificate to the Imperial Guard.\"\n- id: 3122 # Granite shield\n  examine: \"A solid stone shield.\"\n- id: 3123 # Shaikahan bones\n  examine: \"Large glistening bones which glow with a pale yellow aura.\"\n- id: 3125 # Jogre bones\n  examine: \"Fairly big bones which smell distinctly of Jogre.\"\n- id: 3127 # Burnt jogre bones\n  examine: \"These blackened Jogre bones have been somehow burnt.\"\n- id: 3128 # Pasty jogre bones\n  examine: \"Burnt Jogre bones smothered with raw Karambwanji Paste.\"\n- id: 3129 # Pasty jogre bones\n  examine: \"Burnt Jogre bones smothered with cooked Karambwanji paste.\"\n- id: 3130 # Marinated j' bones\n  examine: \"Burnt Jogre bones marinated in a lovely Karambwanji sauce. Perfect.\"\n- id: 3131 # Pasty jogre bones\n  examine: \"Jogre bones smothered with raw Karambwanji paste.\"\n- id: 3132 # Pasty jogre bones\n  examine: \"Jogre bones smothered with cooked Karambwanji paste.\"\n- id: 3133 # Marinated j' bones\n  examine: \"Jogre Bones marinated in Karambwanji sauce. Not quite right.\"\n- id: 3135 # Prison key\n  examine: \"The key to the troll prison.\"\n- id: 3136 # Cell key 1\n  examine: \"The key to Godric's cell in the troll prison.\"\n- id: 3137 # Cell key 2\n  examine: \"The key to Mad Eadgar's cell in the troll prison.\"\n- id: 3138 # Potato cactus\n  examine: \"How am I supposed to eat that?!\"\n- id: 3140 # Dragon chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 3142 # Raw karambwan\n  examine: \"A raw green octopus.\"\n- id: 3144 # Cooked karambwan\n  examine: \"Cooked octopus. It looks very nutritious.\"\n- id: 3147 # Cooked karambwan\n  examine: \"Cooked octopus. It looks very nutritious.\"\n- id: 3148 # Burnt karambwan\n  examine: \"Burnt octopus.\"\n- id: 3150 # Raw karambwanji\n  examine: \"Small brightly coloured tropical fish.\"\n- id: 3152 # Karambwan paste\n  examine: \"Freshly made octopus paste.\"\n- id: 3153 # Karambwan paste\n  examine: \"Freshly made octopus paste. This smells quite nauseating.\"\n- id: 3154 # Karambwan paste\n  examine: \"Freshly made octopus paste.\"\n- id: 3155 # Karambwanji paste\n  examine: \"This paste smells of raw fish.\"\n- id: 3156 # Karambwanji paste\n  examine: \"This paste smells of cooked fish.\"\n- id: 3157 # Karambwan vessel\n  examine: \"A wide bodied and thin necked vessel, encrusted with sea salt.\"\n- id: 3159 # Karambwan vessel\n  examine: \"This Karambwan Vessel is loaded with Karambwanji.\"\n- id: 3161 # Crafting manual\n  examine: \"A set of instructions explaining how to construct a Karambwan vessel.\"\n- id: 3162 # Sliced banana\n  examine: \"You swear you had more than three slices before.\"\n- id: 3164 # Karamjan rum\n  examine: \"The Karamjan rum has slices of banana floating in it.\"\n- id: 3165 # Karamjan rum\n  examine: \"A banana has been stuffed into the neck of this bottle.\"\n- id: 3166 # Monkey corpse\n  examine: \"It's the body of a dead monkey.\"\n- id: 3167 # Monkey skin\n  examine: \"It's the skin of a dead monkey.\"\n- id: 3168 # Seaweed sandwich\n  examine: \"A 'Seaweed in Monkey Skin' sandwich. Perfect for statue repair.\"\n- id: 3169 # Stuffed monkey\n  examine: \"A body of a dead monkey, tastefully stuffed with seaweed.\"\n- id: 3170 # Bronze spear(kp)\n  examine: \"A Karambwan poisoned bronze tipped spear.\"\n- id: 3171 # Iron spear(kp)\n  examine: \"A Karambwan poisoned iron tipped spear.\"\n- id: 3172 # Steel spear(kp)\n  examine: \"A Karambwan poisoned steel tipped spear.\"\n- id: 3173 # Mithril spear(kp)\n  examine: \"A Karambwan poisoned mithril tipped spear.\"\n- id: 3174 # Adamant spear(kp)\n  examine: \"A Karambwan poisoned adamantite tipped spear.\"\n- id: 3175 # Rune spear(kp)\n  examine: \"A Karambwan poisoned rune tipped spear.\"\n- id: 3176 # Dragon spear(kp)\n  examine: \"A Karambwan poisoned dragon tipped spear.\"\n- id: 3179 # Monkey bones\n  examine: \"These are smallish monkey bones.\"\n- id: 3180 # Monkey bones\n  examine: \"These are medium sized monkey bones.\"\n- id: 3181 # Monkey bones\n  examine: \"These are quite large monkey bones.\"\n- id: 3182 # Monkey bones\n  examine: \"These are quite large monkey bones.\"\n- id: 3183 # Monkey bones\n  examine: \"These are small monkey bones.\"\n- id: 3185 # Monkey bones\n  examine: \"These are smallish monkey bones. They smell extremely nauseating.\"\n- id: 3186 # Monkey bones\n  examine: \"These are smallish monkey bones. They smell extremely nauseating.\"\n- id: 3187 # Bones\n  examine: \"They seem to shake slightly... It might be a good idea to bury them.\"\n- id: 3188 # Cleaning cloth\n  examine: \"A spirit soaked piece of silk which can be used to remove poison.\"\n- id: 3190 # Bronze halberd\n  examine: \"A bronze halberd.\"\n- id: 3192 # Iron halberd\n  examine: \"An iron halberd.\"\n- id: 3194 # Steel halberd\n  examine: \"A steel halberd.\"\n- id: 3196 # Black halberd\n  examine: \"A black halberd.\"\n- id: 3198 # Mithril halberd\n  examine: \"A mithril halberd.\"\n- id: 3200 # Adamant halberd\n  examine: \"An adamant halberd.\"\n- id: 3202 # Rune halberd\n  examine: \"A rune halberd.\"\n- id: 3204 # Dragon halberd\n  examine: \"A dragon halberd.\"\n- id: 3206 # King's message\n  examine: \"A summons from King Lathas.\"\n- id: 3208 # Crystal pendant\n  examine: \"Lord Iorwerth's crystal pendant.\"\n- id: 3209 # Sulphur\n  examine: \"A piece of sulphur formation.\"\n- id: 3211 # Limestone\n  examine: \"Some limestone.\"\n- id: 3213 # Quicklime\n  examine: \"Some quicklime.\"\n- id: 3214 # Pot of quicklime\n  examine: \"A pot of ground quicklime.\"\n- id: 3215 # Ground sulphur\n  examine: \"A pile of ground sulphur.\"\n- id: 3216 # Barrel\n  examine: \"An empty barrel.\"\n- id: 3218 # Barrel bomb\n  examine: \"A barrel full of fire oil.\"\n- id: 3219 # Barrel bomb\n  examine: \"A fused barrel full of fire oil.\"\n- id: 3221 # Barrel of naphtha\n  examine: \"A barrel full of naphtha.\"\n- id: 3222 # Naphtha mix\n  examine: \"A barrel full of naphtha and sulphur.\"\n- id: 3223 # Naphtha mix\n  examine: \"A barrel full of naphtha and quicklime.\"\n- id: 3224 # Strip of cloth\n  examine: \"A strip of cloth.\"\n- id: 3226 # Raw rabbit\n  examine: \"Might taste better cooked.\"\n- id: 3228 # Cooked rabbit\n  examine: \"Mmm this looks tasty.\"\n- id: 3230 # Big book of bangs\n  examine: \"A book by Mel Achy.\"\n- id: 3239 # Bark\n  examine: \"Bark from a hollow tree.\"\n- id: 3261 # Goutweed\n  examine: \"A pale, tough looking herb.\"\n- id: 3262 # Troll thistle\n  examine: \"It's tough and spiky.\"\n- id: 3263 # Dried thistle\n  examine: \"It'll be easier to grind now.\"\n- id: 3264 # Ground thistle\n  examine: \"It's ready for mixing.\"\n- id: 3265 # Troll potion\n  examine: \"It's part of Eadgar's plan.\"\n- id: 3266 # Drunk parrot\n  examine: \"It's rather drunk.\"\n- id: 3267 # Dirty robe\n  examine: \"It's dirty and smelly.\"\n- id: 3268 # Fake man\n  examine: \"It's good enough to fool a troll.\"\n- id: 3269 # Storeroom key\n  examine: \"The key to the Troll storeroom.\"\n- id: 3270 # Alco-chunks\n  examine: \"Pineapple chunks dipped in strong liquor.\"\n- id: 3327 # Myre snelm\n  examine: \"A marshy coloured snail shell helmet.\"\n- id: 3329 # Blood'n'tar snelm\n  examine: \"A red and black Snail shell helmet.\"\n- id: 3331 # Ochre snelm\n  examine: \"A muddy yellow snail shell helmet.\"\n- id: 3333 # Bruise blue snelm\n  examine: \"A moody blue snail shell helmet.\"\n- id: 3335 # Broken bark snelm\n  examine: \"An orange and bark coloured snail shell helmet.\"\n- id: 3337 # Myre snelm\n  examine: \"A swamp coloured pointed snail shell helmet.\"\n- id: 3339 # Blood'n'tar snelm\n  examine: \"A red and black pointed snail shell helmet.\"\n- id: 3341 # Ochre snelm\n  examine: \"A muddy yellow coloured pointed snail shell helmet.\"\n- id: 3343 # Bruise blue snelm\n  examine: \"A moody blue pointed snail shell helmet.\"\n- id: 3345 # Blamish myre shell\n  examine: \"A large 'Myre' coloured blamish snail shell, looks protective.\"\n- id: 3347 # Blamish red shell\n  examine: \"A large red and black blamish snail shell, looks protective.\"\n- id: 3349 # Blamish ochre shell\n  examine: \"A large muddy yellow coloured blamish snail shell, looks protective.\"\n- id: 3351 # Blamish blue shell\n  examine: \"A large blue coloured blamish snail shell, looks protective.\"\n- id: 3353 # Blamish bark shell\n  examine: \"A large bark coloured blamish snail shell, looks protective.\"\n- id: 3355 # Blamish myre shell\n  examine: \"A large 'Myre' coloured blamish snail shell, looks protective.\"\n- id: 3357 # Blamish red shell\n  examine: \"A large red coloured blamish snail shell, looks protective.\"\n- id: 3359 # Blamish ochre shell\n  examine: \"A large ochre coloured blamish snail shell, looks protective.\"\n- id: 3361 # Blamish blue shell\n  examine: \"A large blue coloured blamish snail shell, looks protective.\"\n- id: 3363 # Thin snail\n  examine: \"The thin, slimy corpse of a deceased giant snail.\"\n- id: 3365 # Lean snail\n  examine: \"The lean, slimy corpse of a deceased giant snail.\"\n- id: 3367 # Fat snail\n  examine: \"The fat, slimy corpse of a deceased giant snail.\"\n- id: 3369 # Thin snail meat\n  examine: \"A succulently slimy slice of sumptuous snail.\"\n- id: 3371 # Lean snail meat\n  examine: \"A succulently slimy slice of sumptuous snail.\"\n- id: 3373 # Fat snail meat\n  examine: \"A succulently slimy slice of sumptuous snail.\"\n- id: 3375 # Burnt snail\n  examine: \"A slightly super-saute'ed snail.\"\n- id: 3377 # Sample bottle\n  examine: \"An empty sample bottle.\"\n- id: 3381 # Cooked slimy eel\n  examine: \"A cooked slimy eel - not delicious, but pretty nutritious.\"\n- id: 3383 # Burnt eel\n  examine: \"It looks like it's seen one too many fires.\"\n- id: 3385 # Splitbark helm\n  examine: \"A wooden helmet.\"\n- id: 3387 # Splitbark body\n  examine: \"Provides good protection.\"\n- id: 3389 # Splitbark legs\n  examine: \"These should protect my legs.\"\n- id: 3391 # Splitbark gauntlets\n  examine: \"These should keep my hands safe.\"\n- id: 3395 # Diary\n  examine: \"A diary belonging to Herbi Flax.\"\n- id: 3396 # Loar remains\n  examine: \"The remains of a deadly shade.\"\n- id: 3398 # Phrin remains\n  examine: \"The remains of a deadly shade.\"\n- id: 3400 # Riyl remains\n  examine: \"The remains of a deadly shade.\"\n- id: 3402 # Asyn remains\n  examine: \"The remains of a deadly shade.\"\n- id: 3404 # Fiyr remains\n  examine: \"The remains of a deadly shade.\"\n- id: 3406 # Unfinished potion\n  examine: \"I need another ingredient to finish this potion.\"\n- id: 3408 # Serum 207 (4)\n  examine: \"4 doses serum 207 as described in Herbi Flax's diary.\"\n- id: 3410 # Serum 207 (3)\n  examine: \"3 doses serum 207 as described in Herbi Flax's diary.\"\n- id: 3412 # Serum 207 (2)\n  examine: \"2 doses serum 207 as described in Herbi Flax's diary.\"\n- id: 3414 # Serum 207 (1)\n  examine: \"1 dose serum 207 as described in Herbi Flax's diary.\"\n- id: 3416 # Serum 208 (4)\n  examine: \"4 doses permanent serum 208 as described in Herbi Flax's diary.\"\n- id: 3417 # Serum 208 (3)\n  examine: \"3 doses permanent serum 208 as described in Herbi Flax's diary.\"\n- id: 3418 # Serum 208 (2)\n  examine: \"2 doses permanent serum 208 as described in Herbi Flax's diary.\"\n- id: 3419 # Serum 208 (1)\n  examine: \"1 dose permanent serum 208 as described in Herbi Flax's diary.\"\n- id: 3420 # Limestone brick\n  examine: \"A well carved limestone brick.\"\n- id: 3422 # Olive oil(4)\n  examine: \"4 doses of olive oil.\"\n- id: 3424 # Olive oil(3)\n  examine: \"3 doses of olive oil.\"\n- id: 3426 # Olive oil(2)\n  examine: \"2 doses of olive oil.\"\n- id: 3428 # Olive oil(1)\n  examine: \"1 dose of olive oil.\"\n- id: 3430 # Sacred oil(4)\n  examine: \"4 doses of sacred Oil.\"\n- id: 3432 # Sacred oil(3)\n  examine: \"3 doses of sacred Oil.\"\n- id: 3434 # Sacred oil(2)\n  examine: \"2 doses of sacred Oil.\"\n- id: 3436 # Sacred oil(1)\n  examine: \"1 dose of sacred Oil.\"\n- id: 3438 # Pyre logs\n  examine: \"Logs prepared with sacred oil for a funeral pyre.\"\n- id: 3440 # Oak pyre logs\n  examine: \"Oak logs prepared with sacred oil for a funeral pyre.\"\n- id: 3442 # Willow pyre logs\n  examine: \"Willow logs prepared with sacred oil for a funeral pyre.\"\n- id: 3444 # Maple pyre logs\n  examine: \"Maple logs prepared with sacred oil for a funeral pyre.\"\n- id: 3446 # Yew pyre logs\n  examine: \"Yew logs prepared with sacred oil for a funeral pyre.\"\n- id: 3448 # Magic pyre logs\n  examine: \"Magic logs prepared with sacred oil for a funeral pyre.\"\n- id: 3450 # Bronze key red\n  examine: \"A bronze key with a blood-red painted eyelet.\"\n- id: 3451 # Bronze key brown\n  examine: \"A bronze key with a brown painted eyelet.\"\n- id: 3452 # Bronze key crimson\n  examine: \"A bronze key with a crimson painted eyelet.\"\n- id: 3453 # Bronze key black\n  examine: \"A bronze key with a black painted eyelet.\"\n- id: 3454 # Bronze key purple\n  examine: \"A bronze key with a purple painted eyelet.\"\n- id: 3455 # Steel key red\n  examine: \"A steel key with a blood-red painted eyelet.\"\n- id: 3456 # Steel key brown\n  examine: \"A steel key with a brown painted eyelet.\"\n- id: 3457 # Steel key crimson\n  examine: \"A steel key with a crimson painted eyelet.\"\n- id: 3458 # Steel key black\n  examine: \"A steel key with a black painted eyelet.\"\n- id: 3459 # Steel key purple\n  examine: \"A steel key with a purple painted eyelet.\"\n- id: 3460 # Black key red\n  examine: \"A black key with a blood-red painted eyelet.\"\n- id: 3461 # Black key brown\n  examine: \"A black key with a brown painted eyelet.\"\n- id: 3462 # Black key crimson\n  examine: \"A black key with a crimson painted eyelet.\"\n- id: 3463 # Black key black\n  examine: \"A black key with a black painted eyelet.\"\n- id: 3464 # Black key purple\n  examine: \"A black key with a purple painted eyelet.\"\n- id: 3465 # Silver key red\n  examine: \"A silver key with a blood-red painted eyelet.\"\n- id: 3466 # Silver key brown\n  examine: \"A silver key with a brown painted eyelet.\"\n- id: 3467 # Silver key crimson\n  examine: \"A silver key with a crimson painted eyelet.\"\n- id: 3468 # Silver key black\n  examine: \"A silver key with a black painted eyelet.\"\n- id: 3469 # Silver key purple\n  examine: \"A silver key with a purple painted eyelet.\"\n- id: 3470 # Fine cloth\n  examine: \"Amazingly untouched by time.\"\n- id: 3472 # Black plateskirt (t)\n  examine: \"Black plateskirt with trim.\"\n- id: 3473 # Black plateskirt (g)\n  examine: \"Black plateskirt with gold trim.\"\n- id: 3476 # Rune plateskirt (g)\n  examine: \"Rune plateskirt with gold trim.\"\n- id: 3477 # Rune plateskirt (t)\n  examine: \"Rune plateskirt with trim.\"\n- id: 3478 # Zamorak plateskirt\n  examine: \"Rune plateskirt in the colours of Zamorak.\"\n- id: 3480 # Guthix plateskirt\n  examine: \"Rune plateskirt in the colours of Guthix.\"\n- id: 3481 # Gilded platebody\n  examine: \"Rune platebody with gold plate.\"\n- id: 3483 # Gilded platelegs\n  examine: \"Rune platelegs with gold plate.\"\n- id: 3485 # Gilded plateskirt\n  examine: \"Rune plateskirt with gold plate.\"\n- id: 3486 # Gilded full helm\n  examine: \"Rune full helmet with gold plate.\"\n- id: 3488 # Gilded kiteshield\n  examine: \"Rune kiteshield with gold plate.\"\n- id: 3678 # Flamtaer hammer\n  examine: \"An exquisitely shaped tool specially designed for fixing temples.\"\n- id: 3688 # Unstrung lyre\n  examine: \"It's almost a musical instrument.\"\n- id: 3689 # Lyre\n  examine: \"It's a musical instrument I don't know how to play.\"\n- id: 3690 # Enchanted lyre\n  examine: \"A musical instrument that I can magically play.\"\n- id: 3691 # Enchanted lyre(1)\n  examine: \"This will teleport me to the Fremennik province when I play it.\"\n- id: 3692 # Branch\n  examine: \"I can use this to make a lyre.\"\n- id: 3693 # Golden fleece\n  examine: \"I can spin this into golden wool...\"\n- id: 3694 # Golden wool\n  examine: \"I can use this to make a lyre.\"\n- id: 3695 # Pet rock\n  examine: \"The lowest maintenance pet you will ever have.\"\n- id: 3696 # Hunters' talisman\n  examine: \"Talisman to bind the Draugen.\"\n- id: 3697 # Hunters' talisman\n  examine: \"Talisman to bind the Draugen.\"\n- id: 3698 # Exotic flower\n  examine: \"Some flowers from a distant land.\"\n- id: 3699 # Fremennik ballad\n  examine: \"A hauntingly beautiful love ballad.\"\n- id: 3700 # Sturdy boots\n  examine: \"A pair of sturdy custom made boots.\"\n- id: 3702 # Custom bow string\n  examine: \"A finely crafted string for a custom bow.\"\n- id: 3703 # Unusual fish\n  examine: \"An extremely odd, non-edible fish.\"\n- id: 3704 # Sea fishing map\n  examine: \"Map showing the best fishing spots out at sea.\"\n- id: 3705 # Weather forecast\n  examine: \"An estimate of expected local weather patterns.\"\n- id: 3706 # Champions token\n  examine: \"Shows the wearer is worthy of the Champions table.\"\n- id: 3707 # Legendary cocktail\n  examine: \"Probably the greatest cocktail in the world.\"\n- id: 3708 # Fiscal statement\n  examine: \"A signed statement promising a reduction on sales tax.\"\n- id: 3709 # Promissory note\n  examine: \"A legally binding contract promising not to enter the longhall.\"\n- id: 3710 # Warriors' contract\n  examine: \"This employment contract is for a warrior to act as a bodyguard.\"\n- id: 3711 # Keg of beer\n  examine: \"A lot of beer in a barrel.\"\n- id: 3712 # Low alcohol keg\n  examine: \"Suspiciously close to beer, but without the side effects.\"\n- id: 3713 # Strange object\n  examine: \"It's some kind of weird little parcel thing.\"\n- id: 3714 # Lit strange object\n  examine: \"It's some kind of weird little parcel thing. On fire.\"\n- id: 3715 # Red disk\n  examine: \"A red coloured disk, apparently made of wood.\"\n- id: 3716 # Red disk\n  examine: \"A red coloured disk, apparently made of wood.\"\n- id: 3718 # Magnet\n  examine: \"A very attractive magnet.\"\n- id: 3719 # Blue thread\n  examine: \"Some blue thread.\"\n- id: 3720 # Small pick\n  examine: \"A small pick for cracking small objects.\"\n- id: 3721 # Toy ship\n  examine: \"Might be fun to play with in the bath.\"\n- id: 3722 # Full bucket\n  examine: \"This bucket is completely full. It has a 5 painted on its side.\"\n- id: 3723 # 4/5ths full bucket\n  examine: \"This bucket is eighty percent full. It has a 5 painted on its side.\"\n- id: 3724 # 3/5ths full bucket\n  examine: \"This bucket is sixty percent full. It has a 5 painted on its side.\"\n- id: 3725 # 2/5ths full bucket\n  examine: \"This bucket is forty percent full. It has a 5 painted on its side.\"\n- id: 3726 # 1/5ths full bucket\n  examine: \"This bucket is twenty percent full. It has a 5 painted on its side.\"\n- id: 3727 # Empty bucket\n  examine: \"This bucket is completely empty. It has a 5 painted on its side.\"\n- id: 3728 # Frozen bucket\n  examine: \"This bucket of water is frozen solid.\"\n- id: 3729 # Full jug\n  examine: \"This jug is completely full. It has a 3 painted on its side.\"\n- id: 3730 # 2/3rds full jug\n  examine: \"This jug is two thirds full. It has a 3 painted on its side.\"\n- id: 3731 # 1/3rds full jug\n  examine: \"This jug is one thirds full. It has a 3 painted on its side.\"\n- id: 3732 # Empty jug\n  examine: \"This jug is completely empty. It has a 3 painted on its side.\"\n- id: 3733 # Frozen jug\n  examine: \"This jug of water is frozen solid.\"\n- id: 3734 # Vase\n  examine: \"An unusually shaped vase. You can see something glinting inside.\"\n- id: 3735 # Vase of water\n  examine: \"An unusually shaped vase full of water. You can see something glinting inside.\"\n- id: 3736 # Frozen vase\n  examine: \"An unusually shaped vase full of ice. You can see something glinting inside.\"\n- id: 3737 # Vase lid\n  examine: \"This looks like a lid to some kind of container...\"\n- id: 3738 # Sealed vase\n  examine: \"The lid is screwed on tightly.\"\n- id: 3739 # Sealed vase\n  examine: \"The lid is screwed on tightly. It is very cold.\"\n- id: 3740 # Sealed vase\n  examine: \"The lid is screwed on tightly. It is full of water.\"\n- id: 3741 # Frozen key\n  examine: \"A key encased in ice.\"\n- id: 3742 # Red herring\n  examine: \"The colouring on it seems to be some kind of sticky goop...\"\n- id: 3743 # Red disk\n  examine: \"A red coloured disk, apparently made of wood.\"\n- id: 3744 # Wooden disk\n  examine: \"A simple looking disk made of wood.\"\n- id: 3745 # Seer's key\n  examine: \"The key to leave the Seer's house.\"\n- id: 3746 # Sticky red goop\n  examine: \"Yup, it's sticky, it's red and it's goop.\"\n- id: 3748 # Fremennik helm\n  examine: \"A sturdy helm worn only by Fremennik clan members.\"\n- id: 3749 # Archer helm\n  examine: \"This helmet is worn by archers.\"\n- id: 3751 # Berserker helm\n  examine: \"This helmet is worn by berserkers.\"\n- id: 3753 # Warrior helm\n  examine: \"This helmet is worn by warriors.\"\n- id: 3755 # Farseer helm\n  examine: \"This helmet is worn by farseers.\"\n- id: 3757 # Fremennik blade\n  examine: \"A sword used only by Fremennik warriors.\"\n- id: 3758 # Fremennik shield\n  examine: \"A shield worn by Fremennik warriors.\"\n- id: 3791 # Fremennik boots\n  examine: \"Very stylish!\"\n- id: 3793 # Fremennik robe\n  examine: \"The latest fashion in Rellekka.\"\n- id: 3795 # Fremennik skirt\n  examine: \"The latest fashion in Rellekka.\"\n- id: 3797 # Fremennik hat\n  examine: \"A silly pointed hat.\"\n- id: 3801 # Keg of beer\n  examine: \"A lot of beer in a barrel.\"\n- id: 3805 # Tankard\n  examine: \"A big cup for a big thirst.\"\n- id: 3827 # Saradomin page 1\n  examine: \"This seems to have been torn from a book...\"\n- id: 3828 # Saradomin page 2\n  examine: \"This seems to have been torn from a book...\"\n- id: 3829 # Saradomin page 3\n  examine: \"This seems to have been torn from a book...\"\n- id: 3830 # Saradomin page 4\n  examine: \"This seems to have been torn from a book...\"\n- id: 3831 # Zamorak page 1\n  examine: \"This seems to have been torn from a book...\"\n- id: 3832 # Zamorak page 2\n  examine: \"This seems to have been torn from a book...\"\n- id: 3833 # Zamorak page 3\n  examine: \"This seems to have been torn from a book...\"\n- id: 3834 # Zamorak page 4\n  examine: \"This seems to have been torn from a book...\"\n- id: 3835 # Guthix page 1\n  examine: \"This seems to have been torn from a book...\"\n- id: 3836 # Guthix page 2\n  examine: \"This seems to have been torn from a book...\"\n- id: 3837 # Guthix page 3\n  examine: \"This seems to have been torn from a book...\"\n- id: 3838 # Guthix page 4\n  examine: \"This seems to have been torn from a book...\"\n- id: 3839 # Damaged book\n  examine: \"An incomplete book of Saradomin.\"\n- id: 3840 # Holy book\n  examine: \"The holy book of Saradomin.\"\n- id: 3841 # Damaged book\n  examine: \"An incomplete book of Zamorak.\"\n- id: 3842 # Unholy book\n  examine: \"The unholy book of Zamorak.\"\n- id: 3843 # Damaged book\n  examine: \"An incomplete book of Guthix.\"\n- id: 3844 # Book of balance\n  examine: \"The holy book of Guthix.\"\n- id: 3845 # Journal\n  examine: \"A daily journal.\"\n- id: 3846 # Diary\n  examine: \"Someone's Diary.\"\n- id: 3847 # Manual\n  examine: \"Looks like some kind of manual.\"\n- id: 3848 # Lighthouse key\n  examine: \"The key to the front door of the lighthouse.\"\n- id: 3849 # Rusty casket\n  examine: \"Looks old and rusty...\"\n- id: 3853 # Games necklace(8)\n  examine: \"An enchanted necklace.\"\n- id: 3855 # Games necklace(7)\n  examine: \"An enchanted necklace.\"\n- id: 3857 # Games necklace(6)\n  examine: \"An enchanted necklace.\"\n- id: 3859 # Games necklace(5)\n  examine: \"An enchanted necklace.\"\n- id: 3861 # Games necklace(4)\n  examine: \"An enchanted necklace.\"\n- id: 3863 # Games necklace(3)\n  examine: \"An enchanted necklace.\"\n- id: 3865 # Games necklace(2)\n  examine: \"An enchanted necklace.\"\n- id: 3867 # Games necklace(1)\n  examine: \"An enchanted necklace.\"\n- id: 3894 # Awful anthem\n  examine: \"It's not very good.\"\n- id: 3895 # Good anthem\n  examine: \"Much better.\"\n- id: 3896 # Treaty\n  examine: \"Just needs the King's signature.\"\n- id: 3897 # Giant nib\n  examine: \"For making a giant pen.\"\n- id: 3898 # Giant pen\n  examine: \"The King should be able to use this.\"\n- id: 3899 # Iron sickle\n  examine: \"Not as good as a pet frog.\"\n- id: 3901 # Ghrim's book\n  examine: \"Managing Thine Kingdom for Noobes by A. Ghrim.\"\n- id: 4001 # Hardy gout tuber\n  examine: \"A hardy gout tuber.\"\n- id: 4002 # Spare controls\n  examine: \"It looks like some kind of control panel.\"\n- id: 4004 # Gnome royal seal\n  examine: \"It's the official Gnome Royal Seal, signed by King Narnode Shareen.\"\n- id: 4005 # Narnode's orders\n  examine: \"Unreadable orders handwritten by King Narnode Shareen.\"\n- id: 4006 # Monkey dentures\n  examine: \"Magical monkey talking dentures! What more can we say? Ook!\"\n- id: 4007 # Enchanted bar\n  examine: \"A gold bar invested with a talkative monkey spirit.\"\n- id: 4008 # Eye of gnome\n  examine: \"It's... the eye of a gnome! Now what on earth could one do with this?\"\n- id: 4012 # Monkey nuts\n  examine: \"These are monkey nuts. Yummy.\"\n- id: 4014 # Monkey bar\n  examine: \"It's a monkey bar. It looks highly nutritious.\"\n- id: 4016 # Banana stew\n  examine: \"It's a bowl full of mushy banana.\"\n- id: 4020 # M'amulet mould\n  examine: \"It's an amulet mould shaped like a monkey head.\"\n- id: 4021 # M'speak amulet\n  examine: \"It's an Amulet of Monkey Speak. It makes vague chattering noises.\"\n- id: 4022 # M'speak amulet\n  examine: \"It's an unstrung Amulet of Monkey Speak. It makes vague chattering noises.\"\n- id: 4023 # Monkey talisman\n  examine: \"A magical talisman in the shape of a monkey head.\"\n- id: 4033 # Monkey\n  examine: \"It's a monkey in your backpack. As you look it pokes you.\"\n- id: 4034 # Monkey skull\n  examine: \"It's a very ancient skull from some kind of ape.\"\n- id: 4035 # 10th squad sigil\n  examine: \"It's the official sigil of the 10th squad of the Royal Guard.\"\n- id: 4037 # Saradomin banner\n  examine: \"The Saradomin Team Standard.\"\n- id: 4039 # Zamorak banner\n  examine: \"The Zamorak Team Standard.\"\n- id: 4041 # Hooded cloak\n  examine: \"The colours of Saradomin.\"\n- id: 4042 # Hooded cloak\n  examine: \"The colours of Zamorak.\"\n- id: 4043 # Rock\n  examine: \"I can use this with the catapult.\"\n- id: 4045 # Explosive potion\n  examine: \"I could use this to destroy things...\"\n- id: 4047 # Climbing rope\n  examine: \"Should be long enough to scale castle walls.\"\n- id: 4049 # Bandages\n  examine: \"A box of bandages for healing.\"\n- id: 4051 # Toolkit\n  examine: \"I can use this to repair things.\"\n- id: 4053 # Barricade\n  examine: \"Use these to block enemy team movement.\"\n- id: 4055 # Castlewars manual\n  examine: \"It's a manual for castlewars.\"\n- id: 4067 # Castle wars ticket\n  examine: \"I can exchange these for further items.\"\n- id: 4068 # Decorative sword\n  examine: \"A very decorative sword.\"\n- id: 4069 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4070 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4071 # Decorative helm\n  examine: \"A very decorative helm.\"\n- id: 4072 # Decorative shield\n  examine: \"A very decorative shield.\"\n- id: 4073 # Damp tinderbox\n  examine: \"Not so useful for lighting a fire.\"\n- id: 4075 # Glowing fungus\n  examine: \"A bizarre fungus. It glows with a pale blue light.\"\n- id: 4077 # Crystal-mine key\n  examine: \"A key I found in the lower levels of the Morytanian mines.\"\n- id: 4078 # Zealot's key\n  examine: \"I stole this from a Saradominist I met South of Mort'ton.\"\n- id: 4079 # Yo-yo\n  examine: \"A gift from Santa.\"\n- id: 4081 # Salve amulet\n  examine: \"Increases the wearer's strength and accuracy by 15%{{Sic|actually 1/6th}} when fighting the undead.\"\n- id: 4082 # Salve shard\n  examine: \"An unstrung crystal imbued with the power of Saradomin.\"\n- id: 4083 # Sled\n  examine: \"It needs waxing before I can use it.\"\n- id: 4084 # Sled\n  examine: \"A waxed sled.\"\n- id: 4085 # Wax\n  examine: \"I can use this to wax my sled.\"\n- id: 4086 # Trollweiss\n  examine: \"These are very rare flowers with a pungent odour.\"\n- id: 4087 # Dragon platelegs\n  examine: \"These look pretty heavy.\"\n- id: 4089 # Mystic hat\n  examine: \"A magical hat.\"\n- id: 4091 # Mystic robe top\n  examine: \"The upper half of a magical robe.\"\n- id: 4093 # Mystic robe bottom\n  examine: \"The lower half of a magical robe.\"\n- id: 4095 # Mystic gloves\n  examine: \"Magical gloves.\"\n- id: 4097 # Mystic boots\n  examine: \"Magical boots.\"\n- id: 4119 # Bronze boots\n  examine: \"These will protect my feet.\"\n- id: 4121 # Iron boots\n  examine: \"These will protect my feet.\"\n- id: 4123 # Steel boots\n  examine: \"These will protect my feet.\"\n- id: 4125 # Black boots\n  examine: \"These will protect my feet.\"\n- id: 4127 # Mithril boots\n  examine: \"These will protect my feet.\"\n- id: 4129 # Adamant boots\n  examine: \"These will protect my feet.\"\n- id: 4131 # Rune boots\n  examine: \"These will protect my feet.\"\n- id: 4150 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4151 # Abyssal whip\n  examine: \"A weapon from the abyss.\"\n- id: 4153 # Granite maul\n  examine: \"Simplicity is the best weapon.\"\n- id: 4155 # Enchanted gem\n  examine: \"I can contact the Slayer Masters with this.\"\n- id: 4156 # Mirror shield\n  examine: \"I can just about see things in this shield's reflection.\"\n- id: 4158 # Leaf-bladed spear\n  examine: \"A spear with a leaf-shaped point.\"\n- id: 4160 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4161 # Bag of salt\n  examine: \"A bag of salt.\"\n- id: 4162 # Rock hammer\n  examine: \"I can even smash stone with this.\"\n- id: 4164 # Facemask\n  examine: \"Stops me breathing in too much dust.\"\n- id: 4166 # Earmuffs\n  examine: \"These will protect my ears from loud noise.\"\n- id: 4168 # Nose peg\n  examine: \"Protects me from any bad smells.\"\n- id: 4170 # Slayer's staff\n  examine: \"An old and magical staff.\"\n- id: 4172 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4173 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4174 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4175 # Broad arrows\n  examine: \"Arrows with a wider than normal tip.\"\n- id: 4179 # Stick\n  examine: \"For playing fetch with.\"\n- id: 4180 # Dragon platelegs\n  examine: \"These look pretty heavy.\"\n- id: 4182 # Goutweed\n  examine: \"A pale, tough looking herb.\"\n- id: 4183 # Star amulet\n  examine: \"A six-pointed marble and obsidian amulet\"\n- id: 4184 # Cavern key\n  examine: \"Upon close examination, this seems to be a key.\"\n- id: 4185 # Tower key\n  examine: \"Upon close examination, this seems to be a key.\"\n- id: 4186 # Shed key\n  examine: \"Upon close examination, this seems to be a key.\"\n- id: 4187 # Marble amulet\n  examine: \"Triangular in shape, made from marble, and as large as your hand.\"\n- id: 4188 # Obsidian amulet\n  examine: \"Triangular in shape, made from obsidian, and as large as your hand.\"\n- id: 4189 # Garden cane\n  examine: \"A length of garden cane.\"\n- id: 4190 # Garden brush\n  examine: \"A typical garden brush.\"\n- id: 4191 # Extended brush\n  examine: \"A typical garden brush, with a cane tied to it.\"\n- id: 4192 # Extended brush\n  examine: \"A typical garden brush, with two canes tied to it.\"\n- id: 4193 # Extended brush\n  examine: \"A typical garden brush, with three canes tied to it.\"\n- id: 4194 # Torso\n  examine: \"A decomposing torso, from which issues the acrid stench of the grave.\"\n- id: 4195 # Arms\n  examine: \"A pair of limp, dead arms.\"\n- id: 4196 # Legs\n  examine: \"A pair of lifeless, rotting legs.\"\n- id: 4197 # Decapitated head\n  examine: \"A gruesome, decapitated head, whose brain has rotted away.\"\n- id: 4198 # Decapitated head\n  examine: \"A gruesome, decapitated head - its eyes stare lifelessly at nothing.\"\n- id: 4199 # Pickled brain\n  examine: \"A pickled brain, submerged inside a jar of vinegar.\"\n- id: 4200 # Conductor mould\n  examine: \"A mould for making silver lightning conductors.\"\n- id: 4201 # Conductor\n  examine: \"A silver lightning conductor.\"\n- id: 4202 # Ring of charos\n  examine: \"The Ring of Charos.\"\n- id: 4203 # Journal\n  examine: \"A book.\"\n- id: 4204 # Letter\n  examine: \"A letter, clearly hastily written.\"\n- id: 4205 # Consecration seed\n  examine: \"This consecration seed looks grey and dead.\"\n- id: 4206 # Consecration seed\n  examine: \"This consecration seed glows with a warm light.\"\n- id: 4209 # Cadarn lineage\n  examine: \"A book on Cadarn clan history.\"\n- id: 4212 # New crystal bow\n  examine: \"A nice sturdy magical bow.\"\n- id: 4224 # New crystal shield\n  examine: \"A nice sturdy crystal shield.\"\n- id: 4236 # Signed oak bow\n  examine: \"This bow has been signed by Robin, Master Bowman.\"\n- id: 4237 # Nettle-water\n  examine: \"It's a bowl of water, with some nettles in it.\"\n- id: 4239 # Nettle tea\n  examine: \"It's a bowl of nettle tea.\"\n- id: 4240 # Nettle tea\n  examine: \"It's a bowl of milky nettle tea.\"\n- id: 4241 # Nettles\n  examine: \"A handful of nettles.\"\n- id: 4242 # Cup of tea\n  examine: \"A nice cup of nettle tea.\"\n- id: 4243 # Cup of tea\n  examine: \"A milky cup of nettle tea.\"\n- id: 4244 # Porcelain cup\n  examine: \"A porcelain cup.\"\n- id: 4245 # Cup of tea\n  examine: \"Some nettle tea in a porcelain cup.\"\n- id: 4246 # Cup of tea\n  examine: \"Some milky nettle tea in a porcelain cup.\"\n- id: 4247 # Mystical robes\n  examine: \"The Robes of Necrovarus.\"\n- id: 4248 # Book of haricanto\n  examine: \"The Book of Haricanto.\"\n- id: 4249 # Translation manual\n  examine: \"A translation manual.\"\n- id: 4250 # Ghostspeak amulet\n  examine: \"The amulet of ghostspeak glows green from the crone's enchantment.\"\n- id: 4251 # Ectophial\n  examine: \"The Ectophial.\"\n- id: 4252 # Ectophial\n  examine: \"The Ectophial.\"\n- id: 4253 # Model ship\n  examine: \"A small wooden ship.\"\n- id: 4254 # Model ship\n  examine: \"A small wooden ship with a silk flag.\"\n- id: 4255 # Bonemeal\n  examine: \"A pot of crushed bones.\"\n- id: 4256 # Bonemeal\n  examine: \"A pot of crushed bat bones.\"\n- id: 4257 # Bonemeal\n  examine: \"A pot of crushed big bones.\"\n- id: 4258 # Bonemeal\n  examine: \"A pot of crushed burnt bones.\"\n- id: 4259 # Bonemeal\n  examine: \"A pot of crushed burnt jogre bones.\"\n- id: 4260 # Bonemeal\n  examine: \"A pot of crushed baby dragon bones.\"\n- id: 4261 # Bonemeal\n  examine: \"A pot of crushed dragon bones.\"\n- id: 4262 # Bonemeal\n  examine: \"A pot of crushed wolf bones.\"\n- id: 4263 # Bonemeal\n  examine: \"A pot of crushed small ninja monkey bones.\"\n- id: 4264 # Bonemeal\n  examine: \"A pot of crushed medium ninja monkey bones.\"\n- id: 4265 # Bonemeal\n  examine: \"A pot of crushed gorilla monkey bones.\"\n- id: 4266 # Bonemeal\n  examine: \"A pot of crushed bearded gorilla monkey bones.\"\n- id: 4267 # Bonemeal\n  examine: \"A pot of crushed monkey bones.\"\n- id: 4268 # Bonemeal\n  examine: \"A pot of crushed small zombie monkey bones.\"\n- id: 4269 # Bonemeal\n  examine: \"A pot of crushed large zombie monkey bones.\"\n- id: 4270 # Bonemeal\n  examine: \"A pot of crushed skeleton bones.\"\n- id: 4271 # Bonemeal\n  examine: \"A pot of crushed jogre bones.\"\n- id: 4272 # Bone key\n  examine: \"A key dropped by Necrovarus.\"\n- id: 4273 # Chest key\n  examine: \"A key to a chest.\"\n- id: 4274 # Map scrap\n  examine: \"A section from some kind of map.\"\n- id: 4275 # Map scrap\n  examine: \"A section from some kind of map.\"\n- id: 4276 # Map scrap\n  examine: \"A section from some kind of map.\"\n- id: 4277 # Treasure map\n  examine: \"A complete treasure map.\"\n- id: 4278 # Ecto-token\n  examine: \"A token with ectoplasm on it.\"\n- id: 4283 # Petition form\n  examine: \"A scroll of paper containing signatures.\"\n- id: 4284 # Bedsheet\n  examine: \"It's a bedsheet.\"\n- id: 4285 # Bedsheet\n  examine: \"It's an ectoplasm-covered bedsheet.\"\n- id: 4286 # Bucket of slime\n  examine: \"It's a bucket of ectoplasm.\"\n- id: 4287 # Raw beef\n  examine: \"This raw beef is rancid.\"\n- id: 4289 # Raw chicken\n  examine: \"This raw chicken is rancid.\"\n- id: 4291 # Cooked chicken\n  examine: \"This cooked chicken looks disgusting.\"\n- id: 4293 # Cooked meat\n  examine: \"I wouldn't eat that if I were you.\"\n- id: 4298 # Ham shirt\n  examine: \"The label says 'Vivid Crimson', but it looks pink to me!\"\n- id: 4300 # Ham robe\n  examine: \"The label says 'Vivid Crimson', but it looks pink to me!\"\n- id: 4302 # Ham hood\n  examine: \"Light-weight head protection and eye shield.\"\n- id: 4304 # Ham cloak\n  examine: \"A HAM cape.\"\n- id: 4315 # Team-1 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4317 # Team-2 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4319 # Team-3 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4321 # Team-4 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4323 # Team-5 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4325 # Team-6 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4327 # Team-7 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4329 # Team-8 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4331 # Team-9 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4333 # Team-10 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4335 # Team-11 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4337 # Team-12 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4339 # Team-13 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4341 # Team-14 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4343 # Team-15 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4345 # Team-16 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4347 # Team-17 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4349 # Team-18 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4351 # Team-19 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4353 # Team-20 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4355 # Team-21 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4357 # Team-22 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4359 # Team-23 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4361 # Team-24 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4363 # Team-25 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4365 # Team-26 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4367 # Team-27 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4369 # Team-28 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4371 # Team-29 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4373 # Team-30 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4375 # Team-31 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4377 # Team-32 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4379 # Team-33 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4381 # Team-34 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4383 # Team-35 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4385 # Team-36 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4387 # Team-37 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4389 # Team-38 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4391 # Team-39 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4393 # Team-40 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4395 # Team-41 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4397 # Team-42 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4399 # Team-43 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4401 # Team-44 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4403 # Team-45 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4405 # Team-46 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4407 # Team-47 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4409 # Team-48 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4411 # Team-49 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4413 # Team-50 cape\n  examine: \"Ooohhh look at the pretty colours...\"\n- id: 4415 # Blunt axe\n  examine: \"A jungle forester's blunt axe.\"\n- id: 4416 # Herbal tincture\n  examine: \"A strong medicinal brew for heavy chests.\"\n- id: 4417 # Guthix rest(4)\n  examine: \"A cup of Guthix Rest.\"\n- id: 4419 # Guthix rest(3)\n  examine: \"A cup of Guthix Rest.\"\n- id: 4421 # Guthix rest(2)\n  examine: \"A cup of Guthix Rest.\"\n- id: 4423 # Guthix rest(1)\n  examine: \"A cup of Guthix Rest.\"\n- id: 4425 # Stodgy mattress\n  examine: \"A half-filled feather mattress.\"\n- id: 4426 # Comfy mattress\n  examine: \"A comfy-looking feather mattress.\"\n- id: 4427 # Iron oxide\n  examine: \"Looks like a bunch of rust to me.\"\n- id: 4428 # Animate rock scroll\n  examine: \"An animate rock spell is written on this parchment.\"\n- id: 4429 # Broken vane part\n  examine: \"These weathervane directionals are broken.\"\n- id: 4430 # Directionals\n  examine: \"The weathervane directionals should work now.\"\n- id: 4431 # Broken vane part\n  examine: \"This weathervane ornament is damaged.\"\n- id: 4432 # Ornament\n  examine: \"A fixed Weathervane ornament.\"\n- id: 4433 # Broken vane part\n  examine: \"A broken Weathervane pillar.\"\n- id: 4434 # Weathervane pillar\n  examine: \"A fixed weathervane rotating pillar.\"\n- id: 4435 # Weather report\n  examine: \"Clear skies ahead, with some chance of showers, thunderstorms, ice and hail.\"\n- id: 4436 # Airtight pot\n  examine: \"This is pretty well sealed.\"\n- id: 4438 # Unfired pot lid\n  examine: \"This needs firing, then it should fit on a normal-sized pot.\"\n- id: 4440 # Pot lid\n  examine: \"This should fit on a normal-sized pot.\"\n- id: 4442 # Breathing salts\n  examine: \"An airtight pot with something inside, most likely breathing salts.\"\n- id: 4443 # Chicken cage\n  examine: \"A large cage for transporting chickens.\"\n- id: 4444 # Sharpened axe\n  examine: \"A jungle forester's super sharp axe.\"\n- id: 4445 # Red mahogany log\n  examine: \"Some mahogany logs which have been professionally cured.\"\n- id: 4446 # Steel key ring\n  examine: \"I can store my keys here.\"\n- id: 4447 # Antique lamp\n  examine: \"I wonder what happens if I rub it...\"\n- id: 4456 # Bowl of hot water\n  examine: \"It's a bowl of hot water.\"\n- id: 4458 # Cup of water\n  examine: \"A cup of water.\"\n- id: 4460 # Cup of hot water\n  examine: \"It's hot!\"\n- id: 4462 # Ruined herb tea\n  examine: \"A ruined herb tea.\"\n- id: 4464 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water and harralander.\"\n- id: 4466 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water and guam.\"\n- id: 4468 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water and marrentill.\"\n- id: 4470 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, harralander and marrentill.\"\n- id: 4472 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, harralander and guam.\"\n- id: 4474 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water and 2 doses of guam.\"\n- id: 4476 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, guam and marrentill.\"\n- id: 4478 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, harralander, marrentill and guam.\"\n- id: 4480 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, 2 doses of guam and marrentill.\"\n- id: 4482 # Herb tea mix\n  examine: \"An unfinished herb tea made up of water, 2 doses of guam and harralander.\"\n- id: 4484 # Safety guarantee\n  examine: \"The strange characters supposedly grant Svidi safe passage into Rellekka.\"\n- id: 4485 # White pearl\n  examine: \"This fruit is known as White Pearl. Should taste good.\"\n- id: 4486 # White pearl seed\n  examine: \"You can grow this seed even in cold mountain ranges!\"\n- id: 4487 # Half a rock\n  examine: \"It's a piece of the Ancient Rock of the mountain people. It's still just a stone.\"\n- id: 4488 # Corpse of woman\n  examine: \"The corpse of a woman who died long ago.\"\n- id: 4489 # Asleif's necklace\n  examine: \"This used to belong to Asleif, daughter of the mountain camp chieftain.\"\n- id: 4490 # Mud\n  examine: \"Yuck, it's sticky, dirty mud.\"\n- id: 4492 # Muddy rock\n  examine: \"A muddy rock.\"\n- id: 4494 # Pole\n  examine: \"It's just a long stick, really.\"\n- id: 4496 # Broken pole\n  examine: \"Splintered into pieces, it has become completely useless to you.\"\n- id: 4498 # Rope\n  examine: \"A coil of rope.\"\n- id: 4500 # Pole\n  examine: \"It's just a long stick, really.\"\n- id: 4502 # Bearhead\n  examine: \"Quite ferocious looking.\"\n- id: 4503 # Decorative sword\n  examine: \"A very decorative sword.\"\n- id: 4504 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4505 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4506 # Decorative helm\n  examine: \"A very decorative helm.\"\n- id: 4507 # Decorative shield\n  examine: \"A very decorative shield.\"\n- id: 4508 # Decorative sword\n  examine: \"A very decorative sword.\"\n- id: 4509 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4510 # Decorative armour\n  examine: \"Very decorative armour.\"\n- id: 4511 # Decorative helm\n  examine: \"A very decorative helm.\"\n- id: 4512 # Decorative shield\n  examine: \"A very decorative shield.\"\n- id: 4513 # Castlewars hood\n  examine: \"The colours of Saradomin.\"\n- id: 4514 # Castlewars cloak\n  examine: \"A fine castlewars Cape.\"\n- id: 4515 # Castlewars hood\n  examine: \"The colours of Zamorak.\"\n- id: 4516 # Castlewars cloak\n  examine: \"A fine castlewars Cape.\"\n- id: 4517 # Giant frog legs\n  examine: \"This could feed a family of gnomes for a week!\"\n- id: 4522 # Oil lamp\n  examine: \"Not the genie sort.\"\n- id: 4524 # Oil lamp\n  examine: \"Not the genie sort.\"\n- id: 4529 # Candle lantern\n  examine: \"A candle in a glass cage.\"\n- id: 4531 # Candle lantern\n  examine: \"A flickering candle in a glass cage.\"\n- id: 4532 # Candle lantern\n  examine: \"A candle in a glass cage.\"\n- id: 4534 # Candle lantern\n  examine: \"A flickering candle in a glass cage.\"\n- id: 4537 # Oil lantern\n  examine: \"An unlit oil lantern.\"\n- id: 4539 # Oil lantern\n  examine: \"It lights your way through the dark places of the earth.\"\n- id: 4540 # Oil lantern frame\n  examine: \"Add the glass to complete.\"\n- id: 4542 # Lantern lens\n  examine: \"A roughly circular disc of glass.\"\n- id: 4548 # Bullseye lantern\n  examine: \"A sturdy steel lantern.\"\n- id: 4550 # Bullseye lantern\n  examine: \"A sturdy steel lantern casting a bright beam.\"\n- id: 4551 # Spiny helmet\n  examine: \"You don't want to wear it inside-out.\"\n- id: 4561 # Purple sweets\n  examine: \"Not likely to last until next Halloween.\"\n- id: 4566 # Rubber chicken\n  examine: \"Perhaps not the most powerful weapon in Gielinor.\"\n- id: 4567 # Gold helmet\n  examine: \"Made of gold and white gold.\"\n- id: 4568 # Dwarven lore\n  examine: \"The book is almost falling apart, you'll have to handle it quite carefully.\"\n- id: 4569 # Book page 1\n  examine: \"A missing page from Rolad's book! It seems to be the first one.\"\n- id: 4570 # Book page 2\n  examine: \"A missing page from Rolad's book! It seems to be the second one.\"\n- id: 4571 # Book page 3\n  examine: \"A missing page from Rolad's book! It seems to be the third one.\"\n- id: 4572 # Pages\n  examine: \"A collection of missing pages from Rolad's book!\"\n- id: 4573 # Pages\n  examine: \"A collection of missing pages from Rolad's book!\"\n- id: 4574 # Base schematics\n  examine: \"These are the base schematics of a dwarven multicannon\"\n- id: 4575 # Schematic\n  examine: \"A transparent overlay - details of something?\"\n- id: 4576 # Schematics\n  examine: \"Transparent overlays - details of something?\"\n- id: 4577 # Schematics\n  examine: \"Transparent overlays - details of something?\"\n- id: 4578 # Schematic\n  examine: \"The assembled schematic for modifying the dwarven multicannon.\"\n- id: 4579 # Cannon ball\n  examine: \"A heavy gold metal sphere.\"\n- id: 4580 # Black spear\n  examine: \"A black tipped spear.\"\n- id: 4582 # Black spear(p)\n  examine: \"A poisoned black tipped spear.\"\n- id: 4584 # Black spear(kp)\n  examine: \"A Karambwan poisoned black tipped spear.\"\n- id: 4585 # Dragon plateskirt\n  examine: \"This looks pretty heavy.\"\n- id: 4587 # Dragon scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 4589 # Keys\n  examine: \"Keys to the Mayor's house..\"\n- id: 4590 # Jewels\n  examine: \"The Mayor of Pollnivneach's wife's jewels.\"\n- id: 4593 # Fake beard\n  examine: \"Makes me itch.\"\n- id: 4597 # Note\n  examine: \"A note found in the Mayor's bedroom mentioning the word 'Fibonacci'.\"\n- id: 4598 # Note\n  examine: \"A list of 5 numbers.\"\n- id: 4601 # Ugthanki dung\n  examine: \"Dung of the Camelus Horribleus variety.\"\n- id: 4602 # Ugthanki dung\n  examine: \"Dung of the Camelus Horribleus variety.\"\n- id: 4603 # Receipt\n  examine: \"A receipt for one 'Camelus Horribleus'.\"\n- id: 4604 # Hag's poison\n  examine: \"A red viscous liquid in a vial.\"\n- id: 4605 # Snake charm\n  examine: \"Makes a hissing sound.\"\n- id: 4606 # Snake basket\n  examine: \"This is used to hold snakes.\"\n- id: 4607 # Snake basket full\n  examine: \"This basket contains a snake.\"\n- id: 4608 # Super kebab\n  examine: \"A meaty and very hot kebab.\"\n- id: 4610 # Red hot sauce\n  examine: \"The bottle feels warm.\"\n- id: 4611 # Desert disguise\n  examine: \"A disguise suitable for the desert.\"\n- id: 4613 # Spinning plate\n  examine: \"It has a picture of a dragon on it.\"\n- id: 4615 # Letter\n  examine: \"An old faded letter.\"\n- id: 4616 # Varmen's notes\n  examine: \"An archaeologist's notes.\"\n- id: 4617 # Display cabinet key\n  examine: \"The museum curator's key.\"\n- id: 4618 # Statuette\n  examine: \"A beautifully-carved stone statuette.\"\n- id: 4619 # Strange implement\n  examine: \"It's pretty, but you wish you knew what it was.\"\n- id: 4620 # Black mushroom\n  examine: \"It looks horrible.\"\n- id: 4621 # Phoenix feather\n  examine: \"A long feather patterned like a flame.\"\n- id: 4622 # Black mushroom ink\n  examine: \"Black ink made out of mushrooms.\"\n- id: 4623 # Phoenix quill pen\n  examine: \"A phoenix feather dipped in ink.\"\n- id: 4624 # Golem program\n  examine: \"It reads 'YOUR TASK IS DONE'.\"\n- id: 4627 # Bandit's brew\n  examine: \"A cheeky little lager.\"\n- id: 4654 # Etchings\n  examine: \"A copy of the engravings found on a mysterious stone tablet.\"\n- id: 4655 # Translation\n  examine: \"A rough translation made from archaeological etchings.\"\n- id: 4656 # Warm key\n  examine: \"This key is unusually warm to the touch.\"\n- id: 4657 # Ring of visibility\n  examine: \"A ring that allows you to see things that are normally invisible...\"\n- id: 4658 # Silver pot\n  examine: \"A silver pot made by Ruantun.\"\n- id: 4659 # Blessed pot\n  examine: \"A silver pot made by Ruantun and blessed on Entrana.\"\n- id: 4660 # Silver pot\n  examine: \"A silver pot made by Ruantun filled with your blood.\"\n- id: 4661 # Blessed pot\n  examine: \"A blessed silver pot made by Ruantun filled with your blood.\"\n- id: 4662 # Silver pot\n  examine: \"A silver pot made by Ruantun filled with blood and garlic.\"\n- id: 4663 # Blessed pot\n  examine: \"A blessed silver pot filled with blood and garlic.\"\n- id: 4664 # Silver pot\n  examine: \"A silver pot made by Ruantun filled with blood, garlic and spices.\"\n- id: 4665 # Blessed pot\n  examine: \"A blessed silver pot filled with blood, garlic and spices.\"\n- id: 4666 # Silver pot\n  examine: \"A silver pot made by Ruantun filled with blood and spices.\"\n- id: 4667 # Blessed pot\n  examine: \"A blessed silver pot filled with blood and spices.\"\n- id: 4668 # Garlic powder\n  examine: \"Finely ground garlic powder.\"\n- id: 4670 # Blood diamond\n  examine: \"The Diamond of Blood.\"\n- id: 4671 # Ice diamond\n  examine: \"The Diamond of Ice.\"\n- id: 4672 # Smoke diamond\n  examine: \"The Diamond of Smoke.\"\n- id: 4673 # Shadow diamond\n  examine: \"The Diamond of Shadow.\"\n- id: 4674 # Gilded cross\n  examine: \"An old and strangely shaped metal cross.\"\n- id: 4675 # Ancient staff\n  examine: \"A magical staff of ancient origin...\"\n- id: 4677 # Catspeak amulet\n  examine: \"It's an amulet of cat speak. It makes vague purring noises.\"\n- id: 4678 # Canopic jar\n  examine: \"Has a lid shaped like a man. I think it contains someone's liver. Yuck.\"\n- id: 4679 # Canopic jar\n  examine: \"Has a lid shaped like an ape. Eeew! I think it contains someone's intestines.\"\n- id: 4680 # Canopic jar\n  examine: \"Has a lid shaped like a bug. Disgusting! I think there's a stomach inside.\"\n- id: 4681 # Canopic jar\n  examine: \"Has a lid shaped like a crocodile. Yuck, I think there are lungs inside.\"\n- id: 4682 # Holy symbol\n  examine: \"Menaphite lucky charm.\"\n- id: 4683 # Unholy symbol\n  examine: \"Sign of the devourer.\"\n- id: 4684 # Linen\n  examine: \"One sheet of mummy wrap.\"\n- id: 4686 # Embalming manual\n  examine: \"Little book of embalming by Bod E. Wrapper.\"\n- id: 4687 # Bucket of sap\n  examine: \"It's a bucket of sap.\"\n- id: 4689 # Pile of salt\n  examine: \"A little heap of salt.\"\n- id: 4691 # Sphinx's token\n  examine: \"Miniature golden statue of a sphinx.\"\n- id: 4693 # Full bucket\n  examine: \"It's a bucket of salty water.\"\n- id: 4694 # Steam rune\n  examine: \"A combined Water and Fire Rune.\"\n- id: 4695 # Mist rune\n  examine: \"A combined Air and Water Rune.\"\n- id: 4696 # Dust rune\n  examine: \"A combined Air and Earth Rune.\"\n- id: 4697 # Smoke rune\n  examine: \"A combined Air and Fire Rune.\"\n- id: 4698 # Mud rune\n  examine: \"A combined Earth and Water Rune.\"\n- id: 4699 # Lava rune\n  examine: \"A combined Earth and Fire Rune.\"\n- id: 4700 # Sapphire lantern\n  examine: \"You need to add lamp oil before you can use it.\"\n- id: 4701 # Sapphire lantern\n  examine: \"A bullseye lantern with a sapphire for a lens.\"\n- id: 4702 # Sapphire lantern\n  examine: \"A lantern casting a bright blue beam.\"\n- id: 4703 # Magic stone\n  examine: \"Doesn't look very special.\"\n- id: 4704 # Stone bowl\n  examine: \"A magic stone bowl for catching the tears of Guthix.\"\n- id: 4707 # Crumbling tome\n  examine: \"This book must be really old!\"\n- id: 4740 # Bolt rack\n  examine: \"Must need a special type of crossbow to use this.\"\n- id: 4773 # Bronze brutal\n  examine: \"Blunt bronze arrow... ouch.\"\n- id: 4778 # Iron brutal\n  examine: \"Blunt iron arrow... ouch.\"\n- id: 4783 # Steel brutal\n  examine: \"Blunt steel arrow... ouch.\"\n- id: 4788 # Black brutal\n  examine: \"Blunt black arrow... ouch.\"\n- id: 4793 # Mithril brutal\n  examine: \"Blunt mithril arrow... ouch.\"\n- id: 4798 # Adamant brutal\n  examine: \"Blunt adamantite arrow... ouch.\"\n- id: 4803 # Rune brutal\n  examine: \"Blunt rune arrow... ouch.\"\n- id: 4808 # Black prism\n  examine: \"A very black prism.\"\n- id: 4809 # Torn page\n  examine: \"A half torn necromantic page.\"\n- id: 4810 # Ruined backpack\n  examine: \"A broken and useless looking backpack with the moniker,'B.Vahn' in it.\"\n- id: 4811 # Dragon inn tankard\n  examine: \"A white ceramic mug with a dragon insignia.\"\n- id: 4812 # Zogre bones\n  examine: \"A pile of Zombie Ogre bones.\"\n- id: 4814 # Sithik portrait\n  examine: \"A classic realist charcoal portrait of Sithik.\"\n- id: 4815 # Sithik portrait\n  examine: \"A semi-nihilistic, pseudo-impressionistic, half-squarist charcoal sketch of Sithik.\"\n- id: 4816 # Signed portrait\n  examine: \"A signed classic realist charcoal portrait of Sithik.\"\n- id: 4817 # Book of portraiture\n  examine: \"A book explaining the art of portraiture.\"\n- id: 4818 # Ogre artefact\n  examine: \"An ancient ogre artefact - resembling a heavy large helm.\"\n- id: 4819 # Bronze nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4820 # Iron nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4821 # Black nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4822 # Mithril nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4823 # Adamantite nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4824 # Rune nails\n  examine: \"Keeps things in place fairly permanently.\"\n- id: 4825 # Unstrung comp bow\n  examine: \"An unstrung composite ogre bow.\"\n- id: 4827 # Comp ogre bow\n  examine: \"A composite ogre bow.\"\n- id: 4829 # Book of 'h.a.m'\n  examine: \"A book of H.A.M affiliation.\"\n- id: 4830 # Fayrg bones\n  examine: \"Ancient ogre bones from the ogre burial tomb.\"\n- id: 4832 # Raurg bones\n  examine: \"Ancient ogre bones from the ogre burial tomb.\"\n- id: 4834 # Ourg bones\n  examine: \"Ancient ogre bones from the ogre burial tomb.\"\n- id: 4836 # Strange potion\n  examine: \"Some strange liquid given to you by Zavistic Rarve.\"\n- id: 4837 # Necromancy book\n  examine: \"A book of necromantic spells.\"\n- id: 4839 # Ogre gate key\n  examine: \"A key to some sort of special tomb area.\"\n- id: 4840 # Unfinished potion\n  examine: \"I need another ingredient to finish this rogue's purse potion.\"\n- id: 4842 # Relicym's balm(4)\n  examine: \"4 doses of Relicym's balm, which helps cure disease.\"\n- id: 4844 # Relicym's balm(3)\n  examine: \"3 doses of Relicym's balm, which helps cure disease.\"\n- id: 4846 # Relicym's balm(2)\n  examine: \"2 doses of Relicym's balm, which helps cure disease.\"\n- id: 4848 # Relicym's balm(1)\n  examine: \"1 dose of Relicym's balm, which helps cure disease.\"\n- id: 4850 # Ogre coffin key\n  examine: \"A key which opens coffins!.\"\n- id: 4852 # Bonemeal\n  examine: \"A pot of crushed zogre bones.\"\n- id: 4853 # Bonemeal\n  examine: \"A pot of crushed fayrg bones.\"\n- id: 4854 # Bonemeal\n  examine: \"A pot of crushed raurg bones.\"\n- id: 4855 # Bonemeal\n  examine: \"A pot of crushed ourg bones.\"\n- id: 5001 # Raw cave eel\n  examine: \"It's incredibly slimy.\"\n- id: 5002 # Burnt cave eel\n  examine: \"It's no longer slimy, or edible.\"\n- id: 5003 # Cave eel\n  examine: \"It's a bit slimy.\"\n- id: 5004 # Frog spawn\n  examine: \"That's disgusting!\"\n- id: 5008 # Brooch\n  examine: \"A stone brooch with a symbol on it.\"\n- id: 5009 # Goblin symbol book\n  examine: \"A book about the ancient goblin tribes.\"\n- id: 5010 # Key\n  examine: \"The key you stole from Sigmund.\"\n- id: 5011 # Silverware\n  examine: \"You found the Lumbridge silverware in the HAM cave.\"\n- id: 5012 # Peace treaty\n  examine: \"A peace treaty between Lumbridge and the Cave Goblins.\"\n- id: 5013 # Mining helmet\n  examine: \"A helmet with a lamp on it.\"\n- id: 5014 # Mining helmet\n  examine: \"A helmet with an unlit lamp on it.\"\n- id: 5016 # Bone spear\n  examine: \"Basic but brutal!\"\n- id: 5018 # Bone club\n  examine: \"Basic but brutal!\"\n- id: 5020 # Minecart ticket\n  examine: \"A ticket to take you from Keldagrim to the dwarven mines under Ice Mountain.\"\n- id: 5021 # Minecart ticket\n  examine: \"A ticket to take you from the dwarven mines under Ice Mountain to Keldagrim.\"\n- id: 5022 # Minecart ticket\n  examine: \"A ticket to take you from Keldagrim to the passage under White Wolf Mountain.\"\n- id: 5023 # Minecart ticket\n  examine: \"A ticket to take you from the passage under White Wolf Mountain to Keldagrim.\"\n- id: 5024 # Woven top\n  examine: \"Far too small to wear.\"\n- id: 5026 # Woven top\n  examine: \"Yellow top, too small for me.\"\n- id: 5028 # Woven top\n  examine: \"Blue top, very tiny.\"\n- id: 5030 # Shirt\n  examine: \"Tiny!\"\n- id: 5032 # Shirt\n  examine: \"Tiny!\"\n- id: 5034 # Shirt\n  examine: \"Tiny!\"\n- id: 5036 # Trousers\n  examine: \"A pair of long dwarven trousers... long for dwarves, of course.\"\n- id: 5038 # Trousers\n  examine: \"A pair of long dwarven trousers... long for dwarves, of course.\"\n- id: 5040 # Trousers\n  examine: \"A pair of long dwarven trousers... long for dwarves, of course.\"\n- id: 5042 # Shorts\n  examine: \"These look great, on dwarves!\"\n- id: 5044 # Shorts\n  examine: \"Yellow shorts. Far too small for you.\"\n- id: 5046 # Shorts\n  examine: \"Blue shorts, these would look great on dwarves!\"\n- id: 5048 # Skirt\n  examine: \"A brown skirt. Size small!\"\n- id: 5050 # Skirt\n  examine: \"Lilac skirt.\"\n- id: 5052 # Skirt\n  examine: \"A blue skirt.\"\n- id: 5056 # Dwarven battleaxe\n  examine: \"This looks very rusty and worn.\"\n- id: 5057 # Dwarven battleaxe\n  examine: \"Three sapphires have been crafted onto the hilt.\"\n- id: 5058 # Dwarven battleaxe\n  examine: \"This axe blade has been sharpened.\"\n- id: 5059 # Dwarven battleaxe\n  examine: \"This axe has a sharp blade and there are sapphires in the hilt.\"\n- id: 5062 # Left boot\n  examine: \"One of a pair I assume.\"\n- id: 5063 # Right boot\n  examine: \"A good looking boot, for the right foot. Literally.\"\n- id: 5064 # Exquisite boots\n  examine: \"A lovely pair of boots.\"\n- id: 5065 # Book on costumes\n  examine: \"An old library book. It bears the title 'Scholars Guide to Dwarven Costumes'.\"\n- id: 5066 # Meeting notes\n  examine: \"These notes are from a meeting of the Keldagrim Consortium.\"\n- id: 5067 # Exquisite clothes\n  examine: \"Clothes for the sculptor's model.\"\n- id: 5070 # Bird nest\n  examine: \"It's a bird's nest with an egg in it.\"\n- id: 5071 # Bird nest\n  examine: \"It's a bird's nest with an egg in it.\"\n- id: 5072 # Bird nest\n  examine: \"It's a bird's nest with an egg in it.\"\n- id: 5073 # Bird nest\n  examine: \"It's a bird's nest with some seeds in it.\"\n- id: 5074 # Bird nest\n  examine: \"It's a bird's nest with a ring in it.\"\n- id: 5075 # Bird nest\n  examine: \"It's an empty bird's nest.\"\n- id: 5076 # Bird's egg\n  examine: \"A red bird's egg.\"\n- id: 5077 # Bird's egg\n  examine: \"A blue bird's egg.\"\n- id: 5078 # Bird's egg\n  examine: \"A green bird's egg.\"\n- id: 5096 # Marigold seed\n  examine: \"A marigold seed - plant in a flower patch.\"\n- id: 5097 # Rosemary seed\n  examine: \"A rosemary seed - plant in a flower patch.\"\n- id: 5098 # Nasturtium seed\n  examine: \"A nasturtium seed - plant in a flower patch.\"\n- id: 5099 # Woad seed\n  examine: \"A woad seed - plant in a flower patch.\"\n- id: 5100 # Limpwurt seed\n  examine: \"A limpwurt seed - plant in a flower patch.\"\n- id: 5101 # Redberry seed\n  examine: \"A redberry bush seed - plant in a bush patch.\"\n- id: 5102 # Cadavaberry seed\n  examine: \"A cadavaberry bush seed - plant in a bush patch.\"\n- id: 5103 # Dwellberry seed\n  examine: \"A dwellberry bush seed - plant in a bush patch.\"\n- id: 5104 # Jangerberry seed\n  examine: \"A jangerberry bush seed - plant in a bush patch.\"\n- id: 5105 # Whiteberry seed\n  examine: \"A whiteberry bush seed - plant in a bush patch.\"\n- id: 5106 # Poison ivy seed\n  examine: \"A poison ivy bush seed - plant in a bush patch.\"\n- id: 5280 # Cactus seed\n  examine: \"A Cactus seed - plant in a cactus patch.\"\n- id: 5281 # Belladonna seed\n  examine: \"Also known as Deadly Nightshade - plant in a belladonna patch.\"\n- id: 5282 # Mushroom spore\n  examine: \"A Bittercap mushroom spore - plant in a mushroom patch.\"\n- id: 5283 # Apple tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5284 # Banana tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5285 # Orange tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5286 # Curry tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5287 # Pineapple seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5288 # Papaya tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5289 # Palm tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5290 # Calquat tree seed\n  examine: \"Plant in a plantpot of soil to grow a sapling.\"\n- id: 5291 # Guam seed\n  examine: \"A guam seed - plant in a herb patch.\"\n- id: 5292 # Marrentill seed\n  examine: \"A marrentill seed - plant in a herb patch.\"\n- id: 5293 # Tarromin seed\n  examine: \"A tarromin seed - plant in a herb patch.\"\n- id: 5294 # Harralander seed\n  examine: \"A harralander seed - plant in a herb patch.\"\n- id: 5295 # Ranarr seed\n  examine: \"A ranarr seed - plant in a herb patch.\"\n- id: 5296 # Toadflax seed\n  examine: \"A toadflax seed - plant in a herb patch.\"\n- id: 5297 # Irit seed\n  examine: \"An irit seed - plant in a herb patch.\"\n- id: 5298 # Avantoe seed\n  examine: \"An avantoe seed - plant in a herb patch.\"\n- id: 5299 # Kwuarm seed\n  examine: \"A kwuarm seed - plant in a herb patch.\"\n- id: 5300 # Snapdragon seed\n  examine: \"A snapdragon seed - plant in a herb patch.\"\n- id: 5301 # Cadantine seed\n  examine: \"A cadantine seed - plant in a herb patch.\"\n- id: 5302 # Lantadyme seed\n  examine: \"A lantadyme seed - plant in a herb patch.\"\n- id: 5303 # Dwarf weed seed\n  examine: \"A dwarf weed seed - plant in a herb patch.\"\n- id: 5304 # Torstol seed\n  examine: \"A torstol seed - plant in a herb patch.\"\n- id: 5305 # Barley seed\n  examine: \"A barley seed - plant in a hops patch.\"\n- id: 5306 # Jute seed\n  examine: \"A jute plant seed - plant in a hops patch.\"\n- id: 5307 # Hammerstone seed\n  examine: \"A Hammerstone hop seed - plant in a hops patch.\"\n- id: 5308 # Asgarnian seed\n  examine: \"An Asgarnian hop seed - plant in a hops patch.\"\n- id: 5309 # Yanillian seed\n  examine: \"A Yanillian hop seed - plant in a hops patch.\"\n- id: 5310 # Krandorian seed\n  examine: \"A Krandorian hop seed - plant in a hops patch.\"\n- id: 5311 # Wildblood seed\n  examine: \"A Wildblood hop seed - plant in a hops patch.\"\n- id: 5312 # Acorn\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5313 # Willow seed\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5314 # Maple seed\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5315 # Yew seed\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5316 # Magic seed\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5317 # Spirit seed\n  examine: \"Plant this in a plantpot of soil to grow a sapling.\"\n- id: 5318 # Potato seed\n  examine: \"A potato seed - plant in an allotment.\"\n- id: 5319 # Onion seed\n  examine: \"An onion seed - plant in an allotment.\"\n- id: 5320 # Sweetcorn seed\n  examine: \"A sweetcorn seed - plant in an allotment.\"\n- id: 5321 # Watermelon seed\n  examine: \"A watermelon seed - plant in an allotment.\"\n- id: 5322 # Tomato seed\n  examine: \"A tomato seed - plant in an allotment.\"\n- id: 5323 # Strawberry seed\n  examine: \"A strawberry seed - plant in an allotment.\"\n- id: 5324 # Cabbage seed\n  examine: \"A cabbage seed - plant in an allotment.\"\n- id: 5325 # Gardening trowel\n  examine: \"Not suitable for archaeological digs.\"\n- id: 5327 # Spade handle\n  examine: \"I need to attach this to its head.\"\n- id: 5328 # Spade head\n  examine: \"I need to attach this to its handle.\"\n- id: 5329 # Secateurs\n  examine: \"Good for pruning away diseased leaves.\"\n- id: 5331 # Watering can\n  examine: \"This watering can is empty.\"\n- id: 5333 # Watering can(1)\n  examine: \"This watering can is almost empty.\"\n- id: 5334 # Watering can(2)\n  examine: \"This watering can is three-quarters empty.\"\n- id: 5335 # Watering can(3)\n  examine: \"This watering can is just under half-full.\"\n- id: 5336 # Watering can(4)\n  examine: \"Some would say this watering can is half-full, others half-empty.\"\n- id: 5337 # Watering can(5)\n  examine: \"This watering can is just over half-full.\"\n- id: 5338 # Watering can(6)\n  examine: \"This watering can is three quarters full.\"\n- id: 5339 # Watering can(7)\n  examine: \"This watering can is almost completely full.\"\n- id: 5340 # Watering can(8)\n  examine: \"This watering can is completely full.\"\n- id: 5341 # Rake\n  examine: \"Use this to clear weeds.\"\n- id: 5343 # Seed dibber\n  examine: \"Use this to plant seeds with.\"\n- id: 5345 # Gardening boots\n  examine: \"A pair of gardening boots.\"\n- id: 5347 # Rake handle\n  examine: \"I need to reattach this to its head.\"\n- id: 5348 # Rake head\n  examine: \"I need to reattach this to its handle.\"\n- id: 5352 # Unfired plant pot\n  examine: \"An unfired plant pot.\"\n- id: 5358 # Oak seedling\n  examine: \"An acorn has been sown in this plant pot.\"\n- id: 5359 # Willow seedling\n  examine: \"A willow tree seed has been sown in this plant pot.\"\n- id: 5360 # Maple seedling\n  examine: \"A maple tree seed has been sown in this plant pot.\"\n- id: 5361 # Yew seedling\n  examine: \"A yew tree seed has been sown in this plant pot.\"\n- id: 5362 # Magic seedling\n  examine: \"A magic tree seed has been sown in this plant pot.\"\n- id: 5363 # Spirit seedling\n  examine: \"A spirit tree seed has been sown in this plant pot.\"\n- id: 5370 # Oak sapling\n  examine: \"This sapling is ready to be replanted in a tree patch.\"\n- id: 5371 # Willow sapling\n  examine: \"This sapling is ready to be replanted in a tree patch.\"\n- id: 5372 # Maple sapling\n  examine: \"This sapling is ready to be replanted in a tree patch.\"\n- id: 5373 # Yew sapling\n  examine: \"This sapling is ready to be replanted in a tree patch.\"\n- id: 5374 # Magic sapling\n  examine: \"This sapling is ready to be replanted in a tree patch.\"\n- id: 5375 # Spirit sapling\n  examine: \"This sapling is ready to be replanted in a Spirit patch.\"\n- id: 5376 # Basket\n  examine: \"An empty fruit basket.\"\n- id: 5378 # Apples(1)\n  examine: \"A fruit basket filled with apples.\"\n- id: 5380 # Apples(2)\n  examine: \"A fruit basket filled with apples.\"\n- id: 5382 # Apples(3)\n  examine: \"A fruit basket filled with apples.\"\n- id: 5384 # Apples(4)\n  examine: \"A fruit basket filled with apples.\"\n- id: 5386 # Apples(5)\n  examine: \"A fruit basket filled with apples.\"\n- id: 5388 # Oranges(1)\n  examine: \"A fruit basket filled with oranges.\"\n- id: 5390 # Oranges(2)\n  examine: \"A fruit basket filled with oranges.\"\n- id: 5392 # Oranges(3)\n  examine: \"A fruit basket filled with oranges.\"\n- id: 5394 # Oranges(4)\n  examine: \"A fruit basket filled with oranges.\"\n- id: 5396 # Oranges(5)\n  examine: \"A fruit basket filled with oranges.\"\n- id: 5398 # Strawberries(1)\n  examine: \"A fruit basket filled with strawberries.\"\n- id: 5400 # Strawberries(2)\n  examine: \"A fruit basket filled with strawberries.\"\n- id: 5402 # Strawberries(3)\n  examine: \"A fruit basket filled with strawberries.\"\n- id: 5404 # Strawberries(4)\n  examine: \"A fruit basket filled with strawberries.\"\n- id: 5406 # Strawberries(5)\n  examine: \"A fruit basket filled with strawberries.\"\n- id: 5408 # Bananas(1)\n  examine: \"A fruit basket filled with bananas.\"\n- id: 5410 # Bananas(2)\n  examine: \"A fruit basket filled with bananas.\"\n- id: 5412 # Bananas(3)\n  examine: \"A fruit basket filled with bananas.\"\n- id: 5414 # Bananas(4)\n  examine: \"A fruit basket filled with bananas.\"\n- id: 5416 # Bananas(5)\n  examine: \"A fruit basket filled with bananas.\"\n- id: 5418 # Empty sack\n  examine: \"An empty vegetable sack.\"\n- id: 5420 # Potatoes(1)\n  examine: \"There is 1 potato in this sack.\"\n- id: 5422 # Potatoes(2)\n  examine: \"There are 2 potatoes in this sack.\"\n- id: 5424 # Potatoes(3)\n  examine: \"There are 3 potatoes in this sack.\"\n- id: 5426 # Potatoes(4)\n  examine: \"There are 4 potatoes in this sack.\"\n- id: 5428 # Potatoes(5)\n  examine: \"There are 5 potatoes in this sack.\"\n- id: 5430 # Potatoes(6)\n  examine: \"There are 6 potatoes in this sack.\"\n- id: 5432 # Potatoes(7)\n  examine: \"There are 7 potatoes in this sack.\"\n- id: 5434 # Potatoes(8)\n  examine: \"There are 8 potatoes in this sack.\"\n- id: 5436 # Potatoes(9)\n  examine: \"There are 9 potatoes in this sack.\"\n- id: 5438 # Potatoes(10)\n  examine: \"There are 10 potatoes in this sack.\"\n- id: 5440 # Onions(1)\n  examine: \"There is 1 onion in this sack.\"\n- id: 5442 # Onions(2)\n  examine: \"There are 2 onions in this sack.\"\n- id: 5444 # Onions(3)\n  examine: \"There are 3 onions in this sack.\"\n- id: 5446 # Onions(4)\n  examine: \"There are 4 onions in this sack.\"\n- id: 5448 # Onions(5)\n  examine: \"There are 5 onions in this sack.\"\n- id: 5450 # Onions(6)\n  examine: \"There are 6 onions in this sack.\"\n- id: 5452 # Onions(7)\n  examine: \"There are 7 onions in this sack.\"\n- id: 5454 # Onions(8)\n  examine: \"There are 8 onions in this sack.\"\n- id: 5456 # Onions(9)\n  examine: \"There are 9 onions in this sack.\"\n- id: 5458 # Onions(10)\n  examine: \"There are 10 onions in this sack.\"\n- id: 5460 # Cabbages(1)\n  examine: \"There is 1 cabbage in this sack.\"\n- id: 5462 # Cabbages(2)\n  examine: \"There are 2 cabbages in this sack.\"\n- id: 5464 # Cabbages(3)\n  examine: \"There are 3 cabbages in this sack.\"\n- id: 5466 # Cabbages(4)\n  examine: \"There are 4 cabbages in this sack.\"\n- id: 5468 # Cabbages(5)\n  examine: \"There are 5 cabbages in this sack.\"\n- id: 5470 # Cabbages(6)\n  examine: \"There are 6 cabbages in this sack.\"\n- id: 5472 # Cabbages(7)\n  examine: \"There are 7 cabbages in this sack.\"\n- id: 5474 # Cabbages(8)\n  examine: \"There are 8 cabbages in this sack.\"\n- id: 5476 # Cabbages(9)\n  examine: \"There are 9 cabbages in this sack.\"\n- id: 5478 # Cabbages(10)\n  examine: \"There are 10 cabbages in this sack.\"\n- id: 5480 # Apple seedling\n  examine: \"An apple tree seed has been sown in this plant pot.\"\n- id: 5481 # Banana seedling\n  examine: \"A banana tree seed has been sown in this plant pot.\"\n- id: 5482 # Orange seedling\n  examine: \"An orange tree seed has been sown in this plant pot.\"\n- id: 5483 # Curry seedling\n  examine: \"A curry tree seed has been sown in this plant pot.\"\n- id: 5484 # Pineapple seedling\n  examine: \"A pineapple plant seed has been sown in this plant pot.\"\n- id: 5485 # Papaya seedling\n  examine: \"A papaya tree seed has been sown in this plant pot.\"\n- id: 5486 # Palm seedling\n  examine: \"A palm tree seed has been sown in this plant pot.\"\n- id: 5487 # Calquat seedling\n  examine: \"A Calquat tree seed has been sown in this plant pot.\"\n- id: 5496 # Apple sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5497 # Banana sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5498 # Orange sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5499 # Curry sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5500 # Pineapple sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5501 # Papaya sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5502 # Palm sapling\n  examine: \"This sapling is ready to be replanted in a fruit tree patch.\"\n- id: 5503 # Calquat sapling\n  examine: \"This sapling is ready to be replanted in a Calquat tree patch.\"\n- id: 5504 # Strawberry\n  examine: \"A freshly picked strawberry.\"\n- id: 5506 # Old man's message\n  examine: \"The Wise Old Man of Draynor Village asked you to take this to someone.\"\n- id: 5507 # Strange book\n  examine: \"A tatty old book belonging to the Wise Old Man of Draynor Village.\"\n- id: 5508 # Book of folklore\n  examine: \"A tatty old book belonging to the Wise Old Man of Draynor Village.\"\n- id: 5509 # Small pouch\n  examine: \"A small pouch used for storing essence.\"\n- id: 5510 # Medium pouch\n  examine: \"A medium-sized pouch used for storing essence.\"\n- id: 5511 # Medium pouch\n  examine: \"A damaged medium-sized pouch used for storing essence.\"\n- id: 5512 # Large pouch\n  examine: \"A large pouch used for storing essence.\"\n- id: 5513 # Large pouch\n  examine: \"A large damaged pouch used for storing essence.\"\n- id: 5514 # Giant pouch\n  examine: \"A giant-sized pouch used for storing essence.\"\n- id: 5515 # Giant pouch\n  examine: \"A damaged giant-sized pouch used for storing essence.\"\n- id: 5516 # Elemental talisman\n  examine: \"A mysterious power emanates from the talisman...\"\n- id: 5518 # Scrying orb\n  examine: \"This contains mystical teleport information...\"\n- id: 5519 # Scrying orb\n  examine: \"This orb apparently contains a cypher spell.\"\n- id: 5520 # Abyssal book\n  examine: \"Some research notes on abyssal space.\"\n- id: 5521 # Binding necklace\n  examine: \"A necklace embedded with mystical power.\"\n- id: 5523 # Tiara mould\n  examine: \"A mould for tiaras.\"\n- id: 5525 # Tiara\n  examine: \"Makes me feel like a Princess.\"\n- id: 5527 # Air tiara\n  examine: \"A tiara infused with the properties of air.\"\n- id: 5529 # Mind tiara\n  examine: \"A tiara infused with the properties of the mind.\"\n- id: 5531 # Water tiara\n  examine: \"A tiara infused with the properties of water.\"\n- id: 5533 # Body tiara\n  examine: \"A tiara infused with the properties of the body.\"\n- id: 5535 # Earth tiara\n  examine: \"A tiara infused with the properties of the earth.\"\n- id: 5537 # Fire tiara\n  examine: \"A tiara infused with the properties of fire.\"\n- id: 5539 # Cosmic tiara\n  examine: \"A tiara infused with the properties of the cosmos.\"\n- id: 5541 # Nature tiara\n  examine: \"A tiara infused with the properties of nature.\"\n- id: 5543 # Chaos tiara\n  examine: \"A tiara infused with the properties of chaos.\"\n- id: 5545 # Law tiara\n  examine: \"A tiara infused with the properties of law.\"\n- id: 5547 # Death tiara\n  examine: \"A tiara infused with the properties of death.\"\n- id: 5553 # Rogue top\n  examine: \"Black banded leather armour, a rogue's dream!\"\n- id: 5554 # Rogue mask\n  examine: \"Black banded leather armour, a rogue's dream!\"\n- id: 5555 # Rogue trousers\n  examine: \"Black banded leather armour, a rogue's dream!\"\n- id: 5556 # Rogue gloves\n  examine: \"Black banded leather gloves, a rogue's dream!\"\n- id: 5557 # Rogue boots\n  examine: \"Black banded leather boots, a rogue's dream!\"\n- id: 5558 # Rogue kit\n  examine: \"It can do almost anything!\"\n- id: 5559 # Flash powder\n  examine: \"A small satchel of bright powder!\"\n- id: 5560 # Stethoscope\n  examine: \"A useful hearing aid.\"\n- id: 5561 # Mystic jewel\n  examine: \"I can escape the Maze with this!\"\n- id: 5568 # Tile\n  examine: \"For a mosaic.\"\n- id: 5569 # Tiles\n  examine: \"For a mosaic.\"\n- id: 5570 # Tiles\n  examine: \"For a mosaic.\"\n- id: 5571 # Tiles\n  examine: \"For a mosaic.\"\n- id: 5574 # Initiate sallet\n  examine: \"An initiate Temple Knight's helm.\"\n- id: 5575 # Initiate hauberk\n  examine: \"An initiate Temple Knight's Armour.\"\n- id: 5576 # Initiate cuisse\n  examine: \"An initiate Temple Knight's leg armour.\"\n- id: 5577 # Cupric sulfate\n  examine: \"A vial of something labelled 'Cupric Sulfate'.\"\n- id: 5578 # Acetic acid\n  examine: \"A vial of something labelled 'Acetic Acid'.\"\n- id: 5579 # Gypsum\n  examine: \"A vial of something labelled 'Gypsum'.\"\n- id: 5580 # Sodium chloride\n  examine: \"A vial of something labelled 'Sodium Chloride'.\"\n- id: 5581 # Nitrous oxide\n  examine: \"A vial of something labelled 'Nitrous Oxide'.\"\n- id: 5582 # Vial of liquid\n  examine: \"A vial of something labelled 'Dihydrogen Monoxide'.\"\n- id: 5583 # Tin ore powder\n  examine: \"A vial of something labelled 'Powdered Tin Ore'.\"\n- id: 5584 # Cupric ore powder\n  examine: \"A vial of something labelled 'Powdered Cupric Ore'.\"\n- id: 5585 # Bronze key\n  examine: \"I hope the mould was accurate enough....\"\n- id: 5586 # Metal spade\n  examine: \"It's a metal spade with a wooden handle.\"\n- id: 5587 # Metal spade\n  examine: \"It's a metal spade without a handle.\"\n- id: 5588 # Alchemical notes\n  examine: \"Looks like a pretty boring read.\"\n- id: 5589 # ??? mixture\n  examine: \"A very hot vial of something or other. The label says 'Cupric Sulfate'.\"\n- id: 5590 # ??? mixture\n  examine: \"A very warm vial of something or other. It's a bit lumpy.\"\n- id: 5591 # ??? mixture\n  examine: \"It looks horrible. I think I messed something up.\"\n- id: 5592 # Tin\n  examine: \"I could probably pour something into this.\"\n- id: 5593 # Tin\n  examine: \"It's full of a white lumpy mixture that seems to be hardening.\"\n- id: 5594 # Tin\n  examine: \"There is an impression of a key embedded in it.\"\n- id: 5595 # Tin\n  examine: \"There is an impression of a key, filled with tin ore.\"\n- id: 5596 # Tin\n  examine: \"There is an impression of a key, filled with copper ore.\"\n- id: 5597 # Tin\n  examine: \"There is an impression of a key, filled with tin and copper ore.\"\n- id: 5598 # Tin\n  examine: \"There is a bronze key surrounded by plaster in this tin.\"\n- id: 5599 # Tin\n  examine: \"There is a strange concoction filling this tin.\"\n- id: 5600 # Tin\n  examine: \"A tin layered with some stuff from a vial.\"\n- id: 5601 # Chisel\n  examine: \"Good for detailed crafting.\"\n- id: 5602 # Bronze wire\n  examine: \"Useful for crafting items.\"\n- id: 5603 # Shears\n  examine: \"For shearing sheep.\"\n- id: 5604 # Magnet\n  examine: \"A very attractive magnet.\"\n- id: 5605 # Knife\n  examine: \"A dangerous looking knife.\"\n- id: 5606 # Makeover voucher\n  examine: \"I can exchange this for one free makeover with the makeover mage.\"\n- id: 5607 # Grain\n  examine: \"A sack full of grain.\"\n- id: 5608 # Fox\n  examine: \"I don't think he likes being carried.\"\n- id: 5609 # Chicken\n  examine: \"He'd be easier to carry if I cooked and ate him first...\"\n- id: 5610 # Hourglass\n  examine: \"It's an hourglass.\"\n- id: 5615 # Bonemeal\n  examine: \"A pot of crushed Shaikahan bones.\"\n- id: 5616 # Bronze arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5617 # Iron arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5618 # Steel arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5619 # Mithril arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5620 # Adamant arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5621 # Rune arrow(p+)\n  examine: \"Venomous-looking arrows.\"\n- id: 5622 # Bronze arrow(p++)\n  examine: \"Venomous-looking arrows.\"\n- id: 5623 # Iron arrow(p++)\n  examine: \"Venomous-looking arrows.\"\n- id: 5624 # Steel arrow(p++)\n  examine: \"Venomous-looking arrows.\"\n- id: 5625 # Mithril arrow(p++)\n  examine: \"Venomous-looking arrows.\"\n- id: 5627 # Rune arrow(p++)\n  examine: \"Venomous-looking arrows.\"\n- id: 5628 # Bronze dart(p+)\n  examine: \"A deadly poisoned dart with a bronze tip.\"\n- id: 5629 # Iron dart(p+)\n  examine: \"A deadly poisoned dart with an iron tip.\"\n- id: 5630 # Steel dart(p+)\n  examine: \"A deadly poisoned dart with a steel tip.\"\n- id: 5631 # Black dart(p+)\n  examine: \"A deadly poisoned dart with a black tip.\"\n- id: 5632 # Mithril dart(p+)\n  examine: \"A deadly poisoned dart with a mithril tip.\"\n- id: 5633 # Adamant dart(p+)\n  examine: \"A deadly poisoned dart with an adamant tip.\"\n- id: 5634 # Rune dart(p+)\n  examine: \"A deadly poisoned dart with a rune tip.\"\n- id: 5635 # Bronze dart(p++)\n  examine: \"A deadly poisoned dart with a bronze tip.\"\n- id: 5636 # Iron dart(p++)\n  examine: \"A deadly poisoned dart with an iron tip.\"\n- id: 5637 # Steel dart(p++)\n  examine: \"A deadly poisoned dart with a steel tip.\"\n- id: 5638 # Black dart(p++)\n  examine: \"A deadly poisoned dart with a black tip.\"\n- id: 5639 # Mithril dart(p++)\n  examine: \"A deadly poisoned dart with a mithril tip.\"\n- id: 5640 # Adamant dart(p++)\n  examine: \"A deadly poisoned dart with an adamant tip.\"\n- id: 5641 # Rune dart(p++)\n  examine: \"A deadly poisoned dart with a rune tip.\"\n- id: 5642 # Bronze javelin(p+)\n  examine: \"A bronze tipped javelin.\"\n- id: 5643 # Iron javelin(p+)\n  examine: \"An iron tipped javelin.\"\n- id: 5644 # Steel javelin(p+)\n  examine: \"A steel tipped javelin.\"\n- id: 5645 # Mithril javelin(p+)\n  examine: \"A mithril tipped javelin.\"\n- id: 5647 # Rune javelin(p+)\n  examine: \"A rune tipped javelin.\"\n- id: 5649 # Iron javelin(p++)\n  examine: \"An iron tipped javelin.\"\n- id: 5650 # Steel javelin(p++)\n  examine: \"A steel tipped javelin.\"\n- id: 5651 # Mithril javelin(p++)\n  examine: \"A mithril tipped javelin.\"\n- id: 5653 # Rune javelin(p++)\n  examine: \"A rune tipped javelin.\"\n- id: 5654 # Bronze knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5655 # Iron knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5656 # Steel knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5657 # Mithril knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5658 # Black knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5659 # Adamant knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5660 # Rune knife(p+)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5661 # Bronze knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5662 # Iron knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5663 # Steel knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5664 # Mithril knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5665 # Black knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5667 # Rune knife(p++)\n  examine: \"A finely balanced throwing knife.\"\n- id: 5668 # Iron dagger(p+)\n  examine: \"The blade is covered with poison.\"\n- id: 5670 # Bronze dagger(p+)\n  examine: \"This dagger is poisoned.\"\n- id: 5672 # Steel dagger(p+)\n  examine: \"The blade has been poisoned.\"\n- id: 5674 # Mithril dagger(p+)\n  examine: \"A poisoned Mithril dagger.\"\n- id: 5678 # Rune dagger(p+)\n  examine: \"The blade is covered with a nasty poison.\"\n- id: 5680 # Dragon dagger(p+)\n  examine: \"A powerful dagger.\"\n- id: 5682 # Black dagger(p+)\n  examine: \"This dagger is poisoned.\"\n- id: 5686 # Iron dagger(p++)\n  examine: \"The blade is covered with poison.\"\n- id: 5690 # Steel dagger(p++)\n  examine: \"The blade has been poisoned.\"\n- id: 5692 # Mithril dagger(p++)\n  examine: \"A poisoned Mithril dagger.\"\n- id: 5696 # Rune dagger(p++)\n  examine: \"The blade is covered with a nasty poison.\"\n- id: 5700 # Black dagger(p++)\n  examine: \"This dagger is poisoned.\"\n- id: 5704 # Bronze spear(p+)\n  examine: \"A poisoned bronze tipped spear.\"\n- id: 5706 # Iron spear(p+)\n  examine: \"A poisoned iron tipped spear.\"\n- id: 5708 # Steel spear(p+)\n  examine: \"A poisoned steel tipped spear.\"\n- id: 5710 # Mithril spear(p+)\n  examine: \"A poisoned mithril tipped spear.\"\n- id: 5712 # Adamant spear(p+)\n  examine: \"A poisoned adamantite tipped spear.\"\n- id: 5714 # Rune spear(p+)\n  examine: \"A poisoned rune tipped spear.\"\n- id: 5716 # Dragon spear(p+)\n  examine: \"A poisoned dragon tipped spear.\"\n- id: 5718 # Bronze spear(p++)\n  examine: \"A poisoned bronze tipped spear.\"\n- id: 5720 # Iron spear(p++)\n  examine: \"A poisoned iron tipped spear.\"\n- id: 5722 # Steel spear(p++)\n  examine: \"A poisoned steel tipped spear.\"\n- id: 5724 # Mithril spear(p++)\n  examine: \"A poisoned mithril tipped spear.\"\n- id: 5728 # Rune spear(p++)\n  examine: \"A poisoned rune tipped spear.\"\n- id: 5730 # Dragon spear(p++)\n  examine: \"A poisoned dragon tipped spear.\"\n- id: 5734 # Black spear(p+)\n  examine: \"A poisoned black tipped spear.\"\n- id: 5736 # Black spear(p++)\n  examine: \"A poisoned black tipped spear.\"\n- id: 5738 # Woad leaf\n  examine: \"A slightly bluish leaf.\"\n- id: 5739 # Asgarnian ale(m)\n  examine: \"This looks a good deal stronger than normal Asgarnian Ale.\"\n- id: 5741 # Mature wmb\n  examine: \"This looks a good deal stronger than normal Wizards Mind Bomb.\"\n- id: 5743 # Greenman's ale(m)\n  examine: \"This looks a good deal stronger than normal Greenman's Ale.\"\n- id: 5745 # Dragon bitter(m)\n  examine: \"This looks a good deal stronger than normal Dragon Bitter.\"\n- id: 5747 # Dwarven stout(m)\n  examine: \"This looks a good deal stronger than normal Dwarven Stout.\"\n- id: 5749 # Moonlight mead(m)\n  examine: \"This looks a good deal stronger than normal Moonlight Mead.\"\n- id: 5751 # Axeman's folly\n  examine: \"This might help me chop harder.\"\n- id: 5753 # Axeman's folly(m)\n  examine: \"This looks a good deal stronger than normal Axeman's Folly.\"\n- id: 5755 # Chef's delight\n  examine: \"A fruity, full-bodied ale.\"\n- id: 5757 # Chef's delight(m)\n  examine: \"This looks a good deal stronger than normal Chef's Delight.\"\n- id: 5759 # Slayer's respite\n  examine: \"Ale with bite.\"\n- id: 5761 # Slayer's respite(m)\n  examine: \"This looks a good deal stronger than normal Slayer's Respite.\"\n- id: 5763 # Cider\n  examine: \"A glass of cider.\"\n- id: 5765 # Mature cider\n  examine: \"This looks a good deal stronger than normal cider.\"\n- id: 5767 # Ale yeast\n  examine: \"A pot filled with ale yeast.\"\n- id: 5769 # Calquat keg\n  examine: \"Sliced and hollowed out to form a keg.\"\n- id: 5771 # Dwarven stout(1)\n  examine: \"This keg contains 1 pint of Dwarven Stout.\"\n- id: 5773 # Dwarven stout(2)\n  examine: \"This keg contains 2 pints of Dwarven Stout.\"\n- id: 5775 # Dwarven stout(3)\n  examine: \"This keg contains 3 pints of Dwarven Stout.\"\n- id: 5777 # Dwarven stout(4)\n  examine: \"This keg contains 4 pints of Dwarven Stout.\"\n- id: 5779 # Asgarnian ale(1)\n  examine: \"This keg contains 1 pint of Asgarnian Ale.\"\n- id: 5781 # Asgarnian ale(2)\n  examine: \"This keg contains 2 pints of Asgarnian Ale.\"\n- id: 5783 # Asgarnian ale(3)\n  examine: \"This keg contains 3 pints of Asgarnian Ale.\"\n- id: 5785 # Asgarnian ale(4)\n  examine: \"This keg contains 4 pints of Asgarnian Ale.\"\n- id: 5787 # Greenmans ale(1)\n  examine: \"This keg contains 1 pint of Greenmans Ale.\"\n- id: 5789 # Greenmans ale(2)\n  examine: \"This keg contains 2 pints of Greenmans Ale.\"\n- id: 5791 # Greenmans ale(3)\n  examine: \"This keg contains 3 pints of Greenmans Ale.\"\n- id: 5793 # Greenmans ale(4)\n  examine: \"This keg contains 4 pints of Greenmans Ale.\"\n- id: 5795 # Mind bomb(1)\n  examine: \"This keg contains 1 pint of Wizards Mind Bomb.\"\n- id: 5797 # Mind bomb(2)\n  examine: \"This keg contains 2 pints of Wizards Mind Bomb.\"\n- id: 5799 # Mind bomb(3)\n  examine: \"This keg contains 3 pints of Wizards Mind Bomb.\"\n- id: 5801 # Mind bomb(4)\n  examine: \"This keg contains 4 pints of Wizards Mind Bomb.\"\n- id: 5803 # Dragon bitter(1)\n  examine: \"This keg contains 1 pint of Dragon Bitter.\"\n- id: 5805 # Dragon bitter(2)\n  examine: \"This keg contains 2 pints of Dragon Bitter.\"\n- id: 5807 # Dragon bitter(3)\n  examine: \"This keg contains 3 pints of Dragon Bitter.\"\n- id: 5809 # Dragon bitter(4)\n  examine: \"This keg contains 4 pints of Dragon Bitter.\"\n- id: 5811 # Moonlight mead(1)\n  examine: \"This keg contains 1 pint of Moonlight Mead.\"\n- id: 5813 # Moonlight mead(2)\n  examine: \"This keg contains 2 pints of Moonlight Mead.\"\n- id: 5815 # Moonlight mead(3)\n  examine: \"This keg contains 3 pints of Moonlight Mead.\"\n- id: 5817 # Moonlight mead(4)\n  examine: \"This keg contains 4 pints of Moonlight Mead.\"\n- id: 5819 # Axeman's folly(1)\n  examine: \"This keg contains 1 pint of Axeman's Folly.\"\n- id: 5821 # Axeman's folly(2)\n  examine: \"This keg contains 2 pints of Axeman's Folly.\"\n- id: 5823 # Axeman's folly(3)\n  examine: \"This keg contains 3 pints of Axeman's Folly.\"\n- id: 5825 # Axeman's folly(4)\n  examine: \"This keg contains 4 pints of Axeman's Folly.\"\n- id: 5827 # Chef's delight(1)\n  examine: \"This keg contains 1 pint of Chef's Delight.\"\n- id: 5829 # Chef's delight(2)\n  examine: \"This keg contains 2 pints of Chef's Delight.\"\n- id: 5831 # Chef's delight(3)\n  examine: \"This keg contains 3 pints of Chef's Delight.\"\n- id: 5833 # Chef's delight(4)\n  examine: \"This keg contains 4 pints of Chef's Delight.\"\n- id: 5835 # Slayer's respite(1)\n  examine: \"This keg contains 1 pint of Slayer's Respite.\"\n- id: 5837 # Slayer's respite(2)\n  examine: \"This keg contains 2 pints of Slayer's Respite.\"\n- id: 5839 # Slayer's respite(3)\n  examine: \"This keg contains 3 pints of Slayer's Respite.\"\n- id: 5841 # Slayer's respite(4)\n  examine: \"This keg contains 4 pints of Slayer's Respite.\"\n- id: 5843 # Cider(1)\n  examine: \"This keg contains 1 pint of Cider.\"\n- id: 5845 # Cider(2)\n  examine: \"This keg contains 2 pints of Cider.\"\n- id: 5847 # Cider(3)\n  examine: \"This keg contains 3 pints of Cider.\"\n- id: 5849 # Cider(4)\n  examine: \"This keg contains 4 pints of Cider.\"\n- id: 5851 # Dwarven stout(m1)\n  examine: \"This keg contains 1 pint of mature Dwarven Stout.\"\n- id: 5853 # Dwarven stout(m2)\n  examine: \"This keg contains 2 pints of mature Dwarven Stout.\"\n- id: 5855 # Dwarven stout(m3)\n  examine: \"This keg contains 3 pints of mature Dwarven Stout.\"\n- id: 5857 # Dwarven stout(m4)\n  examine: \"This keg contains 4 pints of mature Dwarven Stout.\"\n- id: 5859 # Asgarnian ale(m1)\n  examine: \"This keg contains 1 pint of mature Asgarnian Ale.\"\n- id: 5861 # Asgarnian ale(m2)\n  examine: \"This keg contains 2 pints of mature Asgarnian Ale.\"\n- id: 5863 # Asgarnian ale(m3)\n  examine: \"This keg contains 3 pints of mature Asgarnian Ale.\"\n- id: 5865 # Asgarnian ale(m4)\n  examine: \"This keg contains 4 pints of mature Asgarnian Ale.\"\n- id: 5867 # Greenmans ale(m1)\n  examine: \"This keg contains 1 pint of mature Greenmans Ale.\"\n- id: 5869 # Greenmans ale(m2)\n  examine: \"This keg contains 2 pints of mature Greenmans Ale.\"\n- id: 5871 # Greenmans ale(m3)\n  examine: \"This keg contains 3 pints of mature Greenmans Ale.\"\n- id: 5873 # Greenmans ale(m4)\n  examine: \"This keg contains 4 pints of mature Greenmans Ale.\"\n- id: 5875 # Mind bomb(m1)\n  examine: \"This keg contains 1 pint of mature Wizards Mind Bomb.\"\n- id: 5877 # Mind bomb(m2)\n  examine: \"This keg contains 2 pints of mature Wizards Mind Bomb.\"\n- id: 5879 # Mind bomb(m3)\n  examine: \"This keg contains 3 pints of mature Wizards Mind Bomb.\"\n- id: 5881 # Mind bomb(m4)\n  examine: \"This keg contains 4 pints of mature Wizards Mind Bomb.\"\n- id: 5883 # Dragon bitter(m1)\n  examine: \"This keg contains 1 pint of mature Dragon Bitter.\"\n- id: 5885 # Dragon bitter(m2)\n  examine: \"This keg contains 2 pints of mature Dragon Bitter.\"\n- id: 5887 # Dragon bitter(m3)\n  examine: \"This keg contains 3 pints of mature Dragon Bitter.\"\n- id: 5889 # Dragon bitter(m4)\n  examine: \"This keg contains 4 pints of mature Dragon Bitter.\"\n- id: 5899 # Axeman's folly(m1)\n  examine: \"This keg contains 1 pint of mature Axeman's Folly.\"\n- id: 5901 # Axeman's folly(m2)\n  examine: \"This keg contains 2 pints of mature Axeman's Folly.\"\n- id: 5903 # Axeman's folly(m3)\n  examine: \"This keg contains 3 pints of mature Axeman's Folly.\"\n- id: 5905 # Axeman's folly(m4)\n  examine: \"This keg contains 4 pints of mature Axeman's Folly.\"\n- id: 5907 # Chef's delight(m1)\n  examine: \"This keg contains 1 pint of mature Chef's Delight.\"\n- id: 5909 # Chef's delight(m2)\n  examine: \"This keg contains 2 pints of mature Chef's Delight.\"\n- id: 5911 # Chef's delight(m3)\n  examine: \"This keg contains 3 pints of mature Chef's Delight.\"\n- id: 5913 # Chef's delight(m4)\n  examine: \"This keg contains 4 pints of mature Chef's Delight.\"\n- id: 5923 # Cider(m1)\n  examine: \"This keg contains 1 pint of mature Cider.\"\n- id: 5925 # Cider(m2)\n  examine: \"This keg contains 2 pints of mature Cider.\"\n- id: 5927 # Cider(m3)\n  examine: \"This keg contains 3 pints of mature Cider.\"\n- id: 5929 # Cider(m4)\n  examine: \"This keg contains 4 pints of mature Cider.\"\n- id: 5931 # Jute fibre\n  examine: \"I can weave this to make sacks.\"\n- id: 5933 # Willow branch\n  examine: \"A branch from a willow tree.\"\n- id: 5935 # Coconut milk\n  examine: \"A vial filled with coconut milk\"\n- id: 5937 # Weapon poison(+)\n  examine: \"A vial of extra strong weapon poison, for spears and daggers.\"\n- id: 5940 # Weapon poison(++)\n  examine: \"A vial of super strong weapon poison, for spears and daggers.\"\n- id: 5943 # Antidote+(4)\n  examine: \"4 doses of extra strong antipoison potion.\"\n- id: 5945 # Antidote+(3)\n  examine: \"3 doses of extra strong antipoison potion.\"\n- id: 5947 # Antidote+(2)\n  examine: \"2 doses of extra strong antipoison potion.\"\n- id: 5949 # Antidote+(1)\n  examine: \"1 dose of extra strong antipoison potion.\"\n- id: 5952 # Antidote++(4)\n  examine: \"4 doses of super strong antipoison potion.\"\n- id: 5954 # Antidote++(3)\n  examine: \"3 doses of super strong antipoison potion.\"\n- id: 5956 # Antidote++(2)\n  examine: \"2 doses of super strong antipoison potion.\"\n- id: 5958 # Antidote++(1)\n  examine: \"1 dose of super strong antipoison potion.\"\n- id: 5960 # Tomatoes(1)\n  examine: \"A fruit basket filled with tomatoes.\"\n- id: 5962 # Tomatoes(2)\n  examine: \"A fruit basket filled with tomatoes.\"\n- id: 5964 # Tomatoes(3)\n  examine: \"A fruit basket filled with tomatoes.\"\n- id: 5966 # Tomatoes(4)\n  examine: \"A fruit basket filled with tomatoes.\"\n- id: 5968 # Tomatoes(5)\n  examine: \"A fruit basket filled with tomatoes.\"\n- id: 5970 # Curry leaf\n  examine: \"I could make a spicy curry with this.\"\n- id: 5972 # Papaya fruit\n  examine: \"Looks delicious.\"\n- id: 5974 # Coconut\n  examine: \"It's a coconut.\"\n- id: 5978 # Coconut shell\n  examine: \"All the milk has been removed.\"\n- id: 5980 # Calquat fruit\n  examine: \"This is the largest fruit I've ever seen.\"\n- id: 5982 # Watermelon\n  examine: \"A juicy watermelon.\"\n- id: 5984 # Watermelon slice\n  examine: \"A slice of watermelon.\"\n- id: 5986 # Sweetcorn\n  examine: \"Raw sweetcorn.\"\n- id: 5988 # Cooked sweetcorn\n  examine: \"Delicious cooked sweetcorn.\"\n- id: 5990 # Burnt sweetcorn\n  examine: \"This sweetcorn has been cooked for too long.\"\n- id: 5992 # Apple mush\n  examine: \"A bucket of apple mush.\"\n- id: 5994 # Hammerstone hops\n  examine: \"A handful of Hammerstone Hops.\"\n- id: 5996 # Asgarnian hops\n  examine: \"A handful of Asgarnian Hops.\"\n- id: 5998 # Yanillian hops\n  examine: \"A handful of Yanillian Hops.\"\n- id: 6000 # Krandorian hops\n  examine: \"A handful of Krandorian Hops.\"\n- id: 6002 # Wildblood hops\n  examine: \"A handful of Wildblood Hops.\"\n- id: 6004 # Mushroom\n  examine: \"A Bittercap Mushroom\"\n- id: 6006 # Barley\n  examine: \"A handful of Barley.\"\n- id: 6008 # Barley malt\n  examine: \"A handful of barley malt.\"\n- id: 6010 # Marigolds\n  examine: \"A bunch of marigolds.\"\n- id: 6012 # Nasturtiums\n  examine: \"A bunch of nasturtiums.\"\n- id: 6014 # Rosemary\n  examine: \"Some rosemary.\"\n- id: 6016 # Cactus spine\n  examine: \"Don't prick yourself with this.\"\n- id: 6020 # Leaves\n  examine: \"A pile of leaves.\"\n- id: 6022 # Leaves\n  examine: \"A pile of Oak tree leaves.\"\n- id: 6024 # Leaves\n  examine: \"A pile of Willow tree leaves.\"\n- id: 6026 # Leaves\n  examine: \"A pile of Yew tree leaves.\"\n- id: 6028 # Leaves\n  examine: \"A pile of Maple tree leaves.\"\n- id: 6030 # Leaves\n  examine: \"A pile of Magic tree leaves.\"\n- id: 6032 # Compost\n  examine: \"Good for plants, helps them grow.\"\n- id: 6036 # Plant cure\n  examine: \"Use this on plants to cure disease.\"\n- id: 6038 # Magic string\n  examine: \"I could use this to make jewellery.\"\n- id: 6040 # Amulet of nature\n  examine: \"An Amulet of Nature.\"\n- id: 6043 # Oak roots\n  examine: \"The roots of the Oak tree.\"\n- id: 6045 # Willow roots\n  examine: \"The roots of the Willow tree.\"\n- id: 6047 # Maple roots\n  examine: \"The roots of the Maple tree.\"\n- id: 6049 # Yew roots\n  examine: \"The roots of the Yew tree.\"\n- id: 6051 # Magic roots\n  examine: \"The roots of the Magic tree.\"\n- id: 6055 # Weeds\n  examine: \"A handful of weeds.\"\n- id: 6057 # Hay sack\n  examine: \"A sack filled with hay.\"\n- id: 6058 # Hay sack\n  examine: \"This sack of hay has a bronze spear sticking through it.\"\n- id: 6059 # Scarecrow\n  examine: \"This should scare the birds.\"\n- id: 6064 # Bloody mourner top\n  examine: \"How do I wash blood stains out?\"\n- id: 6065 # Mourner top\n  examine: \"A thick heavy leather top.\"\n- id: 6067 # Mourner trousers\n  examine: \"A pair of mourner trousers.\"\n- id: 6068 # Mourner gloves\n  examine: \"These will keep my hands warm!\"\n- id: 6069 # Mourner boots\n  examine: \"Comfortable leather boots.\"\n- id: 6070 # Mourner cloak\n  examine: \"A dull brown cape.\"\n- id: 6071 # Mourner letter\n  examine: \"A letter of recommendation.\"\n- id: 6072 # Tegid's soap\n  examine: \"A bar of soap taken from Tegid.\"\n- id: 6073 # Prifddinas' history\n  examine: \"A book on the history of Prifddinas.\"\n- id: 6075 # Eastern discovery\n  examine: \"A book on the exploration of the eastern realm.\"\n- id: 6077 # Eastern settlement\n  examine: \"A book on the settlement of the eastern realm.\"\n- id: 6079 # The great divide\n  examine: \"A book about the great divide.\"\n- id: 6081 # Broken device\n  examine: \"A strange broken device of gnomic design.\"\n- id: 6082 # Fixed device\n  examine: \"A device for firing dye.\"\n- id: 6083 # Tarnished key\n  examine: \"A key that Essyllt gave to me.\"\n- id: 6085 # Red dye bellows\n  examine: \"A large pair of ogre bellows filled with red dye.\"\n- id: 6086 # Blue dye bellows\n  examine: \"A large pair of ogre bellows filled with blue dye.\"\n- id: 6087 # Yellow dye bellows\n  examine: \"A large pair of ogre bellows filled with yellow dye.\"\n- id: 6088 # Green dye bellows\n  examine: \"A large pair of ogre bellows filled with green dye.\"\n- id: 6089 # Blue toad\n  examine: \"A blue dye filled toad.\"\n- id: 6090 # Red toad\n  examine: \"A red dye filled toad.\"\n- id: 6091 # Yellow toad\n  examine: \"A yellow dye filled toad.\"\n- id: 6092 # Green toad\n  examine: \"A green dye filled toad.\"\n- id: 6093 # Rotten apples\n  examine: \"A barrel full of rotten apples.\"\n- id: 6094 # Apple barrel\n  examine: \"A barrel full of mushed apples.\"\n- id: 6095 # Naphtha apple mix\n  examine: \"A barrel full of rotten apples and naphtha.\"\n- id: 6096 # Toxic naphtha\n  examine: \"A barrel full of toxic naphtha.\"\n- id: 6097 # Sieve\n  examine: \"It's a sieve.\"\n- id: 6098 # Toxic powder\n  examine: \"A pile of toxic powder.\"\n- id: 6099 # Teleport crystal (4)\n  examine: \"An enchanted teleportation crystal.\"\n- id: 6100 # Teleport crystal (3)\n  examine: \"An enchanted teleportation crystal.\"\n- id: 6101 # Teleport crystal (2)\n  examine: \"An enchanted teleportation crystal.\"\n- id: 6102 # Teleport crystal (1)\n  examine: \"An enchanted teleportation crystal.\"\n- id: 6104 # New key\n  examine: \"A newly cut key that Essyllt gave to me.\"\n- id: 6106 # Ghostly boots\n  examine: \"They seem to be not quite of this world...\"\n- id: 6107 # Ghostly robe\n  examine: \"A particularly spooky robe top.\"\n- id: 6108 # Ghostly robe\n  examine: \"An unearthly set of robe bottoms.\"\n- id: 6109 # Ghostly hood\n  examine: \"A ghostly hood, fit for a ghostly head.\"\n- id: 6110 # Ghostly gloves\n  examine: \"They seem to fade in and out of existence...\"\n- id: 6111 # Ghostly cloak\n  examine: \"Made of a strange ghostly material...\"\n- id: 6112 # Kelda seed\n  examine: \"Kelda hop seeds can only be grown underground!\"\n- id: 6113 # Kelda hops\n  examine: \"A handful of Kelda Hops.\"\n- id: 6118 # Kelda stout\n  examine: \"A pint of bluish beer.\"\n- id: 6119 # Square stone\n  examine: \"There is a strange yellow marking on this stone.\"\n- id: 6120 # Square stone\n  examine: \"There is a strange green marking on this stone.\"\n- id: 6121 # Letter\n  examine: \"A letter addressed to Elstan of Falador.\"\n- id: 6122 # A chair\n  examine: \"For sitting on.\"\n- id: 6123 # Beer glass\n  examine: \"For drinking... if it were filled.\"\n- id: 6125 # Enchanted lyre(2)\n  examine: \"This will teleport me to the Fremennik province when I play it.\"\n- id: 6126 # Enchanted lyre(3)\n  examine: \"This will teleport me to the Fremennik province when I play it.\"\n- id: 6127 # Enchanted lyre(4)\n  examine: \"This will teleport me to the Fremennik province when I play it.\"\n- id: 6128 # Rock-shell helm\n  examine: \"Protective headwear made from crabs. Better than that sounds.\"\n- id: 6129 # Rock-shell plate\n  examine: \"A sturdy body armour made from rock crab pieces.\"\n- id: 6130 # Rock-shell legs\n  examine: \"Some tough leggings made from rock crab parts.\"\n- id: 6131 # Spined helm\n  examine: \"A helm fit for any Fremennik ranger.\"\n- id: 6133 # Spined body\n  examine: \"A constant reminder that I'm above a Dagannoth in the food chain.\"\n- id: 6135 # Spined chaps\n  examine: \"Stylish leg armour for rangers with a lingering smell of raw fish...\"\n- id: 6137 # Skeletal helm\n  examine: \"Make your foes cower by wearing a skull as a helmet!\"\n- id: 6139 # Skeletal top\n  examine: \"The bones in this armour seem to vibrate with a magical quality...\"\n- id: 6141 # Skeletal bottoms\n  examine: \"A superior set of strengthened slacks for any self respecting seer.\"\n- id: 6143 # Spined boots\n  examine: \"Some finely crafted Fremennik boots, made from spined dagannoth hide.\"\n- id: 6145 # Rock-shell boots\n  examine: \"Some Fremennik boots, made from the shards of a rock crab's shell.\"\n- id: 6147 # Skeletal boots\n  examine: \"Some finely crafted Fremennik boots, made from the bones of a wallasalki.\"\n- id: 6149 # Spined gloves\n  examine: \"Fremennik gloves stitched together from spined dagannoth hide.\"\n- id: 6151 # Rock-shell gloves\n  examine: \"Fremennik gloves stitched together from rock crab shell shards.\"\n- id: 6153 # Skeletal gloves\n  examine: \"Fremennik gloves stitched together from wallasalki bones fragments.\"\n- id: 6155 # Dagannoth hide\n  examine: \"A sturdy piece of dagannoth hide.\"\n- id: 6157 # Rock-shell chunk\n  examine: \"A spherical chunk of rock-shell.\"\n- id: 6159 # Rock-shell shard\n  examine: \"A curved piece of rock-shell.\"\n- id: 6161 # Rock-shell splinter\n  examine: \"A slim piece of rock-shell.\"\n- id: 6163 # Skull piece\n  examine: \"A fearsome looking skull.\"\n- id: 6165 # Ribcage piece\n  examine: \"A slightly damaged ribcage.\"\n- id: 6167 # Fibula piece\n  examine: \"An interesting looking bone shard.\"\n- id: 6169 # Circular hide\n  examine: \"A toughened chunk of dagannoth hide.\"\n- id: 6171 # Flattened hide\n  examine: \"A tattered chunk of dagannoth hide.\"\n- id: 6173 # Stretched hide\n  examine: \"A weathered chunk of dagannoth hide.\"\n- id: 6178 # Raw pheasant\n  examine: \"I need to cook this first.\"\n- id: 6179 # Raw pheasant\n  examine: \"I need to cook this first.\"\n- id: 6180 # Lederhosen top\n  examine: \"A leather strapped top.\"\n- id: 6181 # Lederhosen shorts\n  examine: \"Brown leather shorts with bright white socks?\"\n- id: 6182 # Lederhosen hat\n  examine: \"A hat with a goat's hair attached.\"\n- id: 6183 # Frog token\n  examine: \"I can use this at the Varrock clothes shop.\"\n- id: 6184 # Prince tunic\n  examine: \"Very posh!\"\n- id: 6185 # Prince leggings\n  examine: \"Very posh!\"\n- id: 6186 # Princess blouse\n  examine: \"Very posh!\"\n- id: 6187 # Princess skirt\n  examine: \"Very posh!\"\n- id: 6188 # Frog mask\n  examine: \"Now that's just silly.\"\n- id: 6199 # Mystery box\n  examine: \"Oooh, I wonder what could be inside?\"\n- id: 6200 # Raw fishlike thing\n  examine: \"A raw... fish? Is this a fish??\"\n- id: 6202 # Fishlike thing\n  examine: \"It's a fishlike thing that appears to already be cooked. It looks disgusting.\"\n- id: 6204 # Raw fishlike thing\n  examine: \"A raw... fish? Is this a fish??\"\n- id: 6206 # Fishlike thing\n  examine: \"It's a fishlike thing that appears to already be cooked. It looks disgusting.\"\n- id: 6209 # Small fishing net\n  examine: \"Useful for catching small fish.\"\n- id: 6211 # Teak pyre logs\n  examine: \"Teak logs prepared with sacred oil for a funeral pyre.\"\n- id: 6213 # Mahogany pyre log\n  examine: \"Mahogany logs prepared with sacred oil for a funeral pyre.\"\n- id: 6215 # Broodoo shield (10)\n  examine: \"A scary broodoo shield.\"\n- id: 6217 # Broodoo shield (9)\n  examine: \"A scary broodoo shield.\"\n- id: 6219 # Broodoo shield (8)\n  examine: \"A scary broodoo shield.\"\n- id: 6221 # Broodoo shield (7)\n  examine: \"A scary broodoo shield.\"\n- id: 6223 # Broodoo shield (6)\n  examine: \"A scary broodoo shield.\"\n- id: 6225 # Broodoo shield (5)\n  examine: \"A scary broodoo shield.\"\n- id: 6227 # Broodoo shield (4)\n  examine: \"A scary broodoo shield.\"\n- id: 6229 # Broodoo shield (3)\n  examine: \"A scary broodoo shield.\"\n- id: 6231 # Broodoo shield (2)\n  examine: \"A scary broodoo shield.\"\n- id: 6233 # Broodoo shield (1)\n  examine: \"A scary broodoo shield.\"\n- id: 6235 # Broodoo shield\n  examine: \"A scary broodoo shield.\"\n- id: 6237 # Broodoo shield (10)\n  examine: \"A scary broodoo shield.\"\n- id: 6239 # Broodoo shield (9)\n  examine: \"A scary broodoo shield.\"\n- id: 6241 # Broodoo shield (8)\n  examine: \"A scary broodoo shield.\"\n- id: 6243 # Broodoo shield (7)\n  examine: \"A scary broodoo shield.\"\n- id: 6245 # Broodoo shield (6)\n  examine: \"A scary broodoo shield.\"\n- id: 6247 # Broodoo shield (5)\n  examine: \"A scary broodoo shield.\"\n- id: 6249 # Broodoo shield (4)\n  examine: \"A scary broodoo shield.\"\n- id: 6251 # Broodoo shield (3)\n  examine: \"A scary broodoo shield.\"\n- id: 6253 # Broodoo shield (2)\n  examine: \"A scary broodoo shield.\"\n- id: 6255 # Broodoo shield (1)\n  examine: \"A scary broodoo shield.\"\n- id: 6257 # Broodoo shield\n  examine: \"A scary broodoo shield.\"\n- id: 6259 # Broodoo shield (10)\n  examine: \"A scary broodoo shield.\"\n- id: 6261 # Broodoo shield (9)\n  examine: \"A scary broodoo shield.\"\n- id: 6263 # Broodoo shield (8)\n  examine: \"A scary broodoo shield.\"\n- id: 6265 # Broodoo shield (7)\n  examine: \"A scary broodoo shield.\"\n- id: 6267 # Broodoo shield (6)\n  examine: \"A scary broodoo shield.\"\n- id: 6269 # Broodoo shield (5)\n  examine: \"A scary broodoo shield.\"\n- id: 6271 # Broodoo shield (4)\n  examine: \"A scary broodoo shield.\"\n- id: 6273 # Broodoo shield (3)\n  examine: \"A scary broodoo shield.\"\n- id: 6275 # Broodoo shield (2)\n  examine: \"A scary broodoo shield.\"\n- id: 6277 # Broodoo shield (1)\n  examine: \"A scary broodoo shield.\"\n- id: 6279 # Broodoo shield\n  examine: \"A scary broodoo shield.\"\n- id: 6281 # Thatch spar light\n  examine: \"A wooden pole for use in primitive construction.\"\n- id: 6283 # Thatch spar med\n  examine: \"A wooden pole for use in primitive construction.\"\n- id: 6285 # Thatch spar dense\n  examine: \"A wooden pole for use in primitive construction.\"\n- id: 6287 # Snake hide\n  examine: \"Scaly but not slimy! It could be a useful material if it were tanned.\"\n- id: 6289 # Snakeskin\n  examine: \"Nicely tanned skin from a snake.\"\n- id: 6291 # Spider carcass\n  examine: \"Its creeping days are over!\"\n- id: 6293 # Spider on stick\n  examine: \"A raw spider threaded onto a skewer stick.\"\n- id: 6295 # Spider on shaft\n  examine: \"A raw spider threaded onto an arrow shaft.\"\n- id: 6297 # Spider on stick\n  examine: \"A nicely roasted spider threaded onto a skewer stick.\"\n- id: 6299 # Spider on shaft\n  examine: \"A nicely roasted spider threaded onto an arrow shaft.\"\n- id: 6301 # Burnt spider\n  examine: \"A badly burnt spider threaded onto a charred skewer stick.\"\n- id: 6303 # Spider on shaft\n  examine: \"A badly burnt spider threaded onto a charred arrow shaft.\"\n- id: 6305 # Skewer stick\n  examine: \"A sharp pointed stick, quite resistant to fire.\"\n- id: 6306 # Trading sticks\n  examine: \"Karamja currency.\"\n- id: 6311 # Gout tuber\n  examine: \"Plant this in a herb patch to grow Goutweed.\"\n- id: 6313 # Opal machete\n  examine: \"A jungle specific slashing device.\"\n- id: 6315 # Jade machete\n  examine: \"A jungle specific slashing device.\"\n- id: 6317 # Red topaz machete\n  examine: \"A jungle specific slashing device.\"\n- id: 6319 # Proboscis\n  examine: \"A giant mosquito's proboscis, aerodynamic and sharp!\"\n- id: 6322 # Snakeskin body\n  examine: \"Made from 100% real snakeskin.\"\n- id: 6324 # Snakeskin chaps\n  examine: \"Made from 100% real snake.\"\n- id: 6326 # Snakeskin bandana\n  examine: \"Lightweight head protection.\"\n- id: 6328 # Snakeskin boots\n  examine: \"Made from snakes.\"\n- id: 6332 # Mahogany logs\n  examine: \"Some well-cut mahogany logs.\"\n- id: 6333 # Teak logs\n  examine: \"Some well-cut teak logs.\"\n- id: 6335 # Tribal mask\n  examine: \"A ceremonial wooden mask.\"\n- id: 6337 # Tribal mask\n  examine: \"A ceremonial wooden mask.\"\n- id: 6339 # Tribal mask\n  examine: \"A ceremonial wooden mask.\"\n- id: 6341 # Tribal top\n  examine: \"Local dress.\"\n- id: 6343 # Villager robe\n  examine: \"A brightly coloured robe prized by the Tai Bwo Wannai peoples.\"\n- id: 6345 # Villager hat\n  examine: \"A brightly coloured hat prized by the Tai Bwo Wannai peoples.\"\n- id: 6347 # Villager armband\n  examine: \"A brown armband, as worn by the Tai Bwo Wannai locals.\"\n- id: 6349 # Villager sandals\n  examine: \"A brightly coloured pair of local sandals.\"\n- id: 6351 # Tribal top\n  examine: \"Local dress.\"\n- id: 6353 # Villager robe\n  examine: \"A brightly coloured robe prized by the Tai Bwo Wannai peoples.\"\n- id: 6355 # Villager hat\n  examine: \"A brightly coloured hat prized by the Tai Bwo Wannai peoples.\"\n- id: 6357 # Villager sandals\n  examine: \"A brightly coloured pair of local sandals.\"\n- id: 6359 # Villager armband\n  examine: \"A light blue armband, as worn by the Tai Bwo Wannai locals.\"\n- id: 6361 # Tribal top\n  examine: \"Local dress.\"\n- id: 6363 # Villager robe\n  examine: \"A brightly coloured robe prized by the Tai Bwo Wannai peoples.\"\n- id: 6365 # Villager hat\n  examine: \"A brightly coloured hat prized by the Tai Bwo Wannai peoples.\"\n- id: 6367 # Villager sandals\n  examine: \"A brightly coloured pair of local sandals.\"\n- id: 6369 # Villager armband\n  examine: \"A dark blue armband, as worn by the Tai Bwo Wannai locals.\"\n- id: 6371 # Tribal top\n  examine: \"Local dress.\"\n- id: 6373 # Villager robe\n  examine: \"A brightly coloured robe prized by the Tai Bwo Wannai peoples.\"\n- id: 6375 # Villager hat\n  examine: \"A brightly coloured hat prized by the Tai Bwo Wannai peoples.\"\n- id: 6377 # Villager sandals\n  examine: \"A brightly coloured pair of local sandals.\"\n- id: 6379 # Villager armband\n  examine: \"A white armband, as worn by the Tai Bwo Wannai locals.\"\n- id: 6382 # Fez\n  examine: \"A Fez hat. Juss like that.\"\n- id: 6384 # Desert top\n  examine: \"A bit itchy.\"\n- id: 6386 # Desert robes\n  examine: \"Has a coarse hard wearing texture.\"\n- id: 6388 # Desert top\n  examine: \"Good for those cold desert nights.\"\n- id: 6390 # Desert legs\n  examine: \"Better than factor 50 sun cream.\"\n- id: 6408 # Oak blackjack(o)\n  examine: \"An offensive blackjack.\"\n- id: 6410 # Oak blackjack(d)\n  examine: \"A defensive blackjack.\"\n- id: 6412 # Willow blackjack(o)\n  examine: \"An offensive blackjack.\"\n- id: 6414 # Willow blackjack(d)\n  examine: \"A defensive blackjack.\"\n- id: 6416 # Maple blackjack\n  examine: \"A solid bit of maple.\"\n- id: 6418 # Maple blackjack(o)\n  examine: \"An offensive blackjack.\"\n- id: 6420 # Maple blackjack(d)\n  examine: \"A defensive blackjack.\"\n- id: 6422 # Air rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 6424 # Water rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 6426 # Earth rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 6428 # Fire rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 6430 # Chaos rune\n  examine: \"Used for low level missile spells.\"\n- id: 6432 # Death rune\n  examine: \"Used for medium level missile spells.\"\n- id: 6434 # Law rune\n  examine: \"Used for teleport spells.\"\n- id: 6436 # Mind rune\n  examine: \"Used for basic level missile spells.\"\n- id: 6438 # Body rune\n  examine: \"Used for curse spells.\"\n- id: 6448 # Spadeful of coke\n  examine: \"A spadeful of refined coal.\"\n- id: 6453 # White rose seed\n  examine: \"A white rosebush seed.\"\n- id: 6454 # Red rose seed\n  examine: \"A red rosebush seed.\"\n- id: 6455 # Pink rose seed\n  examine: \"A pink rosebush seed.\"\n- id: 6456 # Vine seed\n  examine: \"A grapevine seed.\"\n- id: 6457 # Delphinium seed\n  examine: \"A delphinium seed.\"\n- id: 6458 # Orchid seed\n  examine: \"A pink orchid seed.\"\n- id: 6459 # Orchid seed\n  examine: \"A yellow orchid seed.\"\n- id: 6460 # Snowdrop seed\n  examine: \"A snowdrop seed.\"\n- id: 6461 # White tree shoot\n  examine: \"A shoot that has been cut from a dying White Tree.\"\n- id: 6462 # White tree shoot\n  examine: \"A shoot that has been cut from a dying White Tree.\"\n- id: 6464 # White tree sapling\n  examine: \"A young White Tree sapling.\"\n- id: 6465 # Ring of charos(a)\n  examine: \"The power within this ring has been activated.\"\n- id: 6466 # Rune shards\n  examine: \"A rune essence chip that has been broken into shards.\"\n- id: 6467 # Rune dust\n  examine: \"Crushed rune essence.\"\n- id: 6468 # Plant cure\n  examine: \"This plant cure emits potency.\"\n- id: 6469 # White tree fruit\n  examine: \"Looks delicious.\"\n- id: 6470 # Compost potion(4)\n  examine: \"Pour this on compost to turn it into super-compost.\"\n- id: 6472 # Compost potion(3)\n  examine: \"Pour this on compost to turn it into super-compost.\"\n- id: 6474 # Compost potion(2)\n  examine: \"Pour this on compost to turn it into super-compost.\"\n- id: 6476 # Compost potion(1)\n  examine: \"Pour this on compost to turn it into super-compost.\"\n- id: 6478 # Trolley\n  examine: \"I can use this to move heavy objects.\"\n- id: 6479 # List\n  examine: \"A list of things that I must collect for Queen Ellamaria.\"\n- id: 6522 # Toktz-xil-ul\n  examine: \"A razor sharp ring of obsidian.\"\n- id: 6523 # Toktz-xil-ak\n  examine: \"A razor sharp sword of obsidian.\"\n- id: 6524 # Toktz-ket-xil\n  examine: \"A spiked shield of obsidian.\"\n- id: 6525 # Toktz-xil-ek\n  examine: \"A large knife of obsidian.\"\n- id: 6526 # Toktz-mej-tal\n  examine: \"A staff of obsidian.\"\n- id: 6527 # Tzhaar-ket-em\n  examine: \"A mace of obsidian.\"\n- id: 6528 # Tzhaar-ket-om\n  examine: \"A maul of obsidian.\"\n- id: 6529 # Tokkul\n  examine: \"It's a token of some kind made from obsidian.\"\n- id: 6541 # Mouse toy\n  examine: \"An Advanced Combat Training Device.\"\n- id: 6542 # Present\n  examine: \"Thanks for all your help! Love, Bob & Neite.\"\n- id: 6543 # Antique lamp\n  examine: \"I wonder what happens if I rub it...\"\n- id: 6544 # Catspeak amulet(e)\n  examine: \"It's an amulet of cat speak. It makes vague purring noises.\"\n- id: 6545 # Chores\n  examine: \"A list of chores that Bob gave you to do.\"\n- id: 6546 # Recipe\n  examine: \"It says on the back 'My favourite recipe.'\"\n- id: 6548 # Nurse hat\n  examine: \"A nurse's hat, but does it have healing powers?\"\n- id: 6549 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6550 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6551 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6552 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6553 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6554 # Lazy cat\n  examine: \"Lethargic.\"\n- id: 6555 # Wily cat\n  examine: \"Wild.\"\n- id: 6556 # Wily cat\n  examine: \"Wild.\"\n- id: 6557 # Wily cat\n  examine: \"Wild.\"\n- id: 6558 # Wily cat\n  examine: \"Wild.\"\n- id: 6559 # Wily cat\n  examine: \"Wild.\"\n- id: 6560 # Wily cat\n  examine: \"Wild.\"\n- id: 6562 # Mud battlestaff\n  examine: \"It's a slightly magical stick.\"\n- id: 6563 # Mystic mud staff\n  examine: \"It's a slightly magical stick.\"\n- id: 6568 # Obsidian cape\n  examine: \"A cape of woven obsidian plates.\"\n- id: 6570 # Fire cape\n  examine: \"A cape of fire.\"\n- id: 6571 # Uncut onyx\n  examine: \"This would be worth more cut.\"\n- id: 6573 # Onyx\n  examine: \"This looks valuable.\"\n- id: 6575 # Onyx ring\n  examine: \"A valuable ring.\"\n- id: 6577 # Onyx necklace\n  examine: \"I wonder if this is valuable.\"\n- id: 6581 # Onyx amulet\n  examine: \"I wonder if I can get this enchanted.\"\n- id: 6583 # Ring of stone\n  examine: \"An enchanted ring.\"\n- id: 6585 # Amulet of fury\n  examine: \"A very powerful onyx amulet.\"\n- id: 6587 # White claws\n  examine: \"A set of fighting claws.\"\n- id: 6589 # White battleaxe\n  examine: \"A vicious looking axe.\"\n- id: 6591 # White dagger\n  examine: \"A vicious white dagger.\"\n- id: 6593 # White dagger(p)\n  examine: \"This dagger is poisoned.\"\n- id: 6595 # White dagger(p+)\n  examine: \"This dagger is poisoned.\"\n- id: 6597 # White dagger(p++)\n  examine: \"This dagger is poisoned.\"\n- id: 6599 # White halberd\n  examine: \"A white halberd.\"\n- id: 6601 # White mace\n  examine: \"A spiky mace.\"\n- id: 6603 # White magic staff\n  examine: \"A Magical staff.\"\n- id: 6605 # White sword\n  examine: \"A razor sharp sword.\"\n- id: 6607 # White longsword\n  examine: \"A razor sharp longsword.\"\n- id: 6609 # White 2h sword\n  examine: \"A two handed sword.\"\n- id: 6611 # White scimitar\n  examine: \"A vicious, curved sword.\"\n- id: 6613 # White warhammer\n  examine: \"I don't think it's intended for joinery.\"\n- id: 6615 # White chainbody\n  examine: \"A series of connected metal rings.\"\n- id: 6617 # White platebody\n  examine: \"Provides excellent protection.\"\n- id: 6619 # White boots\n  examine: \"These will protect my feet.\"\n- id: 6621 # White med helm\n  examine: \"A medium sized helmet.\"\n- id: 6623 # White full helm\n  examine: \"A full face helmet.\"\n- id: 6625 # White platelegs\n  examine: \"Big, White and heavy looking.\"\n- id: 6627 # White plateskirt\n  examine: \"Big, White and heavy looking.\"\n- id: 6629 # White gloves\n  examine: \"These will keep my hands warm!\"\n- id: 6631 # White sq shield\n  examine: \"A medium square shield.\"\n- id: 6633 # White kiteshield\n  examine: \"A large metal shield.\"\n- id: 6635 # Commorb\n  examine: \"A Temple Knight Communication Orb. Top Secret!\"\n- id: 6636 # Solus's hat\n  examine: \"Proof that I have defeated the evil mage Solus.\"\n- id: 6638 # Colour wheel\n  examine: \"A key to the nature of light itself.\"\n- id: 6639 # Hand mirror\n  examine: \"A small hand mirror.\"\n- id: 6641 # Yellow crystal\n  examine: \"A yellow crystal.\"\n- id: 6643 # Cyan crystal\n  examine: \"A cyan crystal.\"\n- id: 6644 # Blue crystal\n  examine: \"A blue crystal.\"\n- id: 6646 # Fractured crystal\n  examine: \"A fractured crystal, one of the edges is clear.\"\n- id: 6647 # Fractured crystal\n  examine: \"A fractured crystal, one of the edges is clear.\"\n- id: 6648 # Item list\n  examine: \"It's a list of items I need to collect.\"\n- id: 6649 # Edern's journal\n  examine: \"The journal of Nissyen Edern.\"\n- id: 6650 # Blackened crystal\n  examine: \"A blackened crystal sample.\"\n- id: 6651 # Newly made crystal\n  examine: \"A newly formed crystal.\"\n- id: 6652 # Newly made crystal\n  examine: \"A warm energy radiates from this crystal.\"\n- id: 6653 # Crystal trinket\n  examine: \"A small crystal trinket.\"\n- id: 6654 # Camo top\n  examine: \"Examine what?\"\n- id: 6655 # Camo bottoms\n  examine: \"Examine what?\"\n- id: 6656 # Camo helmet\n  examine: \"Examine what?\"\n- id: 6657 # Camo top\n  examine: \"Examine what?\"\n- id: 6658 # Camo bottoms\n  examine: \"Examine what?\"\n- id: 6659 # Camo helmet\n  examine: \"Examine what?\"\n- id: 6660 # Fishing explosive\n  examine: \"The jar keeps shaking...I'm scared.\"\n- id: 6662 # Broken fishing rod\n  examine: \"This fishing rod seems to have been bitten in half...\"\n- id: 6663 # Forlorn boot\n  examine: \"It seems someone vacated this boot in a hurry...\"\n- id: 6664 # Fishing explosive\n  examine: \"The jar keeps shaking...I'm scared.\"\n- id: 6665 # Mudskipper hat\n  examine: \"Fishy, damp and smelly.\"\n- id: 6666 # Flippers\n  examine: \"Strangely uncomfortable flippers.\"\n- id: 6668 # Fishbowl\n  examine: \"A fishless fishbowl.\"\n- id: 6669 # Fishbowl\n  examine: \"A fishless fishbowl with some seaweed.\"\n- id: 6670 # Fishbowl\n  examine: \"A fishbowl with a Tiny Bluefish in it.\"\n- id: 6671 # Fishbowl\n  examine: \"A fishbowl with a Tiny Greenfish in it.\"\n- id: 6672 # Fishbowl\n  examine: \"A fishbowl with a Tiny Spinefish in it.\"\n- id: 6673 # Fishbowl and net\n  examine: \"An empty fishbowl in a net.\"\n- id: 6674 # Tiny net\n  examine: \"A tiny net for grabbing tiny fish.\"\n- id: 6675 # An empty box\n  examine: \"'Ingredients; Ground Guam and Ground Seaweed.'\"\n- id: 6677 # Guam in a box\n  examine: \"'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Guam Leaf...\"\n- id: 6678 # Guam in a box?\n  examine: \"'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Guam Leaf...\"\n- id: 6679 # Seaweed in a box\n  examine: \"'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Seaweed...\"\n- id: 6680 # Seaweed in a box?\n  examine: \"'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Seaweed...\"\n- id: 6681 # Ground guam\n  examine: \"One of the ingredients for making fish food.\"\n- id: 6683 # Ground seaweed\n  examine: \"One of the ingredients for making fish food.\"\n- id: 6685 # Saradomin brew(4)\n  examine: \"4 doses of Saradomin brew.\"\n- id: 6687 # Saradomin brew(3)\n  examine: \"3 doses of Saradomin brew.\"\n- id: 6689 # Saradomin brew(2)\n  examine: \"2 doses of Saradomin brew.\"\n- id: 6691 # Saradomin brew(1)\n  examine: \"1 dose of Saradomin brew.\"\n- id: 6693 # Crushed nest\n  examine: \"A crushed bird's nest.\"\n- id: 6696 # Ice cooler\n  examine: \"Contains ice-cold water.\"\n- id: 6697 # Pat of butter\n  examine: \"A pat of freshly churned butter.\"\n- id: 6699 # Burnt potato\n  examine: \"This potato doesn't look edible.\"\n- id: 6701 # Baked potato\n  examine: \"It'd taste even better with some toppings.\"\n- id: 6703 # Potato with butter\n  examine: \"A baked potato with butter.\"\n- id: 6705 # Potato with cheese\n  examine: \"A baked potato with butter and cheese.\"\n- id: 6707 # Camulet\n  examine: \"An amulet of Camel-speak. It makes vague braying noises.\"\n- id: 6708 # Slayer gloves\n  examine: \"Especially good against diseased arachnids.\"\n- id: 6710 # Blindweed seed\n  examine: \"A Blindweed seed - plant in a Blindweed patch.\"\n- id: 6711 # Blindweed\n  examine: \"An inedible, foul smelling herb.\"\n- id: 6712 # Bucket of water\n  examine: \"It's a bucket of... water?\"\n- id: 6713 # Wrench\n  examine: \"A heavy metal wrench.\"\n- id: 6714 # Holy wrench\n  examine: \"A shining paragon of wrenchly virtue.\"\n- id: 6715 # Sluglings\n  examine: \"They look at you balefully. 'Feed us...'\"\n- id: 6716 # Karamthulhu\n  examine: \"A sinister looking squid.\"\n- id: 6717 # Karamthulhu\n  examine: \"A sinister looking squid.\"\n- id: 6718 # Fever spider body\n  examine: \"A diseased deceased Fever Spider. Handle with care.\"\n- id: 6719 # Unsanitary swill\n  examine: \"Sorry, I mean a bucket of 'rum'.\"\n- id: 6720 # Slayer gloves\n  examine: \"Especially good against diseased arachnids.\"\n- id: 6721 # Rusty scimitar\n  examine: \"A decent enough weapon gone rusty.\"\n- id: 6722 # Zombie head\n  examine: \"Alas...I hardly knew him.\"\n- id: 6724 # Seercull\n  examine: \"An ancient Fremennik bow that was once used to battle the Moon Clan.\"\n- id: 6728 # Bonemeal\n  examine: \"A pot of crushed Dagannoth-king bones.\"\n- id: 6729 # Dagannoth bones\n  examine: \"These would feed a dogfish for months!\"\n- id: 6731 # Seers ring\n  examine: \"A mysterious ring that can fill the wearer with magical power...\"\n- id: 6733 # Archers ring\n  examine: \"A fabled ring that improves the wearer's skill with a bow...\"\n- id: 6735 # Warrior ring\n  examine: \"A legendary ring once worn by Fremennik warriors.\"\n- id: 6737 # Berserker ring\n  examine: \"A ring reputed to bring out a berserk fury in its wearer.\"\n- id: 6739 # Dragon axe\n  examine: \"A very powerful axe.\"\n- id: 6741 # Broken axe\n  examine: \"Bob can fix this for me.\"\n- id: 6745 # Silverlight\n  examine: \"The magical sword 'Silverlight', stained black with mushroom ink.\"\n- id: 6746 # Darklight\n  examine: \"The magical sword 'Silverlight', enhanced with the blood of Agrith-Naar.\"\n- id: 6747 # Demonic sigil mould\n  examine: \"Used to make the sigil of the demon Agrith-Naar.\"\n- id: 6748 # Demonic sigil\n  examine: \"A sigil used for the summoning of the demon Agrith-Naar.\"\n- id: 6749 # Demonic tome\n  examine: \"Will this book help in summoning Agrith-Naar?\"\n- id: 6750 # Black desert shirt\n  examine: \"A desert shirt stained black with mushroom ink.\"\n- id: 6752 # Black desert robe\n  examine: \"A desert robe stained black with mushroom ink.\"\n- id: 6754 # Enchanted key\n  examine: \"It seems to change temperature as I walk.\"\n- id: 6755 # Journal\n  examine: \"Somebody's private journal.\"\n- id: 6756 # Letter\n  examine: \"A sealed letter to the king.\"\n- id: 6757 # Letter\n  examine: \"A sealed letter to Jorral.\"\n- id: 6758 # Scroll\n  examine: \"A timeline of the outpost.\"\n- id: 6759 # Chest\n  examine: \"A dirty chest.\"\n- id: 6760 # Guthix mjolnir\n  examine: \"A Guthix Mjolnir.\"\n- id: 6762 # Saradomin mjolnir\n  examine: \"A Saradomin Mjolnir.\"\n- id: 6764 # Zamorak mjolnir\n  examine: \"A Zamorak Mjolnir.\"\n- id: 6766 # Cat antipoison\n  examine: \"Antipoison for Pox.\"\n- id: 6767 # Book\n  examine: \"The book of Rats.\"\n- id: 6768 # Poisoned cheese\n  examine: \"A little more smelly than usual.\"\n- id: 6769 # Music scroll\n  examine: \"Charming.\"\n- id: 6770 # Directions\n  examine: \"Jimmy Dazzler's directions.\"\n- id: 6771 # Pot of weeds\n  examine: \"Contains garden weeds.\"\n- id: 6772 # Smouldering pot\n  examine: \"Contains slowly burning garden weeds.\"\n- id: 6773 # Rat pole\n  examine: \"A pole for putting rats on.\"\n- id: 6774 # Rat pole\n  examine: \"A pole with one rat on it.\"\n- id: 6775 # Rat pole\n  examine: \"A pole with two rats on it.\"\n- id: 6776 # Rat pole\n  examine: \"A pole with three rats on it.\"\n- id: 6777 # Rat pole\n  examine: \"A pole with four rats on it.\"\n- id: 6778 # Rat pole\n  examine: \"A pole with five rats on it.\"\n- id: 6779 # Rat pole\n  examine: \"A pole with six rats on it.\"\n- id: 6785 # Statuette\n  examine: \"A statue of the goddess Elidinis.\"\n- id: 6786 # Robe of elidinis\n  examine: \"This looks quite old.\"\n- id: 6787 # Robe of elidinis\n  examine: \"A patched up robe.\"\n- id: 6788 # Torn robe\n  examine: \"This robe is too torn to wear.\"\n- id: 6789 # Torn robe\n  examine: \"This robe is too torn to wear.\"\n- id: 6790 # Shoes\n  examine: \"Awusah's Shoes.\"\n- id: 6791 # Sole\n  examine: \"Awusah's Sole.\"\n- id: 6792 # Ancestral key\n  examine: \"An ancient key from the shrine in Nardah.\"\n- id: 6793 # Ballad\n  examine: \"The Ballad of Jareesh.\"\n- id: 6794 # Choc-ice\n  examine: \"Better eat this before it melts.\"\n- id: 6796 # Lamp\n  examine: \"Ooh a nice shiny lamp.\"\n- id: 6797 # Watering can\n  examine: \"This watering can is empty.\"\n- id: 6809 # Granite legs\n  examine: \"These look pretty heavy.\"\n- id: 6810 # Bonemeal\n  examine: \"A pot of crushed wyvern bones.\"\n- id: 6812 # Wyvern bones\n  examine: \"Bones of a large flying creature!\"\n- id: 6814 # Fur\n  examine: \"This would make warm clothing.\"\n- id: 6817 # Slender blade\n  examine: \"A slender two-handed sword.\"\n- id: 6818 # Bow-sword\n  examine: \"A sharp sword that can also fire arrows.\"\n- id: 6819 # Large pouch\n  examine: \"A large pouch used for storing essence.\"\n- id: 6821 # Orb\n  examine: \"A glowing orb.\"\n- id: 6822 # Star bauble\n  examine: \"An unpainted bauble shaped like a star.\"\n- id: 6823 # Star bauble\n  examine: \"A bauble shaped like a star painted yellow.\"\n- id: 6824 # Star bauble\n  examine: \"A bauble shaped like a star painted red.\"\n- id: 6825 # Star bauble\n  examine: \"A bauble shaped like a star painted blue.\"\n- id: 6826 # Star bauble\n  examine: \"A bauble shaped like a star painted green.\"\n- id: 6827 # Star bauble\n  examine: \"A bauble shaped like a star painted pink.\"\n- id: 6828 # Box bauble\n  examine: \"An unpainted bauble shaped like a gift.\"\n- id: 6829 # Box bauble\n  examine: \"A bauble shaped like a gift painted yellow.\"\n- id: 6830 # Box bauble\n  examine: \"A bauble shaped like a gift painted red.\"\n- id: 6831 # Box bauble\n  examine: \"A bauble shaped like a gift painted blue.\"\n- id: 6832 # Box bauble\n  examine: \"A bauble shaped like a gift painted green.\"\n- id: 6833 # Box bauble\n  examine: \"A bauble shaped like a gift painted pink.\"\n- id: 6834 # Diamond bauble\n  examine: \"An unpainted bauble shaped like a diamond.\"\n- id: 6835 # Diamond bauble\n  examine: \"A bauble shaped like a diamond painted yellow.\"\n- id: 6836 # Diamond bauble\n  examine: \"A bauble shaped like a diamond painted red.\"\n- id: 6837 # Diamond bauble\n  examine: \"A bauble shaped like a diamond painted blue.\"\n- id: 6838 # Diamond bauble\n  examine: \"A bauble shaped like a diamond painted green.\"\n- id: 6839 # Diamond bauble\n  examine: \"A bauble shaped like a diamond painted pink.\"\n- id: 6840 # Tree bauble\n  examine: \"An unpainted bauble shaped like a wintumber tree.\"\n- id: 6841 # Tree bauble\n  examine: \"A bauble shaped like a wintumber tree painted yellow.\"\n- id: 6842 # Tree bauble\n  examine: \"A bauble shaped like a wintumber tree painted red.\"\n- id: 6843 # Tree bauble\n  examine: \"A bauble shaped like a wintumber tree painted blue.\"\n- id: 6844 # Tree bauble\n  examine: \"A bauble shaped like a wintumber tree painted green.\"\n- id: 6845 # Tree bauble\n  examine: \"A bauble shaped like a wintumber tree painted pink.\"\n- id: 6846 # Bell bauble\n  examine: \"An unpainted bauble shaped like a bell.\"\n- id: 6847 # Bell bauble\n  examine: \"A bauble shaped like a bell painted yellow.\"\n- id: 6848 # Bell bauble\n  examine: \"A bauble shaped like a bell painted red.\"\n- id: 6849 # Bell bauble\n  examine: \"A bauble shaped like a bell painted blue.\"\n- id: 6850 # Bell bauble\n  examine: \"A bauble shaped like a bell painted green.\"\n- id: 6851 # Bell bauble\n  examine: \"A bauble shaped like a bell painted pink.\"\n- id: 6852 # Puppet box\n  examine: \"A box for storing completed puppets.\"\n- id: 6853 # Bauble box\n  examine: \"A box for storing painted baubles.\"\n- id: 6854 # Puppet box\n  examine: \"A box full of puppets. Bring to the Taverly members gate.\"\n- id: 6855 # Bauble box\n  examine: \"A box full of painted baubles. Give to a Pixie or use to decorate a tree.\"\n- id: 6856 # Bobble hat\n  examine: \"A woolly bobble hat.\"\n- id: 6857 # Bobble scarf\n  examine: \"A woolly scarf.\"\n- id: 6858 # Jester hat\n  examine: \"A woolly jester hat.\"\n- id: 6859 # Jester scarf\n  examine: \"A woolly jester scarf.\"\n- id: 6860 # Tri-jester hat\n  examine: \"A woolly triple bobble jester hat.\"\n- id: 6861 # Tri-jester scarf\n  examine: \"A woolly jester scarf.\"\n- id: 6862 # Woolly hat\n  examine: \"A woolly tobogganing hat.\"\n- id: 6863 # Woolly scarf\n  examine: \"A woolly tobogganing scarf.\"\n- id: 6864 # Marionette handle\n  examine: \"The controlling part of a marionette.\"\n- id: 6865 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6866 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6867 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6868 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6869 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6870 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6871 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6872 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6873 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6874 # Red marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6875 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6876 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6877 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6878 # Blue marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6879 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6880 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6881 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6882 # Green marionette\n  examine: \"I've got no strings ... oh hang on!\"\n- id: 6883 # Peach\n  examine: \"A tasty fruit.\"\n- id: 6885 # Progress hat\n  examine: \"A magic training arena progress hat.\"\n- id: 6886 # Progress hat\n  examine: \"A magic training arena progress hat.\"\n- id: 6887 # Progress hat\n  examine: \"A magic training arena progress hat.\"\n- id: 6889 # Mage's book\n  examine: \"The magical book of the Mage.\"\n- id: 6891 # Arena book\n  examine: \"A book about the Training Arena.\"\n- id: 6893 # Leather boots\n  examine: \"Comfortable leather boots.\"\n- id: 6894 # Adamant kiteshield\n  examine: \"A large metal shield.\"\n- id: 6895 # Adamant med helm\n  examine: \"A medium sized helmet.\"\n- id: 6896 # Emerald\n  examine: \"This looks valuable.\"\n- id: 6897 # Rune longsword\n  examine: \"A razor sharp longsword.\"\n- id: 6898 # Cylinder\n  examine: \"A green cylinder.\"\n- id: 6899 # Cube\n  examine: \"A yellow cube.\"\n- id: 6900 # Icosahedron\n  examine: \"A blue icosahedron.\"\n- id: 6901 # Pentamid\n  examine: \"A red pentamid.\"\n- id: 6902 # Orb\n  examine: \"A white sphere.\"\n- id: 6903 # Dragonstone\n  examine: \"This looks valuable.\"\n- id: 6904 # Animals' bones\n  examine: \"Various animals' bones.\"\n- id: 6905 # Animals' bones\n  examine: \"Various animals' bones.\"\n- id: 6906 # Animals' bones\n  examine: \"Various animals' bones.\"\n- id: 6907 # Animals' bones\n  examine: \"Various animals' bones.\"\n- id: 6908 # Beginner wand\n  examine: \"A beginner level wand.\"\n- id: 6910 # Apprentice wand\n  examine: \"An apprentice level wand.\"\n- id: 6912 # Teacher wand\n  examine: \"A teacher level wand.\"\n- id: 6914 # Master wand\n  examine: \"A master level wand.\"\n- id: 6916 # Infinity top\n  examine: \"Mystical robes.\"\n- id: 6918 # Infinity hat\n  examine: \"A mystic hat.\"\n- id: 6920 # Infinity boots\n  examine: \"Mystical boots.\"\n- id: 6922 # Infinity gloves\n  examine: \"Mystical gloves.\"\n- id: 6924 # Infinity bottoms\n  examine: \"Mystical robes.\"\n- id: 6945 # Sandy hand\n  examine: \"A severed hand covered with sand.\"\n- id: 6946 # Beer soaked hand\n  examine: \"A severed hand dripping with beer.\"\n- id: 6947 # Bert's rota\n  examine: \"A copy of a work rota.\"\n- id: 6948 # Sandy's rota\n  examine: \"An original work rota.\"\n- id: 6949 # A magic scroll\n  examine: \"This scroll glows with an inner light.\"\n- id: 6950 # Magical orb\n  examine: \"An ordinary looking magical scrying orb.\"\n- id: 6951 # Magical orb (a)\n  examine: \"This magical scrying orb pulsates as it stores information.\"\n- id: 6952 # Truth serum\n  examine: \"Fluid sloshes innocently in this vial.\"\n- id: 6953 # Bottled water\n  examine: \"A bottle of water.\"\n- id: 6954 # Redberry juice\n  examine: \"Redberry Juice sloshes around in this vial waiting for white berries to be added.\"\n- id: 6955 # Pink dye\n  examine: \"A vial of pink dye.\"\n- id: 6956 # Rose tinted lens\n  examine: \"This lens has a pinkish tinge to it.\"\n- id: 6957 # Wizard's head\n  examine: \"A decapitated, sand covered head.\"\n- id: 6958 # Sand\n  examine: \"A handful of sand from Sandy's pocket.\"\n- id: 6961 # Baguette\n  examine: \"A freshly baked baguette.\"\n- id: 6962 # Triangle sandwich\n  examine: \"A freshly made triangle sandwich.\"\n- id: 6963 # Roll\n  examine: \"A freshly made roll.\"\n- id: 6964 # Coins\n  examine: \"Lovely money!\"\n- id: 6965 # Square sandwich\n  examine: \"A freshly made square sandwich.\"\n- id: 6966 # Prison key\n  examine: \"A key to the prison.\"\n- id: 6967 # Dragon med helm\n  examine: \"Makes the wearer pretty intimidating.\"\n- id: 6969 # Shark\n  examine: \"I'd better be careful eating this.\"\n- id: 6970 # Pyramid top\n  examine: \"It's a solid gold pyramid!\"\n- id: 6971 # Sandstone (1kg)\n  examine: \"A tiny chunk of sandstone.\"\n- id: 6973 # Sandstone (2kg)\n  examine: \"A small chunk of sandstone.\"\n- id: 6975 # Sandstone (5kg)\n  examine: \"A medium-sized chunk of sandstone.\"\n- id: 6977 # Sandstone (10kg)\n  examine: \"A large chunk of sandstone.\"\n- id: 6979 # Granite (500g)\n  examine: \"A tiny chunk of granite.\"\n- id: 6981 # Granite (2kg)\n  examine: \"A small chunk of granite.\"\n- id: 6983 # Granite (5kg)\n  examine: \"A medium-sized chunk of granite.\"\n- id: 6985 # Sandstone (20kg)\n  examine: \"A huge twenty-kilo block of sandstone.\"\n- id: 6986 # Sandstone (32kg)\n  examine: \"A huge thirty-two-kilo block of sandstone.\"\n- id: 6987 # Sandstone body\n  examine: \"The body of a sandstone statue.\"\n- id: 6988 # Sandstone base\n  examine: \"The base and legs of a sandstone statue.\"\n- id: 6989 # Stone head\n  examine: \"A granite head shaped like the sculptor Lazim.\"\n- id: 6990 # Stone head\n  examine: \"A granite head shaped like the god Zamorak.\"\n- id: 6991 # Stone head\n  examine: \"A granite head shaped like the god Icthlarin.\"\n- id: 6992 # Stone head\n  examine: \"A granite head shaped like a camel.\"\n- id: 6993 # Z sigil\n  examine: \"A metal sigil in the shape of a Z.\"\n- id: 6994 # M sigil\n  examine: \"A metal sigil in the shape of an M.\"\n- id: 6995 # R sigil\n  examine: \"A metal sigil in the shape of an R.\"\n- id: 6996 # K sigil\n  examine: \"A metal sigil in the shape of a K.\"\n- id: 6997 # Stone left arm\n  examine: \"The left arm of a large stone statue.\"\n- id: 6998 # Stone right arm\n  examine: \"The right arm of a large stone statue.\"\n- id: 6999 # Stone left leg\n  examine: \"The left leg of a large stone statue.\"\n- id: 7000 # Stone right leg\n  examine: \"The right leg of a large stone statue.\"\n- id: 7001 # Camel mould (p)\n  examine: \"A positive clay mould of a camel's head.\"\n- id: 7002 # Stone head\n  examine: \"A granite head that will fit exactly into the pedestal.\"\n- id: 7003 # Camel mask\n  examine: \"Blend in in the desert.\"\n- id: 7051 # Unlit bug lantern\n  examine: \"A lantern to aid attacking Harpie bugs.\"\n- id: 7053 # Lit bug lantern\n  examine: \"A lantern to aid attacking Harpie bugs.\"\n- id: 7054 # Chilli potato\n  examine: \"A baked potato with chilli con carne.\"\n- id: 7056 # Egg potato\n  examine: \"A baked potato with egg and tomato.\"\n- id: 7058 # Mushroom potato\n  examine: \"A baked potato with mushroom and onions.\"\n- id: 7060 # Tuna potato\n  examine: \"A baked potato with tuna and sweetcorn.\"\n- id: 7062 # Chilli con carne\n  examine: \"A bowl of meat in chilli-con-carne sauce.\"\n- id: 7064 # Egg and tomato\n  examine: \"A bowl of scrambled eggs and tomato.\"\n- id: 7066 # Mushroom & onion\n  examine: \"A bowl of fried mushroom and onions.\"\n- id: 7068 # Tuna and corn\n  examine: \"A bowl of cooked tuna and sweetcorn.\"\n- id: 7070 # Minced meat\n  examine: \"A bowl of finely minced meat.\"\n- id: 7072 # Spicy sauce\n  examine: \"A bowl of spicy sauce.\"\n- id: 7074 # Chopped garlic\n  examine: \"A bowl of chopped garlic.\"\n- id: 7076 # Uncooked egg\n  examine: \"A bowl of uncooked egg.\"\n- id: 7078 # Scrambled egg\n  examine: \"A bowl of scrambled egg.\"\n- id: 7080 # Sliced mushrooms\n  examine: \"A bowl of sliced Bittercap mushrooms.\"\n- id: 7082 # Fried mushrooms\n  examine: \"A bowl of fried Bittercap mushrooms.\"\n- id: 7084 # Fried onions\n  examine: \"A bowl of sliced, fried onions.\"\n- id: 7086 # Chopped tuna\n  examine: \"A bowl of finely chopped tuna.\"\n- id: 7088 # Sweetcorn\n  examine: \"A bowl of cooked sweetcorn.\"\n- id: 7090 # Burnt egg\n  examine: \"A bowl of burnt, overcooked egg.\"\n- id: 7092 # Burnt onion\n  examine: \"A bowl of blackened onions.\"\n- id: 7094 # Burnt mushroom\n  examine: \"A bowl of burnt sliced mushroom.\"\n- id: 7108 # Gunpowder\n  examine: \"Best keep this away from naked flames.\"\n- id: 7109 # Fuse\n  examine: \"Burns very well.\"\n- id: 7110 # Stripy pirate shirt\n  examine: \"A sea worthy shirt.\"\n- id: 7112 # Pirate bandana\n  examine: \"Essential pirate wear.\"\n- id: 7114 # Pirate boots\n  examine: \"Not for land lubbers.\"\n- id: 7116 # Pirate leggings\n  examine: \"A sea worthy pair of trousers.\"\n- id: 7118 # Canister\n  examine: \"A cannister holding shrapnel.\"\n- id: 7119 # Cannon ball\n  examine: \"A heavy metal ball.\"\n- id: 7120 # Ramrod\n  examine: \"For cleaning and packing the cannon.\"\n- id: 7121 # Repair plank\n  examine: \"A plank of wood to repair the hull with.\"\n- id: 7122 # Stripy pirate shirt\n  examine: \"A sea worthy shirt.\"\n- id: 7124 # Pirate bandana\n  examine: \"Essential pirate wear.\"\n- id: 7126 # Pirate leggings\n  examine: \"A sea worthy pair of trousers.\"\n- id: 7128 # Stripy pirate shirt\n  examine: \"A sea worthy shirt.\"\n- id: 7130 # Pirate bandana\n  examine: \"Essential pirate wear.\"\n- id: 7132 # Pirate leggings\n  examine: \"A sea worthy pair of trousers.\"\n- id: 7134 # Stripy pirate shirt\n  examine: \"A sea worthy shirt.\"\n- id: 7136 # Pirate bandana\n  examine: \"Essential pirate wear.\"\n- id: 7138 # Pirate leggings\n  examine: \"A sea worthy pair of trousers.\"\n- id: 7140 # Lucky cutlass\n  examine: \"Feels quite lucky.\"\n- id: 7141 # Harry's cutlass\n  examine: \"I hope he doesn't want it back.\"\n- id: 7142 # Rapier\n  examine: \"The very butcher of a silk button.\"\n- id: 7143 # Plunder\n  examine: \"Looks valuable.\"\n- id: 7144 # Book o' piracy\n  examine: \"By Cap'n Hook-Hand Morrisane.\"\n- id: 7145 # Cannon barrel\n  examine: \"A working cannon barrel.\"\n- id: 7146 # Broken cannon\n  examine: \"Not likely to work again.\"\n- id: 7148 # Repair plank\n  examine: \"A plank of wood to repair the hull with.\"\n- id: 7149 # Canister\n  examine: \"A cannister holding shrapnel.\"\n- id: 7150 # Tacks\n  examine: \"Useful for pinning up paintings.\"\n- id: 7155 # Rope\n  examine: \"A coil of rope.\"\n- id: 7156 # Tinderbox\n  examine: \"Useful for lighting a fire.\"\n- id: 7157 # Braindeath 'rum'\n  examine: \"I think it is eating through the bottle.\"\n- id: 7158 # Dragon 2h sword\n  examine: \"A two-handed Dragon Sword.\"\n- id: 7159 # Insulated boots\n  examine: \"They're heavily insulated wellies.\"\n- id: 7162 # Pie recipe book\n  examine: \"Lots of pie recipes for me to try.\"\n- id: 7164 # Part mud pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7166 # Part mud pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7168 # Raw mud pie\n  examine: \"Needs to be baked before I can use it.\"\n- id: 7170 # Mud pie\n  examine: \"All the good of the earth.\"\n- id: 7172 # Part garden pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7174 # Part garden pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7176 # Raw garden pie\n  examine: \"Needs cooking before I eat it.\"\n- id: 7178 # Garden pie\n  examine: \"What I wouldn't give for a good steak about now...\"\n- id: 7180 # Half a garden pie\n  examine: \"What I wouldn't give for a good steak about now...\"\n- id: 7182 # Part fish pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7184 # Part fish pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7186 # Raw fish pie\n  examine: \"Raw fish is risky, better cook it.\"\n- id: 7188 # Fish pie\n  examine: \"Bounty of the sea.\"\n- id: 7190 # Half a fish pie\n  examine: \"Bounty of the sea.\"\n- id: 7192 # Part admiral pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7194 # Part admiral pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7196 # Raw admiral pie\n  examine: \"This would taste a lot better cooked.\"\n- id: 7198 # Admiral pie\n  examine: \"Much tastier than a normal fish pie.\"\n- id: 7200 # Half an admiral pie\n  examine: \"Much tastier than a normal fish pie.\"\n- id: 7202 # Part wild pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7204 # Part wild pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7206 # Raw wild pie\n  examine: \"Good as it looks, I'd better cook it.\"\n- id: 7208 # Wild pie\n  examine: \"A triumph of man over nature.\"\n- id: 7210 # Half a wild pie\n  examine: \"A triumph of man over nature.\"\n- id: 7212 # Part summer pie\n  examine: \"Still needs two more ingredients.\"\n- id: 7214 # Part summer pie\n  examine: \"Still needs one more ingredient.\"\n- id: 7216 # Raw summer pie\n  examine: \"Fresh fruit may be good for you, but I should really cook this.\"\n- id: 7218 # Summer pie\n  examine: \"All the fruits of a very small forest.\"\n- id: 7220 # Half a summer pie\n  examine: \"All the fruits of a very small forest.\"\n- id: 7222 # Burnt rabbit\n  examine: \"This could be mistaken for charcoal.\"\n- id: 7223 # Roast rabbit\n  examine: \"A delicious looking piece of roast rabbit.\"\n- id: 7224 # Skewered rabbit\n  examine: \"All ready to be used on a fire.\"\n- id: 7225 # Iron spit\n  examine: \"An iron spit.\"\n- id: 7230 # Skewered chompy\n  examine: \"A skewered chompy bird.\"\n- id: 7319 # Red boater\n  examine: \"Stylish!\"\n- id: 7321 # Orange boater\n  examine: \"Stylish!\"\n- id: 7323 # Green boater\n  examine: \"Stylish!\"\n- id: 7325 # Blue boater\n  examine: \"Stylish!\"\n- id: 7327 # Black boater\n  examine: \"Stylish!\"\n- id: 7329 # Red firelighter\n  examine: \"Makes firelighting a lot easier.\"\n- id: 7330 # Green firelighter\n  examine: \"Makes firelighting a lot easier.\"\n- id: 7331 # Blue firelighter\n  examine: \"Makes firelighting a lot easier.\"\n- id: 7362 # Studded body (g)\n  examine: \"Those studs should provide a bit more protection. Nice trim too!\"\n- id: 7364 # Studded body (t)\n  examine: \"Those studs should provide a bit more protection. Nice trim too!\"\n- id: 7366 # Studded chaps (g)\n  examine: \"Those studs should provide a bit more protection. Nice trim too!\"\n- id: 7368 # Studded chaps (t)\n  examine: \"Those studs should provide a bit more protection. Nice trim too!\"\n- id: 7386 # Blue skirt (g)\n  examine: \"Leg covering favoured by women and wizards. With a colourful trim!\"\n- id: 7388 # Blue skirt (t)\n  examine: \"Leg covering favoured by women and wizards. With a colourful trim!\"\n- id: 7398 # Enchanted robe\n  examine: \"Enchanted Wizards robes.\"\n- id: 7399 # Enchanted top\n  examine: \"Enchanted Wizards robes.\"\n- id: 7400 # Enchanted hat\n  examine: \"A three pointed hat of magic.\"\n- id: 7404 # Red logs\n  examine: \"A number of chemical covered wooden logs.\"\n- id: 7405 # Green logs\n  examine: \"A number of chemical covered wooden logs.\"\n- id: 7406 # Blue logs\n  examine: \"A number of chemical covered wooden logs.\"\n- id: 7408 # Draynor skull\n  examine: \"I shouldn't joke, this is a grave matter.\"\n- id: 7409 # Magic secateurs\n  examine: \"The only way to kill a Tanglefoot.\"\n- id: 7410 # Queen's secateurs\n  examine: \"Contains the Fairy Queen's magic essence.\"\n- id: 7411 # Symptoms list\n  examine: \"A list of the Fairy Queen's symptoms.\"\n- id: 7413 # Bird nest\n  examine: \"It's a bird's nest with some seeds in it.\"\n- id: 7416 # Mole claw\n  examine: \"A mole claw.\"\n- id: 7418 # Mole skin\n  examine: \"The skin of a large mole.\"\n- id: 7421 # Fungicide spray 10\n  examine: \"Pumps fungicide.\"\n- id: 7422 # Fungicide spray 9\n  examine: \"Pumps fungicide.\"\n- id: 7423 # Fungicide spray 8\n  examine: \"Pumps fungicide.\"\n- id: 7424 # Fungicide spray 7\n  examine: \"Pumps fungicide.\"\n- id: 7425 # Fungicide spray 6\n  examine: \"Pumps fungicide.\"\n- id: 7426 # Fungicide spray 5\n  examine: \"Pumps fungicide.\"\n- id: 7427 # Fungicide spray 4\n  examine: \"Pumps fungicide.\"\n- id: 7428 # Fungicide spray 3\n  examine: \"Pumps fungicide.\"\n- id: 7429 # Fungicide spray 2\n  examine: \"Pumps fungicide.\"\n- id: 7430 # Fungicide spray 1\n  examine: \"Pumps fungicide.\"\n- id: 7431 # Fungicide spray 0\n  examine: \"Pumps fungicide.\"\n- id: 7432 # Fungicide\n  examine: \"Does exactly what it says on the tin.\"\n- id: 7433 # Wooden spoon\n  examine: \"Spoooooon!\"\n- id: 7435 # Egg whisk\n  examine: \"A large whisk of death.\"\n- id: 7437 # Spork\n  examine: \"Use the spork.\"\n- id: 7439 # Spatula\n  examine: \"A large spatula... of doom!\"\n- id: 7441 # Frying pan\n  examine: \"Looks like it's non-stick too!\"\n- id: 7443 # Skewer\n  examine: \"Generally used for impaling fresh meat.\"\n- id: 7445 # Rolling pin\n  examine: \"That's how I roll!\"\n- id: 7447 # Kitchen knife\n  examine: \"A sharp, dependable knife, for filleting meat.\"\n- id: 7449 # Meat tenderiser\n  examine: \"Often used to soften tough meat up.\"\n- id: 7451 # Cleaver\n  examine: \"An effective tool for chopping tough meat.\"\n- id: 7463 # Cornflour\n  examine: \"A little heap of cornflour.\"\n- id: 7464 # Book on chickens\n  examine: \"A tatty old book belonging to the Wise Old Man of Draynor Village.\"\n- id: 7465 # Vanilla pod\n  examine: \"Surprise, it looks like a vanilla pod.\"\n- id: 7468 # Pot of cornflour\n  examine: \"It's cornflour in a pot.\"\n- id: 7470 # Cornflour mixture\n  examine: \"A mixture of milk, cream and cornflour.\"\n- id: 7471 # Milky mixture\n  examine: \"It's a bucket of milk and cream.\"\n- id: 7472 # Cinnamon\n  examine: \"Some cinnamon sticks.\"\n- id: 7473 # Brulee\n  examine: \"It's just missing a sprinkling of cinnamon.\"\n- id: 7474 # Brulee\n  examine: \"A pot of brulee mixture - needs egg.\"\n- id: 7475 # Brulee\n  examine: \"Perfect, it just needs flambeing.\"\n- id: 7476 # Brulee supreme\n  examine: \"A pot of brulee supreme.\"\n- id: 7477 # Evil chicken's egg\n  examine: \"What came first, the chicken or...\"\n- id: 7478 # Dragon token\n  examine: \"It's got a dragon on it.\"\n- id: 7479 # Spicy stew\n  examine: \"It's a meat and potato stew with fancy seasoning.\"\n- id: 7480 # Red spice (4)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7481 # Red spice (3)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7482 # Red spice (2)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7483 # Red spice (1)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7484 # Orange spice (4)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7485 # Orange spice (3)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7486 # Orange spice (2)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7487 # Orange spice (1)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7488 # Brown spice (4)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7489 # Brown spice (3)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7490 # Brown spice (2)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7491 # Brown spice (1)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7492 # Yellow spice (4)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7493 # Yellow spice (3)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7494 # Yellow spice (2)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7495 # Yellow spice (1)\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7496 # Empty spice shaker\n  examine: \"Allows for equal distribution of spice.\"\n- id: 7497 # Dirty blast\n  examine: \"A cool refreshing fruit mix. With ash in for some reason.\"\n- id: 7498 # Antique lamp\n  examine: \"I wonder what happens if I rub it...\"\n- id: 7508 # Asgoldian ale\n  examine: \"There appears to be a coin in the bottom. Liked by dwarves.\"\n- id: 7509 # Dwarven rock cake\n  examine: \"Red hot and glowing, ouch! Only for dwarf consumption.\"\n- id: 7510 # Dwarven rock cake\n  examine: \"Cool and heavy as a brick. Only for dwarf consumption.\"\n- id: 7511 # Slop of compromise\n  examine: \"Two out of two goblin generals prefer it!\"\n- id: 7512 # Soggy bread\n  examine: \"Previously a nice crispy loaf of bread. Now just kind of icky.\"\n- id: 7513 # Spicy maggots\n  examine: \"They clearly taste so much better this way!\"\n- id: 7514 # Dyed orange\n  examine: \"Orange slices which have been dyed, but it looks more like they died.\"\n- id: 7515 # Breadcrumbs\n  examine: \"Glad these aren't in my bed.\"\n- id: 7516 # Kelp\n  examine: \"Slightly damp seaweed.\"\n- id: 7517 # Ground kelp\n  examine: \"Kelp flakes. Smells of the sea.\"\n- id: 7518 # Crab meat\n  examine: \"A smelly meat.\"\n- id: 7519 # Crab meat\n  examine: \"A smelly meat.\"\n- id: 7520 # Burnt crab meat\n  examine: \"Oh dear, it's burnt.\"\n- id: 7521 # Cooked crab meat\n  examine: \"Nice and Tasty!\"\n- id: 7523 # Cooked crab meat\n  examine: \"Nice and Tasty!\"\n- id: 7524 # Cooked crab meat\n  examine: \"Nice and Tasty!\"\n- id: 7525 # Cooked crab meat\n  examine: \"Nice and Tasty!\"\n- id: 7526 # Cooked crab meat\n  examine: \"Nice and Tasty!\"\n- id: 7527 # Ground crab meat\n  examine: \"A smelly paste.\"\n- id: 7528 # Ground cod\n  examine: \"A smelly paste.\"\n- id: 7529 # Raw fishcake\n  examine: \"Would taste nicer if I cooked it.\"\n- id: 7530 # Cooked fishcake\n  examine: \"Mmmm, reminds me of the seaside.\"\n- id: 7531 # Burnt fishcake\n  examine: \"Darn thing's all burnt!\"\n- id: 7532 # Mudskipper hide\n  examine: \"Hmmm, what can I use this for?\"\n- id: 7533 # Rock\n  examine: \"A rock.\"\n- id: 7534 # Fishbowl helmet\n  examine: \"You'll look daft, but at least you won't drown!\"\n- id: 7535 # Diving apparatus\n  examine: \"I'll need a helmet to make this work.\"\n- id: 7536 # Fresh crab claw\n  examine: \"Fresh off the crab itself.\"\n- id: 7537 # Crab claw\n  examine: \"If it's good enough for crabs, it's good enough for me!\"\n- id: 7538 # Fresh crab shell\n  examine: \"Fresh off the crab itself.\"\n- id: 7539 # Crab helmet\n  examine: \"If it's good enough for crabs, it's good enough for me!\"\n- id: 7540 # Broken crab claw\n  examine: \"Darn, it's useless now.\"\n- id: 7541 # Broken crab shell\n  examine: \"Darn, it's useless now.\"\n- id: 7542 # Cake of guidance\n  examine: \"Imbued with knowledge itself.\"\n- id: 7543 # Raw guide cake\n  examine: \"Now all I need to do is cook it.\"\n- id: 7544 # Enchanted egg\n  examine: \"Egg containing knowledge.\"\n- id: 7545 # Enchanted milk\n  examine: \"Guiding milk.\"\n- id: 7546 # Enchanted flour\n  examine: \"A pot of special flour.\"\n- id: 7547 # Druid pouch\n  examine: \"An empty druid pouch.\"\n- id: 7548 # Potato seed\n  examine: \"A potato seed - plant in an allotment.\"\n- id: 7550 # Onion seed\n  examine: \"An onion seed - plant in an allotment.\"\n- id: 7552 # Mithril arrow\n  examine: \"Arrows with mithril heads.\"\n- id: 7554 # Fire rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 7556 # Water rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 7558 # Air rune\n  examine: \"One of the 4 basic elemental Runes.\"\n- id: 7560 # Chaos rune\n  examine: \"Used for low level missile spells.\"\n- id: 7562 # Tomato seed\n  examine: \"A tomato seed - plant in an allotment.\"\n- id: 7564 # Balloon toad\n  examine: \"An inflated toad tied to a rock like a balloon.\"\n- id: 7565 # Balloon toad\n  examine: \"An inflated toad tied to a rock like a balloon.\"\n- id: 7566 # Raw jubbly\n  examine: \"The uncooked meat of a Jubbly bird.\"\n- id: 7568 # Cooked jubbly\n  examine: \"Lovely Jubbly!\"\n- id: 7570 # Burnt jubbly\n  examine: \"The burnt meat of a Jubbly bird.\"\n- id: 7572 # Red banana\n  examine: \"Like a banana only redder.\"\n- id: 7573 # Tchiki monkey nuts\n  examine: \"Like Monkey Nuts only Tchikier.\"\n- id: 7574 # Sliced red banana\n  examine: \"Perfect for stuffing snakes.\"\n- id: 7575 # Tchiki nut paste\n  examine: \"Mixing this with jam would just be wrong.\"\n- id: 7576 # Snake corpse\n  examine: \"Like a snake only not alive.\"\n- id: 7577 # Raw stuffed snake\n  examine: \"This snake is stuffed right up.\"\n- id: 7578 # Odd stuffed snake\n  examine: \"Is this really what you wanted to do?\"\n- id: 7579 # Stuffed snake\n  examine: \"Fit for a Monkey King.\"\n- id: 7580 # Snake over-cooked\n  examine: \"It's a burnt snake.\"\n- id: 7581 # Overgrown hellcat\n  examine: \"Your hellish pet cat!!\"\n- id: 7582 # Hell cat\n  examine: \"Your hellish pet cat!!\"\n- id: 7583 # Hell-kitten\n  examine: \"Your hellish pet cat!!\"\n- id: 7584 # Lazy hell cat\n  examine: \"Your hellish pet cat!!\"\n- id: 7585 # Wily hellcat\n  examine: \"Your hellish pet cat!!\"\n- id: 7587 # Coffin\n  examine: \"Filled with items. Like a bank, but spookier!\"\n- id: 7588 # Coffin\n  examine: \"Filled with items. Like a bank, but spookier!\"\n- id: 7589 # Coffin\n  examine: \"Filled with items. Like a bank, but spookier!\"\n- id: 7590 # Coffin\n  examine: \"Filled with items. Like a bank, but spookier!\"\n- id: 7591 # Coffin\n  examine: \"Filled with items. Like a bank, but spookier!\"\n- id: 7592 # Zombie shirt\n  examine: \"Aside from the braaaains on the lapel, it's still quite good.\"\n- id: 7593 # Zombie trousers\n  examine: \"Good for a shamble about town.\"\n- id: 7594 # Zombie mask\n  examine: \"I look 40,000 years old in this...\"\n- id: 7595 # Zombie gloves\n  examine: \"Smells pretty funky.\"\n- id: 7596 # Zombie boots\n  examine: \"Thrilling.\"\n- id: 7622 # Bucket of rubble\n  examine: \"A bucket partially filled with rubble.\"\n- id: 7624 # Bucket of rubble\n  examine: \"A bucket almost full of rubble.\"\n- id: 7626 # Bucket of rubble\n  examine: \"A bucket totally filled with rubble.\"\n- id: 7628 # Plaster fragment\n  examine: \"A fragment of plaster with some impressions on it.\"\n- id: 7629 # Dusty scroll\n  examine: \"An ancient tattered scroll.\"\n- id: 7630 # Crate\n  examine: \"An old and musty looking crate.\"\n- id: 7632 # Temple library key\n  examine: \"A key for the Temple Library.\"\n- id: 7633 # Ancient book\n  examine: \"A book about seven warrior priests, written about 200 years ago.\"\n- id: 7634 # Battered tome\n  examine: \"An ancient history book.\"\n- id: 7635 # Leather book\n  examine: \"An ancient leather-bound tome.\"\n- id: 7636 # Rod dust\n  examine: \"Rod of Ivandis dust.\"\n- id: 7637 # Silvthrill rod\n  examine: \"A silvery rod of mithril and silver with a sapphire on the top.\"\n- id: 7638 # Silvthrill rod\n  examine: \"An enchanted rod of mithril and silver with a sapphire on the top.\"\n- id: 7649 # Rod clay mould\n  examine: \"Rod of Ivandis mould.\"\n- id: 7650 # Silver dust\n  examine: \"It's ground up silver.\"\n- id: 7660 # Guthix balance(4)\n  examine: \"A potion of harralander, red spiders eggs, garlic and silver dust.\"\n- id: 7662 # Guthix balance(3)\n  examine: \"A potion of harralander, red spiders eggs, garlic and silver dust.\"\n- id: 7664 # Guthix balance(2)\n  examine: \"A potion of harralander, red spiders eggs, garlic and silver dust.\"\n- id: 7666 # Guthix balance(1)\n  examine: \"A potion of harralander, red spiders eggs, garlic and silver dust.\"\n- id: 7668 # Gadderhammer\n  examine: \"A specially crafted hammer with strange markings on it.\"\n- id: 7671 # Boxing gloves\n  examine: \"I think they look a bit silly.\"\n- id: 7673 # Boxing gloves\n  examine: \"I think they look a bit silly.\"\n- id: 7675 # Wooden sword\n  examine: \"A less-than razor sharp sword.\"\n- id: 7676 # Wooden shield\n  examine: \"A less-than strong shield.\"\n- id: 7678 # Prize key\n  examine: \"You can use this to open the prize chest!\"\n- id: 7679 # Pugel\n  examine: \"A good tool for bashing someone.\"\n- id: 7681 # Game book\n  examine: \"Party Pete's Bumper Book Of Games\"\n- id: 7688 # Kettle\n  examine: \"The kettle is empty\"\n- id: 7690 # Full kettle\n  examine: \"It's full of cold water.\"\n- id: 7691 # Hot kettle\n  examine: \"It's full of boiling water.\"\n- id: 7692 # Pot of tea (4)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7694 # Pot of tea (3)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7696 # Pot of tea (2)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7698 # Pot of tea (1)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7700 # Teapot with leaves\n  examine: \"Add boiling water to make a tea.\"\n- id: 7702 # Teapot\n  examine: \"This teapot is empty.\"\n- id: 7704 # Pot of tea (4)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7706 # Pot of tea (3)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7708 # Pot of tea (2)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7710 # Pot of tea (1)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7712 # Teapot with leaves\n  examine: \"Add boiling water to make a tea.\"\n- id: 7714 # Teapot\n  examine: \"This teapot is empty.\"\n- id: 7716 # Pot of tea (4)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7718 # Pot of tea (3)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7720 # Pot of tea (2)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7722 # Pot of tea (1)\n  examine: \"I'd really like a nice cup of tea.\"\n- id: 7724 # Teapot with leaves\n  examine: \"Add boiling water to make a tea.\"\n- id: 7726 # Teapot\n  examine: \"This teapot is empty.\"\n- id: 7728 # Empty cup\n  examine: \"An empty cup.\"\n- id: 7730 # Cup of tea\n  examine: \"A nice cup of nettle tea.\"\n- id: 7731 # Cup of tea\n  examine: \"A milky cup of nettle tea.\"\n- id: 7732 # Porcelain cup\n  examine: \"A porcelain cup.\"\n- id: 7733 # Cup of tea\n  examine: \"Some nettle tea in a porcelain cup.\"\n- id: 7734 # Cup of tea\n  examine: \"Some milky nettle tea in a porcelain cup.\"\n- id: 7735 # Porcelain cup\n  examine: \"A porcelain cup.\"\n- id: 7736 # Cup of tea\n  examine: \"Some nettle tea in a porcelain cup.\"\n- id: 7737 # Cup of tea\n  examine: \"Some milky nettle tea in a porcelain cup.\"\n- id: 7738 # Tea leaves\n  examine: \"Mmm, how about a nice cup of tea?\"\n- id: 7740 # Beer\n  examine: \"A glass of frothy ale.\"\n- id: 7742 # Beer glass\n  examine: \"I need to fill this with beer.\"\n- id: 7744 # Asgarnian ale\n  examine: \"Probably the finest ale in Asgarnia.\"\n- id: 7746 # Greenman's ale\n  examine: \"A glass of frothy ale.\"\n- id: 7748 # Dragon bitter\n  examine: \"A glass of bitter.\"\n- id: 7750 # Moonlight mead\n  examine: \"A foul smelling brew.\"\n- id: 7752 # Cider\n  examine: \"A glass of cider.\"\n- id: 7754 # Chef's delight\n  examine: \"A fruity, full-bodied ale.\"\n- id: 7759 # Toy soldier\n  examine: \"Nice bit of crafting!\"\n- id: 7761 # Toy soldier (wound)\n  examine: \"Nice bit of crafting!\"\n- id: 7763 # Toy doll\n  examine: \"Nice bit of crafting!\"\n- id: 7765 # Toy doll (wound)\n  examine: \"Nice bit of crafting!\"\n- id: 7767 # Toy mouse\n  examine: \"Nice bit of crafting!\"\n- id: 7769 # Toy mouse (wound)\n  examine: \"Nice bit of crafting!\"\n- id: 7771 # Toy cat\n  examine: \"Nice bit of crafting!\"\n- id: 7773 # Branch\n  examine: \"Leafless bush branch.\"\n- id: 7774 # Reward token\n  examine: \"Yellow reward token earned from helping people journey to Burgh de Rott.\"\n- id: 7775 # Reward token\n  examine: \"Red reward token earned from helping people journey to Burgh de Rott.\"\n- id: 7776 # Reward token\n  examine: \"Blue reward token earned from helping people journey to Burgh de Rott.\"\n- id: 7777 # Long vine\n  examine: \"A long section of vine made up of lots of shorter sections.\"\n- id: 7778 # Short vine\n  examine: \"A short section of vines.\"\n- id: 7779 # Fishing tome\n  examine: \"A tome of learning which focuses on Fishing.\"\n- id: 7780 # Fishing tome\n  examine: \"A tome of learning which focuses on Fishing.\"\n- id: 7781 # Fishing tome\n  examine: \"A tome of learning which focuses on Fishing.\"\n- id: 7782 # Agility tome\n  examine: \"A tome of learning which focuses on Agility.\"\n- id: 7783 # Agility tome\n  examine: \"A tome of learning which focuses on Agility.\"\n- id: 7784 # Agility tome\n  examine: \"A tome of learning which focuses on Agility.\"\n- id: 7785 # Thieving tome\n  examine: \"A tome of learning which focuses on Thieving.\"\n- id: 7786 # Thieving tome\n  examine: \"A tome of learning which focuses on Thieving.\"\n- id: 7787 # Thieving tome\n  examine: \"A tome of learning which focuses on Thieving.\"\n- id: 7788 # Slayer tome\n  examine: \"A tome of learning which focuses on the Slayer skill.\"\n- id: 7789 # Slayer tome\n  examine: \"A tome of learning which focuses on the Slayer skill.\"\n- id: 7790 # Slayer tome\n  examine: \"A tome of learning which focuses on the Slayer skill.\"\n- id: 7791 # Mining tome\n  examine: \"A tome of learning which focuses on the Mining skill.\"\n- id: 7792 # Mining tome\n  examine: \"A tome of learning which focuses on the Mining skill.\"\n- id: 7793 # Mining tome\n  examine: \"A tome of learning which focuses on the Mining skill.\"\n- id: 7794 # Firemaking tome\n  examine: \"A tome of learning which focuses on the Firemaking skill.\"\n- id: 7795 # Firemaking tome\n  examine: \"A tome of learning which focuses on the Firemaking skill.\"\n- id: 7796 # Firemaking tome\n  examine: \"A tome of learning which focuses on the Firemaking skill.\"\n- id: 7797 # Woodcutting tome\n  examine: \"A tome of learning which focuses on the Woodcutting skill.\"\n- id: 7798 # Woodcutting tome\n  examine: \"A tome of learning which focuses on the Woodcutting skill.\"\n- id: 7799 # Woodcutting tome\n  examine: \"A tome of learning which focuses on the Woodcutting skill.\"\n- id: 7800 # Snail shell\n  examine: \"A shell from a giant snail.\"\n- id: 7801 # Snake hide\n  examine: \"Scaly but not slimy!\"\n- id: 7803 # Yin yang amulet\n  examine: \"A non-magical copy of the make-over mage's amulet.\"\n- id: 7806 # Anger sword\n  examine: \"A heavy duty sword.\"\n- id: 7807 # Anger battleaxe\n  examine: \"A heavy duty axe.\"\n- id: 7808 # Anger mace\n  examine: \"A heavy duty mace.\"\n- id: 7809 # Anger spear\n  examine: \"A heavy duty spear.\"\n- id: 7810 # Jug of vinegar\n  examine: \"This wine clearly did not age well.\"\n- id: 7811 # Pot of vinegar\n  examine: \"Well, this pot is certainly full of vinegar and no mistake.\"\n- id: 7812 # Goblin skull\n  examine: \"This needs a good polish.\"\n- id: 7813 # Bone in vinegar\n  examine: \"There is a goblin bone in here.\"\n- id: 7814 # Goblin skull\n  examine: \"This bone belongs in a museum!\"\n- id: 7815 # Bear ribs\n  examine: \"This needs a good polish.\"\n- id: 7816 # Bone in vinegar\n  examine: \"There is a bear bone in here.\"\n- id: 7817 # Bear ribs\n  examine: \"This bone belongs in a museum!\"\n- id: 7818 # Ram skull\n  examine: \"This needs a good polish.\"\n- id: 7819 # Bone in vinegar\n  examine: \"There is a ram bone in here.\"\n- id: 7820 # Ram skull\n  examine: \"This bone belongs in a museum!\"\n- id: 7821 # Unicorn bone\n  examine: \"This needs a good polish.\"\n- id: 7822 # Bone in vinegar\n  examine: \"There is a unicorn bone in here.\"\n- id: 7823 # Unicorn bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7824 # Giant rat bone\n  examine: \"This needs a good polish.\"\n- id: 7825 # Bone in vinegar\n  examine: \"There is a giant rat bone in here.\"\n- id: 7826 # Giant rat bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7827 # Giant bat wing\n  examine: \"This needs a good polish.\"\n- id: 7828 # Bone in vinegar\n  examine: \"There is a giant bat bone in here.\"\n- id: 7829 # Giant bat wing\n  examine: \"This bone belongs in a museum!\"\n- id: 7830 # Wolf bone\n  examine: \"This needs a good polish.\"\n- id: 7831 # Bone in vinegar\n  examine: \"There is a wolf bone in here.\"\n- id: 7832 # Wolf bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7833 # Bat wing\n  examine: \"This needs a good polish.\"\n- id: 7834 # Bone in vinegar\n  examine: \"There is a bat bone in here.\"\n- id: 7835 # Bat wing\n  examine: \"This bone belongs in a museum!\"\n- id: 7836 # Rat bone\n  examine: \"This needs a good polish.\"\n- id: 7837 # Bone in vinegar\n  examine: \"There is a rat bone in here.\"\n- id: 7838 # Rat bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7839 # Baby dragon bone\n  examine: \"This needs a good polish.\"\n- id: 7840 # Bone in vinegar\n  examine: \"There is a baby blue dragon bone in here.\"\n- id: 7841 # Baby dragon bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7842 # Ogre ribs\n  examine: \"This needs a good polish.\"\n- id: 7843 # Bone in vinegar\n  examine: \"There is an ogre bone in here.\"\n- id: 7844 # Ogre ribs\n  examine: \"This bone belongs in a museum!\"\n- id: 7845 # Jogre bone\n  examine: \"This needs a good polish.\"\n- id: 7846 # Bone in vinegar\n  examine: \"There is a jogre bone in here.\"\n- id: 7847 # Jogre bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7848 # Zogre bone\n  examine: \"This needs a good polish.\"\n- id: 7849 # Bone in vinegar\n  examine: \"There is a zogre bone in here.\"\n- id: 7850 # Zogre bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7851 # Mogre bone\n  examine: \"This needs a good polish.\"\n- id: 7852 # Bone in vinegar\n  examine: \"There is a mogre bone in here.\"\n- id: 7853 # Mogre bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7854 # Monkey paw\n  examine: \"This needs a good polish.\"\n- id: 7855 # Bone in vinegar\n  examine: \"There is a monkey bone in here.\"\n- id: 7856 # Monkey paw\n  examine: \"This bone belongs in a museum!\"\n- id: 7857 # Dagannoth ribs\n  examine: \"This needs a good polish.\"\n- id: 7858 # Bone in vinegar\n  examine: \"There is a Dagannoth bone in here.\"\n- id: 7859 # Dagannoth ribs\n  examine: \"This bone belongs in a museum!\"\n- id: 7860 # Snake spine\n  examine: \"This needs a good polish.\"\n- id: 7861 # Bone in vinegar\n  examine: \"There is a snake bone in here.\"\n- id: 7862 # Snake spine\n  examine: \"This bone belongs in a museum!\"\n- id: 7863 # Zombie bone\n  examine: \"This needs a good polish.\"\n- id: 7864 # Bone in vinegar\n  examine: \"There is a zombie bone in here.\"\n- id: 7865 # Zombie bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7866 # Werewolf bone\n  examine: \"This needs a good polish.\"\n- id: 7867 # Bone in vinegar\n  examine: \"There is a werewolf bone in here.\"\n- id: 7868 # Werewolf bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7869 # Moss giant bone\n  examine: \"This needs a good polish.\"\n- id: 7870 # Bone in vinegar\n  examine: \"A Moss Giant's bone\"\n- id: 7871 # Moss giant bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7872 # Fire giant bone\n  examine: \"This needs a good polish.\"\n- id: 7873 # Bone in vinegar\n  examine: \"There is a fire giant bone in here.\"\n- id: 7874 # Fire giant bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7875 # Ice giant ribs\n  examine: \"This needs a good polish.\"\n- id: 7876 # Bone in vinegar\n  examine: \"There is an ice giant bone in here.\"\n- id: 7877 # Ice giant ribs\n  examine: \"This bone belongs in a museum!\"\n- id: 7878 # Terrorbird wing\n  examine: \"This needs a good polish.\"\n- id: 7879 # Bone in vinegar\n  examine: \"There is a terrorbird bone in here.\"\n- id: 7880 # Terrorbird wing\n  examine: \"This bone belongs in a museum!\"\n- id: 7881 # Ghoul bone\n  examine: \"This needs a good polish.\"\n- id: 7882 # Bone in vinegar\n  examine: \"There is a ghoul bone in here.\"\n- id: 7883 # Ghoul bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7884 # Troll bone\n  examine: \"This needs a good polish.\"\n- id: 7885 # Bone in vinegar\n  examine: \"There is a troll bone in here.\"\n- id: 7886 # Troll bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7887 # Seagull wing\n  examine: \"This needs a good polish.\"\n- id: 7888 # Bone in vinegar\n  examine: \"There is a seagull bone in here.\"\n- id: 7889 # Seagull wing\n  examine: \"This bone belongs in a museum!\"\n- id: 7890 # Undead cow ribs\n  examine: \"This needs a good polish.\"\n- id: 7891 # Bone in vinegar\n  examine: \"There is an undead cow bone in here.\"\n- id: 7892 # Undead cow ribs\n  examine: \"This bone belongs in a museum!\"\n- id: 7893 # Experiment bone\n  examine: \"This needs a good polish.\"\n- id: 7894 # Bone in vinegar\n  examine: \"There is an experiment bone in here.\"\n- id: 7895 # Experiment bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7896 # Rabbit bone\n  examine: \"This needs a good polish.\"\n- id: 7897 # Bone in vinegar\n  examine: \"There is a rabbit bone in here.\"\n- id: 7898 # Rabbit bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7899 # Basilisk bone\n  examine: \"This needs a good polish.\"\n- id: 7900 # Bone in vinegar\n  examine: \"There is a basilisk bone in here.\"\n- id: 7901 # Basilisk bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7902 # Desert lizard bone\n  examine: \"This needs a good polish.\"\n- id: 7903 # Bone in vinegar\n  examine: \"There is a desert lizard bone in here.\"\n- id: 7904 # Desert lizard bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7905 # Cave goblin skull\n  examine: \"This needs a good polish.\"\n- id: 7906 # Bone in vinegar\n  examine: \"There is a cave goblin bone in here.\"\n- id: 7907 # Cave goblin skull\n  examine: \"This bone belongs in a museum!\"\n- id: 7908 # Big frog leg\n  examine: \"This needs a good polish.\"\n- id: 7909 # Bone in vinegar\n  examine: \"There is a big frog bone in here.\"\n- id: 7910 # Big frog leg\n  examine: \"This bone belongs in a museum!\"\n- id: 7911 # Vulture wing\n  examine: \"This needs a good polish.\"\n- id: 7912 # Bone in vinegar\n  examine: \"There is a vulture bone in here.\"\n- id: 7913 # Vulture wing\n  examine: \"This bone belongs in a museum!\"\n- id: 7914 # Jackal bone\n  examine: \"This needs a good polish.\"\n- id: 7915 # Bone in vinegar\n  examine: \"There is a jackal bone in here.\"\n- id: 7916 # Jackal bone\n  examine: \"This bone belongs in a museum!\"\n- id: 7917 # Ram skull helm\n  examine: \"Makes me feel baaad to the bone.\"\n- id: 7918 # Bonesack\n  examine: \"The Bonesack is a little old item that protects like leather.\"\n- id: 7919 # Bottle of wine\n  examine: \"A very good vintage.\"\n- id: 7921 # Empty wine bottle\n  examine: \"This one has clearly been taken down and passed around.\"\n- id: 7922 # Al kharid flyer\n  examine: \"The money off voucher has expired.\"\n- id: 7927 # Easter ring\n  examine: \"A ring given to you by the Easter Bunny.\"\n- id: 7928 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7929 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7930 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7931 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7932 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7933 # Easter egg\n  examine: \"\\\"Best before 1st May 2014.\\\"\"\n- id: 7934 # Field ration\n  examine: \"A field ration to help your wounds go away.\"\n- id: 7936 # Pure essence\n  examine: \"An uncharged Rune Stone of extra capability.\"\n- id: 7939 # Tortoise shell\n  examine: \"A word in your shell-like.\"\n- id: 7941 # Iron sheet\n  examine: \"A sturdy sheet of iron.\"\n- id: 7942 # Fresh monkfish\n  examine: \"Freshly caught. Needs cooking.\"\n- id: 7943 # Fresh monkfish\n  examine: \"Freshly caught and cooked - perfect for storing. Not so good for eating.\"\n- id: 7944 # Raw monkfish\n  examine: \"I should try cooking this.\"\n- id: 7946 # Monkfish\n  examine: \"A tasty fish.\"\n- id: 7948 # Burnt monkfish\n  examine: \"Maybe a little less heat next time.\"\n- id: 7950 # Bone seeds\n  examine: \"A highly portable army of skeletal magi.\"\n- id: 7951 # Herman's book\n  examine: \"A book taken from the desk of Herman Caranos.\"\n- id: 7952 # Axe handle\n  examine: \"Useless without the head.\"\n- id: 7954 # Burnt shrimp\n  examine: \"Oops!\"\n- id: 7956 # Casket\n  examine: \"I hope there's treasure in it.\"\n- id: 7957 # White apron\n  examine: \"A mostly clean apron.\"\n- id: 7958 # Mining prop\n  examine: \"A prop for holding up a tunnel roof.\"\n- id: 7959 # Heavy box\n  examine: \"A box full of stolen Etceterian items.\"\n- id: 7960 # Empty box\n  examine: \"It says 'To the dungeons' on the side.\"\n- id: 7961 # Burnt diary\n  examine: \"A diary with one page.\"\n- id: 7962 # Burnt diary\n  examine: \"A diary with two pages.\"\n- id: 7963 # Burnt diary\n  examine: \"A diary with three pages.\"\n- id: 7964 # Burnt diary\n  examine: \"A diary with four pages.\"\n- id: 7965 # Burnt diary\n  examine: \"A diary with five pages.\"\n- id: 7966 # Letter\n  examine: \"Sigrid's letter to Vargas.\"\n- id: 7967 # Engine\n  examine: \"A dwarf-made coal engine. It looks very sturdy.\"\n- id: 7968 # Scroll\n  examine: \"An official-looking scroll.\"\n- id: 7969 # Pulley beam\n  examine: \"A beam with a pulley attached.\"\n- id: 7970 # Long pulley beam\n  examine: \"A long beam with a pulley attached.\"\n- id: 7971 # Longer pulley beam\n  examine: \"A very long beam with a pulley attached.\"\n- id: 7972 # Lift manual\n  examine: \"The manual for an AMCE Lift-In-A-Box™.\"\n- id: 7973 # Beam\n  examine: \"A wooden beam.\"\n- id: 7975 # Crawling hand\n  examine: \"I should get it stuffed!\"\n- id: 7976 # Cockatrice head\n  examine: \"I should get it stuffed!\"\n- id: 7977 # Basilisk head\n  examine: \"I should get it stuffed!\"\n- id: 7978 # Kurask head\n  examine: \"I should get it stuffed!\"\n- id: 7979 # Abyssal head\n  examine: \"I should get it stuffed!\"\n- id: 7980 # Kbd heads\n  examine: \"I should get them stuffed!\"\n- id: 7981 # Kq head\n  examine: \"I should get it stuffed!\"\n- id: 7989 # Big bass\n  examine: \"Whopper! I should get this stuffed!\"\n- id: 7991 # Big swordfish\n  examine: \"Whopper! I should get this stuffed!\"\n- id: 7993 # Big shark\n  examine: \"It's a monster! I should get this stuffed!\"\n- id: 7995 # Arthur portrait\n  examine: \"A portrait of King Arthur.\"\n- id: 7996 # Elena portrait\n  examine: \"A portrait of Elena.\"\n- id: 7997 # Keldagrim portrait\n  examine: \"A painting of the statue of King Alvis of Keldagrim.\"\n- id: 7998 # Misc. portrait\n  examine: \"A portrait of Prince Brand and Princess Astrid of Miscellania.\"\n- id: 7999 # Desert painting\n  examine: \"The searing Kharid Desert.\"\n- id: 8000 # Isafdar painting\n  examine: \"The exotic land of the Elves.\"\n- id: 8001 # Karamja painting\n  examine: \"The tropical coast of Karamja.\"\n- id: 8002 # Lumbridge painting\n  examine: \"Oxtable's famous painting of the Lumbridge water mill.\"\n- id: 8003 # Morytania painting\n  examine: \"A painting of the spooky forests of Morytania.\"\n- id: 8004 # Small map\n  examine: \"A small map of Gielinor.\"\n- id: 8005 # Medium map\n  examine: \"A medium map of Gielinor.\"\n- id: 8006 # Large map\n  examine: \"A large map of Gielinor.\"\n- id: 8007 # Varrock teleport\n  examine: \"A teleport to Varrock.\"\n- id: 8008 # Lumbridge teleport\n  examine: \"A teleport to Lumbridge.\"\n- id: 8009 # Falador teleport\n  examine: \"A teleport to Falador.\"\n- id: 8010 # Camelot teleport\n  examine: \"A teleport to Camelot.\"\n- id: 8011 # Ardougne teleport\n  examine: \"A teleport to East Ardougne.\"\n- id: 8013 # Teleport to house\n  examine: \"A teleport to one's own house.\"\n- id: 8014 # Bones to bananas\n  examine: \"A tablet containing a magic spell.\"\n- id: 8015 # Bones to peaches\n  examine: \"A tablet containing a magic spell.\"\n- id: 8019 # Enchant diamond\n  examine: \"A tablet containing a magic spell.\"\n- id: 8021 # Enchant onyx\n  examine: \"A tablet containing a magic spell.\"\n- id: 8261 # Cockatrice head\n  examine: \"I should get it stuffed!\"\n- id: 8262 # Basilisk head\n  examine: \"I should get it stuffed!\"\n- id: 8263 # Kurask head\n  examine: \"I should get it stuffed!\"\n- id: 8264 # Abyssal head\n  examine: \"I should get it stuffed!\"\n- id: 8265 # Kbd heads\n  examine: \"I should get them stuffed!\"\n- id: 8266 # Kq head\n  examine: \"I should get it stuffed!\"\n- id: 8279 # Silverlight\n  examine: \"The magical sword 'Silverlight'.\"\n- id: 8280 # Excalibur\n  examine: \"This used to belong to King Arthur.\"\n- id: 8281 # Darklight\n  examine: \"The magical sword 'Silverlight', enhanced with the blood of Agrith-Naar.\"\n- id: 8417 # Bagged dead tree\n  examine: \"You can plant this in your garden.\"\n- id: 8419 # Bagged nice tree\n  examine: \"You can plant this in your garden.\"\n- id: 8421 # Bagged oak tree\n  examine: \"You can plant this in your garden.\"\n- id: 8423 # Bagged willow tree\n  examine: \"You can plant this in your garden.\"\n- id: 8425 # Bagged maple tree\n  examine: \"You can plant this in your garden.\"\n- id: 8427 # Bagged yew tree\n  examine: \"You can plant this in your garden.\"\n- id: 8429 # Bagged magic tree\n  examine: \"You can plant this in your garden.\"\n- id: 8431 # Bagged plant 1\n  examine: \"You can plant this in your garden.\"\n- id: 8433 # Bagged plant 2\n  examine: \"You can plant this in your garden.\"\n- id: 8435 # Bagged plant 3\n  examine: \"You can plant this in your garden.\"\n- id: 8437 # Thorny hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8439 # Nice hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8441 # Small box hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8443 # Topiary hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8445 # Fancy hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8447 # Tall fancy hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8449 # Tall box hedge\n  examine: \"You can plant this in your garden.\"\n- id: 8451 # Bagged flower\n  examine: \"You can plant this in your garden.\"\n- id: 8453 # Bagged daffodils\n  examine: \"You can plant this in your garden.\"\n- id: 8455 # Bagged bluebells\n  examine: \"You can plant this in your garden.\"\n- id: 8457 # Bagged sunflower\n  examine: \"You can plant this in your garden.\"\n- id: 8459 # Bagged marigolds\n  examine: \"You can plant this in your garden.\"\n- id: 8461 # Bagged roses\n  examine: \"You can plant this in your garden.\"\n- id: 8463 # Construction guide\n  examine: \"How to build a house.\"\n- id: 8464 # Rune heraldic helm\n  examine: \"The colours represent the symbol of Arrav.\"\n- id: 8466 # Rune heraldic helm\n  examine: \"The colours represent Asgarnia.\"\n- id: 8468 # Rune heraldic helm\n  examine: \"The colours represent the Dorgeshuun brooch.\"\n- id: 8470 # Rune heraldic helm\n  examine: \"The colours represent a dragon.\"\n- id: 8472 # Rune heraldic helm\n  examine: \"The colours represent a fairy.\"\n- id: 8474 # Rune heraldic helm\n  examine: \"The colours represent Guthix.\"\n- id: 8476 # Rune heraldic helm\n  examine: \"The colours represent the HAM cult.\"\n- id: 8478 # Rune heraldic helm\n  examine: \"The colours represent the mythical 'horse'.\"\n- id: 8480 # Rune heraldic helm\n  examine: \"The colours represent a Jogre.\"\n- id: 8482 # Rune heraldic helm\n  examine: \"The colours represent Kandarin.\"\n- id: 8484 # Rune heraldic helm\n  examine: \"The colours represent Misthalin.\"\n- id: 8486 # Rune heraldic helm\n  examine: \"The colours represent money.\"\n- id: 8488 # Rune heraldic helm\n  examine: \"The colours represent Saradomin.\"\n- id: 8490 # Rune heraldic helm\n  examine: \"The colours represent a skull.\"\n- id: 8492 # Rune heraldic helm\n  examine: \"The colours represent Varrock.\"\n- id: 8494 # Rune heraldic helm\n  examine: \"The colours represent Zamorak.\"\n- id: 8496 # Crude chair\n  examine: \"How does it all fit in there?\"\n- id: 8498 # Wooden chair\n  examine: \"How does it all fit in there?\"\n- id: 8500 # Rocking chair\n  examine: \"How does it all fit in there?\"\n- id: 8502 # Oak chair\n  examine: \"How does it all fit in there?\"\n- id: 8504 # Oak armchair\n  examine: \"How does it all fit in there?\"\n- id: 8506 # Teak armchair\n  examine: \"How does it all fit in there?\"\n- id: 8508 # Mahogany armchair\n  examine: \"How does it all fit in there?\"\n- id: 8510 # Bookcase\n  examine: \"How does it all fit in there?\"\n- id: 8512 # Oak bookcase\n  examine: \"How does it all fit in there?\"\n- id: 8516 # Beer barrel\n  examine: \"How does it all fit in there?\"\n- id: 8518 # Cider barrel\n  examine: \"How does it all fit in there?\"\n- id: 8520 # Asgarnian ale\n  examine: \"How does it all fit in there?\"\n- id: 8522 # Greenman's ale\n  examine: \"How does it all fit in there?\"\n- id: 8524 # Dragon bitter\n  examine: \"How does it all fit in there?\"\n- id: 8526 # Chef's delight\n  examine: \"How does it all fit in there?\"\n- id: 8528 # Kitchen table\n  examine: \"How does it all fit in there?\"\n- id: 8530 # Oak kitchen table\n  examine: \"How does it all fit in there?\"\n- id: 8532 # Teak kitchen table\n  examine: \"How does it all fit in there?\"\n- id: 8548 # Wood dining table\n  examine: \"How does it all fit in there?\"\n- id: 8550 # Oak dining table\n  examine: \"How does it all fit in there?\"\n- id: 8552 # Carved oak table\n  examine: \"How does it all fit in there?\"\n- id: 8554 # Teak table\n  examine: \"How does it all fit in there?\"\n- id: 8556 # Carved teak table\n  examine: \"How does it all fit in there?\"\n- id: 8558 # Mahogany table\n  examine: \"How does it all fit in there?\"\n- id: 8560 # Opulent table\n  examine: \"How does it all fit in there?\"\n- id: 8562 # Wooden bench\n  examine: \"How does it all fit in there?\"\n- id: 8564 # Oak bench\n  examine: \"How does it all fit in there?\"\n- id: 8566 # Carved oak bench\n  examine: \"How does it all fit in there?\"\n- id: 8568 # Teak dining bench\n  examine: \"How does it all fit in there?\"\n- id: 8570 # Carved teak bench\n  examine: \"How does it all fit in there?\"\n- id: 8572 # Mahogany bench\n  examine: \"How does it all fit in there?\"\n- id: 8574 # Gilded bench\n  examine: \"How does it all fit in there?\"\n- id: 8576 # Wooden bed\n  examine: \"How does it all fit in there?\"\n- id: 8578 # Oak bed\n  examine: \"How does it all fit in there?\"\n- id: 8580 # Large oak bed\n  examine: \"How does it all fit in there?\"\n- id: 8582 # Teak bed\n  examine: \"How does it all fit in there?\"\n- id: 8584 # Large teak bed\n  examine: \"How does it all fit in there?\"\n- id: 8586 # Four-poster bed\n  examine: \"How does it all fit in there?\"\n- id: 8588 # Gilded four-poster\n  examine: \"How does it all fit in there?\"\n- id: 8590 # Oak clock\n  examine: \"How does it all fit in there?\"\n- id: 8592 # Teak clock\n  examine: \"How does it all fit in there?\"\n- id: 8594 # Gilded clock\n  examine: \"How does it all fit in there?\"\n- id: 8596 # Shaving stand\n  examine: \"How does it all fit in there?\"\n- id: 8598 # Oak shaving stand\n  examine: \"How does it all fit in there?\"\n- id: 8600 # Oak dresser\n  examine: \"How does it all fit in there?\"\n- id: 8602 # Teak dresser\n  examine: \"How does it all fit in there?\"\n- id: 8604 # Fancy teak dresser\n  examine: \"How does it all fit in there?\"\n- id: 8606 # Mahogany dresser\n  examine: \"How does it all fit in there?\"\n- id: 8608 # Gilded dresser\n  examine: \"How does it all fit in there?\"\n- id: 8610 # Shoe box\n  examine: \"How does it all fit in there?\"\n- id: 8612 # Oak drawers\n  examine: \"How does it all fit in there?\"\n- id: 8614 # Oak wardrobe\n  examine: \"How does it all fit in there?\"\n- id: 8616 # Teak drawers\n  examine: \"How does it all fit in there?\"\n- id: 8618 # Teak wardrobe\n  examine: \"How does it all fit in there?\"\n- id: 8622 # Gilded wardrobe\n  examine: \"How does it all fit in there?\"\n- id: 8650 # Banner\n  examine: \"A banner with the symbol of Arrav.\"\n- id: 8652 # Banner\n  examine: \"A banner with the symbol of Asgarnia.\"\n- id: 8654 # Banner\n  examine: \"A banner with a picture of the Dorgeshuun brooch.\"\n- id: 8656 # Banner\n  examine: \"A banner with a picture of a dragon.\"\n- id: 8658 # Banner\n  examine: \"A banner with a picture of a fairy.\"\n- id: 8660 # Banner\n  examine: \"A banner with the symbol of Guthix.\"\n- id: 8662 # Banner\n  examine: \"A banner with the symbol of the HAM cult.\"\n- id: 8664 # Banner\n  examine: \"A banner with a picture of the mythical 'horse'.\"\n- id: 8666 # Banner\n  examine: \"A banner with a picture of a Jogre.\"\n- id: 8668 # Banner\n  examine: \"A banner with the symbol of Kandarin.\"\n- id: 8670 # Banner\n  examine: \"A banner with the symbol of Misthalin.\"\n- id: 8672 # Banner\n  examine: \"A banner with a picture of a money-bag.\"\n- id: 8674 # Banner\n  examine: \"A banner with the symbol of Saradomin.\"\n- id: 8676 # Banner\n  examine: \"A banner with a picture of a skull.\"\n- id: 8678 # Banner\n  examine: \"A banner with the symbol of Varrock.\"\n- id: 8680 # Banner\n  examine: \"A banner with the symbol of Zamorak.\"\n- id: 8682 # Steel heraldic helm\n  examine: \"The colours represent the symbol of Arrav.\"\n- id: 8684 # Steel heraldic helm\n  examine: \"The colours represent Asgarnia.\"\n- id: 8686 # Steel heraldic helm\n  examine: \"The colours represent the Dorgeshuun brooch.\"\n- id: 8688 # Steel heraldic helm\n  examine: \"The colours represent a dragon.\"\n- id: 8690 # Steel heraldic helm\n  examine: \"The colours represent a fairy.\"\n- id: 8692 # Steel heraldic helm\n  examine: \"The colours represent Guthix.\"\n- id: 8694 # Steel heraldic helm\n  examine: \"The colours represent the HAM cult.\"\n- id: 8696 # Steel heraldic helm\n  examine: \"The colours represent the mythical 'horse'.\"\n- id: 8698 # Steel heraldic helm\n  examine: \"The colours represent a Jogre.\"\n- id: 8700 # Steel heraldic helm\n  examine: \"The colours represent Kandarin.\"\n- id: 8702 # Steel heraldic helm\n  examine: \"The colours represent Misthalin.\"\n- id: 8704 # Steel heraldic helm\n  examine: \"The colours represent money.\"\n- id: 8706 # Steel heraldic helm\n  examine: \"The colours represent Saradomin.\"\n- id: 8708 # Steel heraldic helm\n  examine: \"The colours represent a skull.\"\n- id: 8710 # Steel heraldic helm\n  examine: \"The colours represent Varrock.\"\n- id: 8712 # Steel heraldic helm\n  examine: \"The colours represent Zamorak.\"\n- id: 8714 # Rune kiteshield\n  examine: \"A shield with the symbol of Arrav.\"\n- id: 8716 # Rune kiteshield\n  examine: \"A shield with the symbol of Asgarnia.\"\n- id: 8718 # Rune kiteshield\n  examine: \"A shield with a picture of the Dorgeshuun brooch.\"\n- id: 8720 # Rune kiteshield\n  examine: \"A shield with a picture of a dragon.\"\n- id: 8722 # Rune kiteshield\n  examine: \"A shield with a picture of a fairy.\"\n- id: 8724 # Rune kiteshield\n  examine: \"A shield with the symbol of Guthix.\"\n- id: 8726 # Rune kiteshield\n  examine: \"A shield with the symbol of the HAM cult.\"\n- id: 8728 # Rune kiteshield\n  examine: \"A shield with a picture of the mythical 'horse'.\"\n- id: 8730 # Rune kiteshield\n  examine: \"A shield with a picture of a Jogre.\"\n- id: 8732 # Rune kiteshield\n  examine: \"A shield with the symbol of Kandarin.\"\n- id: 8734 # Rune kiteshield\n  examine: \"A shield with the symbol of Misthalin.\"\n- id: 8736 # Rune kiteshield\n  examine: \"A shield with a picture of a money-bag.\"\n- id: 8738 # Rune kiteshield\n  examine: \"A shield with the symbol of Saradomin.\"\n- id: 8740 # Rune kiteshield\n  examine: \"A shield with a picture of a skull.\"\n- id: 8742 # Rune kiteshield\n  examine: \"A shield with the symbol of Varrock.\"\n- id: 8744 # Rune kiteshield\n  examine: \"A shield with the symbol of Zamorak.\"\n- id: 8746 # Steel kiteshield\n  examine: \"A shield with the symbol of Arrav.\"\n- id: 8748 # Steel kiteshield\n  examine: \"A shield with the symbol of Asgarnia.\"\n- id: 8750 # Steel kiteshield\n  examine: \"A shield with a picture of the Dorgeshuun brooch.\"\n- id: 8752 # Steel kiteshield\n  examine: \"A shield with a picture of a dragon.\"\n- id: 8754 # Steel kiteshield\n  examine: \"A shield with a picture of a fairy.\"\n- id: 8756 # Steel kiteshield\n  examine: \"A shield with the symbol of Guthix.\"\n- id: 8758 # Steel kiteshield\n  examine: \"A shield with the symbol of the HAM cult.\"\n- id: 8760 # Steel kiteshield\n  examine: \"A shield with a picture of the mythical 'horse'.\"\n- id: 8762 # Steel kiteshield\n  examine: \"A shield with a picture of a Jogre.\"\n- id: 8764 # Steel kiteshield\n  examine: \"A shield with the symbol of Kandarin.\"\n- id: 8766 # Steel kiteshield\n  examine: \"A shield with the symbol of Misthalin.\"\n- id: 8768 # Steel kiteshield\n  examine: \"A shield with a picture of a money-bag.\"\n- id: 8770 # Steel kiteshield\n  examine: \"A shield with the symbol of Saradomin.\"\n- id: 8772 # Steel kiteshield\n  examine: \"A shield with a picture of a skull.\"\n- id: 8774 # Steel kiteshield\n  examine: \"A shield with the symbol of Varrock.\"\n- id: 8776 # Steel kiteshield\n  examine: \"A shield with the symbol of Zamorak.\"\n- id: 8778 # Oak plank\n  examine: \"A plank of sturdy oak.\"\n- id: 8780 # Teak plank\n  examine: \"A plank of fine teak.\"\n- id: 8782 # Mahogany plank\n  examine: \"A plank of expensive mahogany.\"\n- id: 8784 # Gold leaf\n  examine: \"A very delicate sheet of gold.\"\n- id: 8786 # Marble block\n  examine: \"A beautifully carved marble block.\"\n- id: 8788 # Magic stone\n  examine: \"A magic stone to make high-level furniture.\"\n- id: 8790 # Bolt of cloth\n  examine: \"A bolt of ordinary cloth.\"\n- id: 8792 # Clockwork\n  examine: \"A clockwork mechanism.\"\n- id: 8794 # Saw\n  examine: \"Good for cutting wood.\"\n- id: 8837 # Timber beam\n  examine: \"A hefty beam of timber, perfect for building temples.\"\n- id: 8839 # Void knight top\n  examine: \"Torso armour from the order of the Void Knights.\"\n- id: 8840 # Void knight robe\n  examine: \"Leg armour from the order of the Void Knights.\"\n- id: 8841 # Void knight mace\n  examine: \"A mace used by the order of the Void Knights.\"\n- id: 8842 # Void knight gloves\n  examine: \"Gloves as used by the order of the Void Knights.\"\n- id: 8844 # Bronze defender\n  examine: \"A defensive weapon.\"\n- id: 8845 # Iron defender\n  examine: \"A defensive weapon.\"\n- id: 8846 # Steel defender\n  examine: \"A defensive weapon.\"\n- id: 8847 # Black defender\n  examine: \"A defensive weapon.\"\n- id: 8848 # Mithril defender\n  examine: \"A defensive weapon.\"\n- id: 8849 # Adamant defender\n  examine: \"A defensive weapon.\"\n- id: 8850 # Rune defender\n  examine: \"A defensive weapon.\"\n- id: 8851 # Warrior guild token\n  examine: \"Warrior Guild Token.\"\n- id: 8856 # Defensive shield\n  examine: \"Large, round, heavy shield.\"\n- id: 8858 # 18lb shot\n  examine: \"Just landed 18lb shot.\"\n- id: 8859 # 22lb shot\n  examine: \"Just landed 22lb shot.\"\n- id: 8860 # One barrel\n  examine: \"To put on your head.\"\n- id: 8861 # Two barrels\n  examine: \"To put on your head.\"\n- id: 8862 # Three barrels\n  examine: \"To put on your head.\"\n- id: 8863 # Four barrels\n  examine: \"To put on your head.\"\n- id: 8864 # Five barrels\n  examine: \"To put on your head.\"\n- id: 8865 # Ground ashes\n  examine: \"A heap of finely ground ashes.\"\n- id: 8866 # Steel key\n  examine: \"You stole this key from a HAM guard.\"\n- id: 8867 # Bronze key\n  examine: \"You stole this key from a HAM guard.\"\n- id: 8868 # Silver key\n  examine: \"You stole this key from a HAM guard.\"\n- id: 8869 # Iron key\n  examine: \"You stole this key from a HAM guard.\"\n- id: 8870 # Zanik\n  examine: \"She's dead, but the mark on her forehead is glowing brightly.\"\n- id: 8871 # Crate with zanik\n  examine: \"It's got Zanik in it.\"\n- id: 8872 # Bone dagger\n  examine: \"A powerful dagger.\"\n- id: 8874 # Bone dagger (p)\n  examine: \"A powerful dagger.\"\n- id: 8876 # Bone dagger (p+)\n  examine: \"A powerful dagger.\"\n- id: 8878 # Bone dagger (p++)\n  examine: \"A powerful dagger.\"\n- id: 8882 # Bone bolts\n  examine: \"Good if you have a bone crossbow!\"\n- id: 8887 # Zanik\n  examine: \"She's dead, but the mark on her forehead is glowing brightly.\"\n- id: 8890 # Coins\n  examine: \"Lovely money!\"\n- id: 8901 # Black mask (10)\n  examine: \"A magic cave horror mask.\"\n- id: 8903 # Black mask (9)\n  examine: \"A magic cave horror mask.\"\n- id: 8905 # Black mask (8)\n  examine: \"A magic cave horror mask.\"\n- id: 8907 # Black mask (7)\n  examine: \"A magic cave horror mask.\"\n- id: 8909 # Black mask (6)\n  examine: \"A magic cave horror mask.\"\n- id: 8911 # Black mask (5)\n  examine: \"A magic cave horror mask.\"\n- id: 8913 # Black mask (4)\n  examine: \"A magic cave horror mask.\"\n- id: 8915 # Black mask (3)\n  examine: \"A magic cave horror mask.\"\n- id: 8917 # Black mask (2)\n  examine: \"A magic cave horror mask.\"\n- id: 8919 # Black mask (1)\n  examine: \"A magic cave horror mask.\"\n- id: 8921 # Black mask\n  examine: \"An inert-seeming cave horror mask.\"\n- id: 8923 # Witchwood icon\n  examine: \"A stick on a string... pure style.\"\n- id: 8924 # Bandana eyepatch\n  examine: \"Essential pirate wear.\"\n- id: 8925 # Bandana eyepatch\n  examine: \"Essential pirate wear.\"\n- id: 8926 # Bandana eyepatch\n  examine: \"Essential pirate wear.\"\n- id: 8927 # Bandana eyepatch\n  examine: \"Essential pirate wear.\"\n- id: 8928 # Hat eyepatch\n  examine: \"Essential pirate wear.\"\n- id: 8929 # Crabclaw hook\n  examine: \"Tied together so they don't come apart.\"\n- id: 8930 # Pipe section\n  examine: \"Crude wooden pipe section.\"\n- id: 8932 # Lumber patch\n  examine: \"Repairs made with this will be patchy at best.\"\n- id: 8934 # Scrapey tree logs\n  examine: \"Slimy logs from the Scrapey tree.\"\n- id: 8936 # Blue flowers\n  examine: \"Very blue.\"\n- id: 8938 # Red flowers\n  examine: \"Very red.\"\n- id: 8940 # Rum\n  examine: \"Alcohol in the loosest sense of the word.\"\n- id: 8941 # Rum\n  examine: \"Alcohol in the loosest sense of the word.\"\n- id: 8942 # Monkey\n  examine: \"A confused looking monkey.\"\n- id: 8943 # Blue monkey\n  examine: \"Bluuuuuuuue Monkeeeeeey!\"\n- id: 8944 # Blue monkey\n  examine: \"Bluuuuuuuue Monkeeeeeey!\"\n- id: 8945 # Blue monkey\n  examine: \"Bluuuuuuuue Monkeeeeeey!\"\n- id: 8946 # Red monkey\n  examine: \"A well red monkey.\"\n- id: 8947 # Red monkey\n  examine: \"A well red monkey.\"\n- id: 8948 # Red monkey\n  examine: \"A well red monkey.\"\n- id: 8949 # Pirate bandana\n  examine: \"A blue bandana.\"\n- id: 8950 # Pirate hat\n  examine: \"A red pirate hat.\"\n- id: 8951 # Pieces of eight\n  examine: \"Piratical currency.\"\n- id: 8952 # Blue naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8953 # Green naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8954 # Red naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8955 # Brown naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8956 # Black naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8957 # Purple naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8958 # Grey naval shirt\n  examine: \"...You can sail the seven seas...\"\n- id: 8959 # Blue tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8960 # Green tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8961 # Red tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8962 # Brown tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8963 # Black tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8964 # Purple tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8965 # Grey tricorn hat\n  examine: \"I could never look square in this.\"\n- id: 8966 # Cutthroat flag\n  examine: \"The flag of The Cutthroat.\"\n- id: 8967 # Guilded smile flag\n  examine: \"The flag of The Guilded Smile.\"\n- id: 8968 # Bronze fist flag\n  examine: \"The flag of The Bronze Fist.\"\n- id: 8969 # Lucky shot flag\n  examine: \"The flag of The Lucky Shot.\"\n- id: 8970 # Treasure flag\n  examine: \"The flag of The Treasure Trove.\"\n- id: 8971 # Phasmatys flag\n  examine: \"The flag of The Phasmatys Pride.\"\n- id: 8972 # Bowl of red water\n  examine: \"A bowl of red water.\"\n- id: 8974 # Bowl of blue water\n  examine: \"A bowl of blue water.\"\n- id: 8976 # Bitternut\n  examine: \"Monkeys seem to like throwing these.\"\n- id: 8977 # Scrapey bark\n  examine: \"Greasy bark from the Scrapey Tree.\"\n- id: 8979 # Bridge section\n  examine: \"Caution; not for use over troubled water.\"\n- id: 8981 # Sweetgrubs\n  examine: \"Well, at least they aren't trying mind-control.\"\n- id: 8986 # Bucket\n  examine: \"It's a wooden bucket.\"\n- id: 8987 # Torch\n  examine: \"An unlit home-made torch.\"\n- id: 8988 # The stuff\n  examine: \"Apparently good for brewing.\"\n- id: 8989 # Brewin' guide\n  examine: \"A how-to of brewing and arson.\"\n- id: 8990 # Brewin' guide\n  examine: \"A how-to of brewing and arson.\"\n- id: 8991 # Blue navy slacks\n  examine: \"Not for slackers.\"\n- id: 8992 # Green navy slacks\n  examine: \"Not for slackers.\"\n- id: 8993 # Red navy slacks\n  examine: \"Not for slackers.\"\n- id: 8994 # Brown navy slacks\n  examine: \"Not for slackers.\"\n- id: 8995 # Black navy slacks\n  examine: \"Not for slackers.\"\n- id: 8996 # Purple navy slacks\n  examine: \"Not for slackers.\"\n- id: 8997 # Grey navy slacks\n  examine: \"Not for slackers.\"\n- id: 9003 # Security book\n  examine: \"WARNING: Contains information which could make your account secure!\"\n- id: 9004 # Stronghold notes\n  examine: \"Information regarding the Stronghold of Security.\"\n- id: 9005 # Fancy boots\n  examine: \"Very nice boots from the Stronghold of Security.\"\n- id: 9006 # Fighting boots\n  examine: \"Very nice boots from the Stronghold of Security.\"\n- id: 9007 # Right skull half\n  examine: \"Ooooh spooky!\"\n- id: 9008 # Left skull half\n  examine: \"Ooooh spooky!\"\n- id: 9009 # Strange skull\n  examine: \"Seems to be for use with a staff or sceptre of some sort.\"\n- id: 9010 # Top of sceptre\n  examine: \"Top half of a broken Sceptre.\"\n- id: 9011 # Bottom of sceptre\n  examine: \"Bottom half of a broken Sceptre.\"\n- id: 9012 # Runed sceptre\n  examine: \"Sceptre with runes on, it appears to be missing something.\"\n- id: 9013 # Skull sceptre\n  examine: \"A fragile magical Sceptre.\"\n- id: 9016 # Gorak claws\n  examine: \"Oversized nail clippings.\"\n- id: 9017 # Star flower\n  examine: \"A rare flower with magical properties.\"\n- id: 9018 # Gorak claw powder\n  examine: \"Ground-down Gorak claws.\"\n- id: 9020 # Queen's secateurs\n  examine: \"Contains the Fairy Queen's magic essence.\"\n- id: 9021 # Magic essence(4)\n  examine: \"4 doses of magic essence potion.\"\n- id: 9022 # Magic essence(3)\n  examine: \"3 doses of magic essence potion.\"\n- id: 9023 # Magic essence(2)\n  examine: \"2 doses of magic essence potion.\"\n- id: 9024 # Magic essence(1)\n  examine: \"1 dose of magic essence potion.\"\n- id: 9025 # Nuff's certificate\n  examine: \"A scroll that says she's a healer, that's Fairy Nuff.\"\n- id: 9026 # Ivory comb\n  examine: \"Gets knots and kinks out of your hair.\"\n- id: 9028 # Golden scarab\n  examine: \"Little ornament in the shape of a scarab.\"\n- id: 9030 # Stone scarab\n  examine: \"Little ornament in the shape of a scarab.\"\n- id: 9032 # Pottery scarab\n  examine: \"Little ornament in the shape of a scarab.\"\n- id: 9034 # Golden statuette\n  examine: \"A small golden statuette.\"\n- id: 9036 # Pottery statuette\n  examine: \"A small statuette.\"\n- id: 9038 # Stone statuette\n  examine: \"A small statuette.\"\n- id: 9040 # Gold seal\n  examine: \"A seal. It's gold.\"\n- id: 9042 # Stone seal\n  examine: \"A seal. Made out of stone obviously.\"\n- id: 9052 # Locust meat\n  examine: \"Delicious and nutritious. Well, nutritious anyway.\"\n- id: 9054 # Red goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9055 # Black goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9056 # Yellow goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9057 # Green goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9058 # Purple goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9059 # Pink goblin mail\n  examine: \"Armour designed to fit goblins.\"\n- id: 9064 # Emerald lantern\n  examine: \"A mystical lantern.\"\n- id: 9065 # Emerald lantern\n  examine: \"A mystical lantern casting a green beam.\"\n- id: 9066 # Emerald lens\n  examine: \"A roughly circular disc of glass.\"\n- id: 9067 # Dream log\n  examine: \"A log of my thoughts...\"\n- id: 9068 # Moonclan helm\n  examine: \"Mystical headgear.\"\n- id: 9069 # Moonclan hat\n  examine: \"A mystical hat.\"\n- id: 9070 # Moonclan armour\n  examine: \"Provides good protection.\"\n- id: 9071 # Moonclan skirt\n  examine: \"This should protect my legs.\"\n- id: 9072 # Moonclan gloves\n  examine: \"These should keep my hands safe.\"\n- id: 9073 # Moonclan boots\n  examine: \"Groovy foot protection.\"\n- id: 9074 # Moonclan cape\n  examine: \"A mystical cape.\"\n- id: 9075 # Astral rune\n  examine: \"Used for Lunar spells.\"\n- id: 9076 # Lunar ore\n  examine: \"This needs refining.\"\n- id: 9077 # Lunar bar\n  examine: \"It's a bar of magic metal.\"\n- id: 9078 # Moonclan manual\n  examine: \"A book of Moonclan history\"\n- id: 9079 # Suqah tooth\n  examine: \"The tooth, the whole tooth, and nothing but the tooth.\"\n- id: 9080 # Suqah hide\n  examine: \"An untanned piece of Suqah hide.\"\n- id: 9081 # Suqah leather\n  examine: \"A piece of Suqah hide that has been expertly tanned into leather.\"\n- id: 9082 # Ground tooth\n  examine: \"A ground Suqah tooth.\"\n- id: 9083 # Seal of passage\n  examine: \"A seal of passage issued by Brundt the Chieftain of the Fremennik.\"\n- id: 9084 # Lunar staff\n  examine: \"A Moonclan staff.\"\n- id: 9085 # Empty vial\n  examine: \"A vessel for holding liquid.\"\n- id: 9086 # Vial of water\n  examine: \"A vessel containing water.\"\n- id: 9087 # Waking sleep vial\n  examine: \"A vessel for dreaming while awake!\"\n- id: 9088 # Guam vial\n  examine: \"A vessel with water and Guam inside.\"\n- id: 9089 # Marr vial\n  examine: \"A vessel with water and Marrentill inside.\"\n- id: 9090 # Guam-marr vial\n  examine: \"A vessel with water, Guam and Marrentill inside.\"\n- id: 9091 # Lunar staff - pt1\n  examine: \"A staff enchanted by air.\"\n- id: 9092 # Lunar staff - pt2\n  examine: \"A staff enchanted by air and fire.\"\n- id: 9093 # Lunar staff - pt3\n  examine: \"A staff enchanted by air, fire and water.\"\n- id: 9094 # Kindling\n  examine: \"Small bits of wood from the first magic tree!\"\n- id: 9095 # Soaked kindling\n  examine: \"Magic wood soaked with a potion of waking sleep. Groovy.\"\n- id: 9096 # Lunar helm\n  examine: \"A mystical helmet.\"\n- id: 9097 # Lunar torso\n  examine: \"Provides good protection.\"\n- id: 9098 # Lunar legs\n  examine: \"These should protect my legs.\"\n- id: 9099 # Lunar gloves\n  examine: \"These should keep my hands safe.\"\n- id: 9100 # Lunar boots\n  examine: \"Mystical foot protection.\"\n- id: 9101 # Lunar cape\n  examine: \"Oooo pretty!\"\n- id: 9102 # Lunar amulet\n  examine: \"Awesome.\"\n- id: 9103 # A special tiara\n  examine: \"I'll be the talk of the town with this... maybe.\"\n- id: 9104 # Lunar ring\n  examine: \"A mysterious ring that can fill the wearer with magical power...\"\n- id: 9139 # Blurite bolts\n  examine: \"Blurite crossbow bolts.\"\n- id: 9140 # Iron bolts\n  examine: \"Iron crossbow bolts.\"\n- id: 9141 # Steel bolts\n  examine: \"Steel crossbow bolts.\"\n- id: 9142 # Mithril bolts\n  examine: \"Mithril crossbow bolts.\"\n- id: 9143 # Adamant bolts\n  examine: \"Adamantite crossbow bolts.\"\n- id: 9144 # Runite bolts\n  examine: \"Runite crossbow bolts.\"\n- id: 9145 # Silver bolts\n  examine: \"Silver crossbow bolts.\"\n- id: 9187 # Jade bolt tips\n  examine: \"Jade bolt tips.\"\n- id: 9188 # Topaz bolt tips\n  examine: \"Red Topaz bolt tips.\"\n- id: 9189 # Sapphire bolt tips\n  examine: \"Sapphire bolt tips.\"\n- id: 9190 # Emerald bolt tips\n  examine: \"Emerald bolt tips.\"\n- id: 9191 # Ruby bolt tips\n  examine: \"Ruby bolt tips.\"\n- id: 9192 # Diamond bolt tips\n  examine: \"Diamond bolt tips.\"\n- id: 9194 # Onyx bolt tips\n  examine: \"Onyx bolt tips.\"\n- id: 9236 # Opal bolts (e)\n  examine: \"Enchanted Opal tipped Bronze Crossbow Bolts.\"\n- id: 9237 # Jade bolts (e)\n  examine: \"Enchanted Jade tipped Blurite Crossbow Bolts.\"\n- id: 9238 # Pearl bolts (e)\n  examine: \"Enchanted Pearl tipped Iron Crossbow Bolts.\"\n- id: 9239 # Topaz bolts (e)\n  examine: \"Enchanted Red Topaz tipped Steel Crossbow Bolts.\"\n- id: 9240 # Sapphire bolts (e)\n  examine: \"Enchanted Sapphire tipped Mithril Crossbow Bolts.\"\n- id: 9241 # Emerald bolts (e)\n  examine: \"Enchanted Emerald tipped Mithril Crossbow Bolts.\"\n- id: 9242 # Ruby bolts (e)\n  examine: \"Enchanted Ruby tipped Adamantite Crossbow Bolts.\"\n- id: 9243 # Diamond bolts (e)\n  examine: \"Enchanted Diamond tipped Adamantite Crossbow Bolts.\"\n- id: 9245 # Onyx bolts (e)\n  examine: \"Enchanted Onyx tipped Runite Crossbow Bolts.\"\n- id: 9287 # Iron bolts (p)\n  examine: \"Some poisoned iron bolts.\"\n- id: 9288 # Steel bolts (p)\n  examine: \"Some poisoned steel bolts.\"\n- id: 9289 # Mithril bolts (p)\n  examine: \"Some poisoned mithril bolts.\"\n- id: 9291 # Runite bolts (p)\n  examine: \"Some poisoned runite bolts.\"\n- id: 9292 # Silver bolts (p)\n  examine: \"Some poisoned silver bolts.\"\n- id: 9335 # Jade bolts\n  examine: \"Jade tipped Blurite crossbow bolts.\"\n- id: 9336 # Topaz bolts\n  examine: \"Topaz tipped Steel crossbow bolts.\"\n- id: 9337 # Sapphire bolts\n  examine: \"Sapphire tipped Mithril crossbow bolts.\"\n- id: 9338 # Emerald bolts\n  examine: \"Emerald tipped Mithril crossbow bolts.\"\n- id: 9339 # Ruby bolts\n  examine: \"Ruby tipped Adamantite crossbow bolts.\"\n- id: 9340 # Diamond bolts\n  examine: \"Diamond tipped Adamantite crossbow bolts.\"\n- id: 9342 # Onyx bolts\n  examine: \"Onyx tipped Runite crossbow bolts.\"\n- id: 9375 # Bronze bolts (unf)\n  examine: \"Unfeathered bronze crossbow bolts.\"\n- id: 9376 # Blurite bolts (unf)\n  examine: \"Unfeathered blurite crossbow bolts.\"\n- id: 9377 # Iron bolts (unf)\n  examine: \"Unfeathered iron crossbow bolts.\"\n- id: 9378 # Steel bolts (unf)\n  examine: \"Unfeathered steel crossbow bolts.\"\n- id: 9379 # Mithril bolts (unf)\n  examine: \"Unfeathered mithril crossbow bolts.\"\n- id: 9380 # Adamant bolts(unf)\n  examine: \"Unfeathered adamantite crossbow bolts.\"\n- id: 9381 # Runite bolts (unf)\n  examine: \"Unfeathered runite crossbow bolts.\"\n- id: 9382 # Silver bolts (unf)\n  examine: \"Unfeathered silver crossbow bolts.\"\n- id: 9416 # Mith grapple tip\n  examine: \"A mithril grapple tip.\"\n- id: 9418 # Mith grapple\n  examine: \"A mithril grapple tipped bolt - needs a rope.\"\n- id: 9419 # Mith grapple\n  examine: \"A mithril grapple tipped bolt with a rope.\"\n- id: 9420 # Bronze limbs\n  examine: \"A pair of bronze crossbow limbs.\"\n- id: 9422 # Blurite limbs\n  examine: \"A pair of blurite crossbow limbs.\"\n- id: 9423 # Iron limbs\n  examine: \"A pair of iron crossbow limbs.\"\n- id: 9425 # Steel limbs\n  examine: \"A pair of steel crossbow limbs.\"\n- id: 9427 # Mithril limbs\n  examine: \"A pair of mithril crossbow limbs.\"\n- id: 9429 # Adamantite limbs\n  examine: \"A pair of adamantite crossbow limbs.\"\n- id: 9431 # Runite limbs\n  examine: \"A pair of runite crossbow limbs.\"\n- id: 9433 # Bolt pouch\n  examine: \"A pouch for storing crossbow bolts.\"\n- id: 9434 # Bolt mould\n  examine: \"A mould for creating silver crossbow bolts.\"\n- id: 9436 # Sinew\n  examine: \"I can use this to make a crossbow string.\"\n- id: 9438 # Crossbow string\n  examine: \"A string for a crossbow.\"\n- id: 9440 # Wooden stock\n  examine: \"A wooden crossbow stock.\"\n- id: 9442 # Oak stock\n  examine: \"An oak crossbow stock.\"\n- id: 9444 # Willow stock\n  examine: \"A willow crossbow stock.\"\n- id: 9446 # Teak stock\n  examine: \"A teak crossbow stock.\"\n- id: 9448 # Maple stock\n  examine: \"A maple crossbow stock.\"\n- id: 9450 # Mahogany stock\n  examine: \"A mahogany crossbow stock.\"\n- id: 9452 # Yew stock\n  examine: \"A yew crossbow stock.\"\n- id: 9467 # Blurite bar\n  examine: \"It's a bar of blurite.\"\n- id: 9468 # Sawdust\n  examine: \"What is left over when a log is made into a plank.\"\n- id: 9469 # Grand seed pod\n  examine: \"A seed pod of the Grand Tree.\"\n- id: 9470 # Gnome scarf\n  examine: \"A scarf. You feel your upper lip stiffening.\"\n- id: 9472 # Gnome goggles\n  examine: \"Tally Ho!\"\n- id: 9474 # Reward token\n  examine: \"This entitles you to one free gnome food delivery.\"\n- id: 9475 # Mint cake\n  examine: \"It looks very minty.\"\n- id: 9477 # Aluft aloft box\n  examine: \"You can check on your delivery details here.\"\n- id: 9478 # Half made batta\n  examine: \"This fruit batta needs baking and garnishing with spice.\"\n- id: 9479 # Unfinished batta\n  examine: \"This fruit batta needs garnishing with spice.\"\n- id: 9480 # Half made batta\n  examine: \"This worm batta needs baking and garnishing with equa leaves.\"\n- id: 9482 # Half made batta\n  examine: \"This toad batta just requires baking to complete.\"\n- id: 9483 # Half made batta\n  examine: \"This cheese and tom batta needs baking and garnishing with equa leaves.\"\n- id: 9485 # Half made batta\n  examine: \"This veg batta needs baking and garnishing with equa leaves.\"\n- id: 9487 # Wizard blizzard\n  examine: \"This looks like a strange mix.\"\n- id: 9489 # Wizard blizzard\n  examine: \"This looks like a strange mix.\"\n- id: 9508 # Wizard blizzard\n  examine: \"This looks like a strange mix.\"\n- id: 9510 # Short green guy\n  examine: \"A Short Green Guy... looks good.\"\n- id: 9512 # Pineapple punch\n  examine: \"A fresh healthy fruit mix.\"\n- id: 9514 # Fruit blast\n  examine: \"A cool refreshing fruit mix.\"\n- id: 9516 # Drunk dragon\n  examine: \"A warm creamy alcoholic beverage.\"\n- id: 9518 # Choc saturday\n  examine: \"A warm creamy alcoholic beverage.\"\n- id: 9520 # Blurberry special\n  examine: \"Looks good... smells strong.\"\n- id: 9522 # Batta tin\n  examine: \"A deep tin used for baking gnome battas in.\"\n- id: 9524 # Batta tin\n  examine: \"A deep tin used for baking gnome battas in.\"\n- id: 9527 # Fruit batta\n  examine: \"It actually smells quite good.\"\n- id: 9529 # Toad batta\n  examine: \"It actually smells quite good.\"\n- id: 9531 # Worm batta\n  examine: \"It actually smells quite good.\"\n- id: 9533 # Vegetable batta\n  examine: \"Well... it looks healthy.\"\n- id: 9535 # Cheese+tom batta\n  examine: \"This smells really good.\"\n- id: 9538 # Toad crunchies\n  examine: \"It actually smells quite good.\"\n- id: 9540 # Spicy crunchies\n  examine: \"Yum... smells spicy.\"\n- id: 9542 # Worm crunchies\n  examine: \"It actually smells quite good.\"\n- id: 9544 # Chocchip crunchies\n  examine: \"Yum... smells good.\"\n- id: 9547 # Worm hole\n  examine: \"It actually smells quite good.\"\n- id: 9549 # Veg ball\n  examine: \"This looks pretty healthy.\"\n- id: 9553 # Chocolate bomb\n  examine: \"Full of creamy, chocolately goodness.\"\n- id: 9558 # Half made bowl\n  examine: \"This unfinished tangled toads legs requires baking.\"\n- id: 9559 # Half made bowl\n  examine: \"This unfinished worm hole needs baking and garnishing with equa leaves.\"\n- id: 9560 # Unfinished bowl\n  examine: \"This unfinished worm hole needs garnishing with equa leaves.\"\n- id: 9561 # Half made bowl\n  examine: \"This unfinished veg ball needs baking and garnishing with equa leaves.\"\n- id: 9562 # Unfinished bowl\n  examine: \"This unfinished veg ball needs garnishing with equa leaves.\"\n- id: 9563 # Half made bowl\n  examine: \"This unfinished choc bomb needs baking, two pots of cream and choc dust.\"\n- id: 9564 # Unfinished bowl\n  examine: \"This unfinished choc bomb needs two pots of cream and chocolate dust.\"\n- id: 9566 # Mixed blizzard\n  examine: \"This Wizzard Blizzard needs pouring, a lime slice and pineapple chunks.\"\n- id: 9567 # Mixed sgg\n  examine: \"This Short Green Guy cocktail needs pouring, a lime slice and equa leaves..\"\n- id: 9568 # Mixed blast\n  examine: \"This Fruit Blast cocktail needs pouring and a lemon slice.\"\n- id: 9569 # Mixed punch\n  examine: \"This Pineapple Punch needs pouring, lime and pineapple chunks and a orange slice.\"\n- id: 9570 # Mixed special\n  examine: \"This Blurberry Special needs pouring, orange and lemon chunks, a lime slice and equa leaves.\"\n- id: 9571 # Mixed saturday\n  examine: \"This Choc Saturday needs pouring, heating and cream and chocolate dust.\"\n- id: 9572 # Mixed saturday\n  examine: \"This Choc Saturday needs heating, and cream and chocolate dust.\"\n- id: 9573 # Mixed saturday\n  examine: \"This Choc Saturday needs cream and chocolate dust to finish.\"\n- id: 9574 # Mixed dragon\n  examine: \"This Drunk Dragon needs pouring, pineapple chunks, cream and heating.\"\n- id: 9575 # Mixed dragon\n  examine: \"This Drunk Dragon needs pineapple chunks, cream and heating.\"\n- id: 9576 # Mixed dragon\n  examine: \"This Drunk Dragon needs heating to finish.\"\n- id: 9577 # Half made crunchy\n  examine: \"This choc chip crunchy needs baking and garnishing with chocolate dust.\"\n- id: 9578 # Unfinished crunchy\n  examine: \"This choc chip crunchy needs garnishing with chocolate dust.\"\n- id: 9579 # Half made crunchy\n  examine: \"This spicy crunchy needs baking and garnishing with spice.\"\n- id: 9580 # Unfinished crunchy\n  examine: \"This spicy crunchy needs garnishing with spice.\"\n- id: 9581 # Half made crunchy\n  examine: \"This toad crunchy needs baking and garnishing with equa leaves.\"\n- id: 9582 # Unfinished crunchy\n  examine: \"This toad crunchy needs garnishing with equa leaves.\"\n- id: 9583 # Half made crunchy\n  examine: \"This worm crunchy needs baking and garnishing with gnome spices.\"\n- id: 9584 # Unfinished crunchy\n  examine: \"This worm crunchy needs garnishing with gnome spices.\"\n- id: 9589 # Dossier\n  examine: \"A dossier containing info on the Black Knight plot.\"\n- id: 9590 # Dossier\n  examine: \"A dossier containing info on the Black Knight plot.\"\n- id: 9592 # Magic glue\n  examine: \"Glue made from tree sap and ground mud runes.\"\n- id: 9593 # Weird gloop\n  examine: \"This doesn't look like it will do anything interesting.\"\n- id: 9594 # Ground mud runes\n  examine: \"Mud runes ground into a powder.\"\n- id: 9597 # A red circle\n  examine: \"A red circular crystalline disc.\"\n- id: 9598 # A red triangle\n  examine: \"A red triangular crystalline disc.\"\n- id: 9599 # A red square\n  examine: \"A red square-shaped crystalline disc.\"\n- id: 9600 # A red pentagon\n  examine: \"A red pentagon-shaped crystalline disc.\"\n- id: 9601 # An orange circle\n  examine: \"An orange circular crystalline disc.\"\n- id: 9602 # An orange triangle\n  examine: \"An orange triangular crystalline disc.\"\n- id: 9603 # An orange square\n  examine: \"An orange square-shaped crystalline disc.\"\n- id: 9604 # Orange pentagon\n  examine: \"An orange pentagon-shaped crystalline disc.\"\n- id: 9605 # A yellow circle\n  examine: \"A yellow circular crystalline disc.\"\n- id: 9606 # A yellow triangle\n  examine: \"A yellow triangular crystalline disc.\"\n- id: 9607 # A yellow square\n  examine: \"A yellow square-shaped crystalline disc.\"\n- id: 9608 # A yellow pentagon\n  examine: \"A yellow pentagon-shaped crystalline disc.\"\n- id: 9609 # A green circle\n  examine: \"A green circular crystalline disc.\"\n- id: 9610 # A green triangle\n  examine: \"A green triangular crystalline disc.\"\n- id: 9611 # A green square\n  examine: \"A green square-shaped crystalline disc.\"\n- id: 9612 # A green pentagon\n  examine: \"A green pentagon-shaped crystalline disc.\"\n- id: 9613 # A blue circle\n  examine: \"A blue circular crystalline disc.\"\n- id: 9614 # A blue triangle\n  examine: \"A blue triangular crystalline disc.\"\n- id: 9615 # A blue square\n  examine: \"A blue square-shaped crystalline disc.\"\n- id: 9616 # A blue pentagon\n  examine: \"A blue pentagon-shaped crystalline disc.\"\n- id: 9617 # An indigo circle\n  examine: \"An indigo circular crystalline disc.\"\n- id: 9618 # An indigo triangle\n  examine: \"An indigo triangular crystalline disc.\"\n- id: 9619 # An indigo square\n  examine: \"An indigo square-shaped crystalline disc.\"\n- id: 9620 # An indigo pentagon\n  examine: \"An indigo pentagon-shaped crystalline disc.\"\n- id: 9621 # A violet circle\n  examine: \"A violet circular crystalline disc.\"\n- id: 9622 # A violet triangle\n  examine: \"A violet triangular crystalline disc.\"\n- id: 9623 # A violet square\n  examine: \"A violet square-shaped crystalline disc.\"\n- id: 9624 # A violet pentagon\n  examine: \"A violet pentagon-shaped crystalline disc.\"\n- id: 9625 # Crystal saw\n  examine: \"A magical saw.\"\n- id: 9627 # A handwritten book\n  examine: \"A book on elven crystal.\"\n- id: 9629 # Tyras helm\n  examine: \"As used by King Tyras' personal guard.\"\n- id: 9632 # Daeyalt ore\n  examine: \"This needs refining.\"\n- id: 9633 # Message\n  examine: \"A message for Veliaf.\"\n- id: 9634 # Vyrewatch top\n  examine: \"Dress like the powerful vyrewatch!\"\n- id: 9636 # Vyrewatch legs\n  examine: \"Dress like the powerful vyrewatch!\"\n- id: 9638 # Vyrewatch shoes\n  examine: \"Dress like the powerful vyrewatch!\"\n- id: 9640 # Citizen top\n  examine: \"Ghetto disguise!\"\n- id: 9642 # Citizen trousers\n  examine: \"Ghetto disguise!\"\n- id: 9644 # Citizen shoes\n  examine: \"Ghetto disguise!\"\n- id: 9646 # Castle sketch 1\n  examine: \"Northern approach of the castle.\"\n- id: 9647 # Castle sketch 2\n  examine: \"Western approach of the castle.\"\n- id: 9648 # Castle sketch 3\n  examine: \"Southern approach of the castle.\"\n- id: 9649 # Message\n  examine: \"A message found behind a loose tile.\"\n- id: 9651 # Large ornate key\n  examine: \"A key to some large, strange door.\"\n- id: 9652 # Haemalchemy\n  examine: \"A book called Haemalchemy Volume 1.\"\n- id: 9653 # Sealed message\n  examine: \"A sealed message from Safalaan to Veliaf.\"\n- id: 9654 # Door key\n  examine: \"A key to some door.\"\n- id: 9655 # Ladder top\n  examine: \"The top of a ladder.\"\n- id: 9665 # Torch\n  examine: \"An unlit home-made torch.\"\n- id: 9668 # Initiate harness m\n  examine: \"Initiate level armour pack.\"\n- id: 9672 # Proselyte sallet\n  examine: \"A Proselyte Temple Knight's helm.\"\n- id: 9674 # Proselyte hauberk\n  examine: \"A Proselyte Temple Knight's armour.\"\n- id: 9676 # Proselyte cuisse\n  examine: \"A Proselyte Temple Knight's leg armour.\"\n- id: 9678 # Proselyte tasset\n  examine: \"A Proselyte Temple Knight's leg armour.\"\n- id: 9680 # Sea slug glue\n  examine: \"A rendered down baby sea slug.\"\n- id: 9681 # Commorb v2\n  examine: \"A Temple Knight Communication Orb. Top Secret!\"\n- id: 9682 # Door transcription\n  examine: \"A copy of the mysterious glyphs.\"\n- id: 9683 # Dead sea slug\n  examine: \"Dead sea slug, very sticky.\"\n- id: 9684 # Page 1\n  examine: \"A page from Maledict's holy book.\"\n- id: 9685 # Page 2\n  examine: \"A page from Maledict's holy book.\"\n- id: 9686 # Page 3\n  examine: \"A page from Maledict's holy book.\"\n- id: 9687 # Fragment 1\n  examine: \"A piece of a torn page.\"\n- id: 9688 # Fragment 2\n  examine: \"A piece of a torn page.\"\n- id: 9689 # Fragment 3\n  examine: \"A piece of a torn page.\"\n- id: 9690 # Blank water rune\n  examine: \"A blank water rune.\"\n- id: 9691 # Water rune\n  examine: \"A water rune.\"\n- id: 9692 # Blank air rune\n  examine: \"A blank air rune.\"\n- id: 9693 # Air rune\n  examine: \"An air rune.\"\n- id: 9694 # Blank earth rune\n  examine: \"A blank earth rune.\"\n- id: 9695 # Earth rune\n  examine: \"An earth rune.\"\n- id: 9696 # Blank mind rune\n  examine: \"A blank mind rune.\"\n- id: 9697 # Mind rune\n  examine: \"A mind rune.\"\n- id: 9698 # Blank fire rune\n  examine: \"A blank fire rune.\"\n- id: 9699 # Fire rune\n  examine: \"A fire rune.\"\n- id: 9703 # Training sword\n  examine: \"Basic training sword.\"\n- id: 9704 # Training shield\n  examine: \"Made of flimsy painted wood.\"\n- id: 9705 # Training bow\n  examine: \"Light and flexible, good for a beginner.\"\n- id: 9706 # Training arrows\n  examine: \"Standard training arrows.\"\n- id: 9715 # Slashed book\n  examine: \"Book of the elemental shield.\"\n- id: 9717 # Beaten book\n  examine: \"Book of the Elemental Helm.\"\n- id: 9718 # Crane schematic\n  examine: \"On the subject of lava dippers.\"\n- id: 9719 # Lever schematic\n  examine: \"A scroll with a lever schematic drawn on it.\"\n- id: 9720 # Crane claw\n  examine: \"A crane claw.\"\n- id: 9721 # Scroll\n  examine: \"A scroll with some writing on it.\"\n- id: 9722 # Key\n  examine: \"Quite a small key.\"\n- id: 9723 # Pipe\n  examine: \"A spare section of pipe.\"\n- id: 9724 # Large cog\n  examine: \"A large cog.\"\n- id: 9725 # Medium cog\n  examine: \"A medium cog.\"\n- id: 9726 # Small cog\n  examine: \"A small cog.\"\n- id: 9727 # Primed bar\n  examine: \"A primed elemental ingot.\"\n- id: 9728 # Primed mind bar\n  examine: \"An elemental mind ingot.\"\n- id: 9729 # Elemental helmet\n  examine: \"A magic helmet.\"\n- id: 9731 # Mind shield\n  examine: \"A magic shield.\"\n- id: 9733 # Mind helmet\n  examine: \"A magic helmet.\"\n- id: 9735 # Desert goat horn\n  examine: \"Not much good for blowing.\"\n- id: 9736 # Goat horn dust\n  examine: \"Finely ground desert goat horn.\"\n- id: 9739 # Combat potion(4)\n  examine: \"4 doses of combat potion.\"\n- id: 9741 # Combat potion(3)\n  examine: \"3 doses of combat potion.\"\n- id: 9743 # Combat potion(2)\n  examine: \"2 doses of combat potion.\"\n- id: 9745 # Combat potion(1)\n  examine: \"1 dose of combat potion.\"\n- id: 9747 # Attack cape\n  examine: \"The cape worn by masters of Attack.\"\n- id: 9748 # Attack cape(t)\n  examine: \"The cape worn by masters of Attack.\"\n- id: 9749 # Attack hood\n  examine: \"Attack skillcape hood.\"\n- id: 9750 # Strength cape\n  examine: \"The cape worn by only the strongest people.\"\n- id: 9751 # Strength cape(t)\n  examine: \"The cape worn by only the strongest people.\"\n- id: 9752 # Strength hood\n  examine: \"Strength skillcape hood.\"\n- id: 9753 # Defence cape\n  examine: \"The cape worn by masters of the art of Defence.\"\n- id: 9754 # Defence cape(t)\n  examine: \"The cape worn by masters of the art of Defence.\"\n- id: 9755 # Defence hood\n  examine: \"Defence skillcape hood.\"\n- id: 9756 # Ranging cape\n  examine: \"The cape worn by master archers.\"\n- id: 9757 # Ranging cape(t)\n  examine: \"The cape worn by master archers.\"\n- id: 9758 # Ranging hood\n  examine: \"Range skillcape hood.\"\n- id: 9759 # Prayer cape\n  examine: \"The cape worn by the most pious of heroes.\"\n- id: 9760 # Prayer cape(t)\n  examine: \"The cape worn by the most pious of heroes.\"\n- id: 9761 # Prayer hood\n  examine: \"Prayer skillcape hood.\"\n- id: 9762 # Magic cape\n  examine: \"The cape worn by the most powerful mages.\"\n- id: 9763 # Magic cape(t)\n  examine: \"The cape worn by the most powerful mages.\"\n- id: 9764 # Magic hood\n  examine: \"Magic skillcape hood.\"\n- id: 9765 # Runecraft cape\n  examine: \"The cape worn by master runecrafters.\"\n- id: 9766 # Runecraft cape(t)\n  examine: \"The cape worn by master runecrafters.\"\n- id: 9768 # Hitpoints cape\n  examine: \"The cape worn by the healthiest adventurers.\"\n- id: 9769 # Hitpoints cape(t)\n  examine: \"The cape worn by the healthiest adventurers.\"\n- id: 9770 # Hitpoints hood\n  examine: \"Hitpoints skillcape hood.\"\n- id: 9771 # Agility cape\n  examine: \"The cape worn by the most agile of heroes.\"\n- id: 9772 # Agility cape(t)\n  examine: \"The cape worn by the most agile of heroes.\"\n- id: 9773 # Agility hood\n  examine: \"Agility skillcape hood.\"\n- id: 9774 # Herblore cape\n  examine: \"The cape worn by the most skilled at the art of Herblore.\"\n- id: 9775 # Herblore cape(t)\n  examine: \"The cape worn by the most skilled at the art of Herblore.\"\n- id: 9776 # Herblore hood\n  examine: \"Herblore skillcape hood.\"\n- id: 9777 # Thieving cape\n  examine: \"The cape worn by master thieves.\"\n- id: 9778 # Thieving cape(t)\n  examine: \"The cape worn by master thieves.\"\n- id: 9779 # Thieving hood\n  examine: \"Thieving skillcape hood.\"\n- id: 9780 # Crafting cape\n  examine: \"The cape worn by master craftworkers.\"\n- id: 9781 # Crafting cape(t)\n  examine: \"The cape worn by master craftworkers.\"\n- id: 9782 # Crafting hood\n  examine: \"Crafting skillcape hood.\"\n- id: 9783 # Fletching cape\n  examine: \"The cape worn by the best of fletchers.\"\n- id: 9784 # Fletching cape(t)\n  examine: \"The cape worn by the best of fletchers.\"\n- id: 9785 # Fletching hood\n  examine: \"Fletching skillcape hood.\"\n- id: 9786 # Slayer cape\n  examine: \"The cape worn by Slayer masters.\"\n- id: 9787 # Slayer cape(t)\n  examine: \"The cape worn by Slayer masters.\"\n- id: 9788 # Slayer hood\n  examine: \"Slayer skillcape hood.\"\n- id: 9789 # Construct. cape\n  examine: \"The cape worn by master builders.\"\n- id: 9790 # Construct. cape(t)\n  examine: \"The cape worn by master builders.\"\n- id: 9791 # Construct. hood\n  examine: \"Construction skillcape hood.\"\n- id: 9792 # Mining cape\n  examine: \"The cape worn by the most skilled miners.\"\n- id: 9793 # Mining cape(t)\n  examine: \"The cape worn by the most skilled miners.\"\n- id: 9794 # Mining hood\n  examine: \"Mining skillcape hood.\"\n- id: 9795 # Smithing cape\n  examine: \"The cape worn by master smiths.\"\n- id: 9796 # Smithing cape(t)\n  examine: \"The cape worn by master smiths.\"\n- id: 9797 # Smithing hood\n  examine: \"Smithing skillcape hood.\"\n- id: 9798 # Fishing cape\n  examine: \"The cape worn by the best fishermen.\"\n- id: 9799 # Fishing cape(t)\n  examine: \"The cape worn by the best fishermen.\"\n- id: 9800 # Fishing hood\n  examine: \"Fishing skillcape hood.\"\n- id: 9801 # Cooking cape\n  examine: \"The cape worn by the world's best chefs.\"\n- id: 9802 # Cooking cape(t)\n  examine: \"The cape worn by the world's best chefs.\"\n- id: 9803 # Cooking hood\n  examine: \"Cooking skillcape hood.\"\n- id: 9804 # Firemaking cape\n  examine: \"The cape worn by master firelighters.\"\n- id: 9805 # Firemaking cape(t)\n  examine: \"The cape worn by master firelighters.\"\n- id: 9806 # Firemaking hood\n  examine: \"Firemaking skillcape hood.\"\n- id: 9807 # Woodcutting cape\n  examine: \"The cape worn by master woodcutters.\"\n- id: 9808 # Woodcut. cape(t)\n  examine: \"The cape worn by master woodcutters.\"\n- id: 9809 # Woodcutting hood\n  examine: \"Woodcutting skillcape hood.\"\n- id: 9810 # Farming cape\n  examine: \"The cape worn by master farmers.\"\n- id: 9811 # Farming cape(t)\n  examine: \"The cape worn by master farmers.\"\n- id: 9812 # Farming hood\n  examine: \"Farming skillcape hood.\"\n- id: 9813 # Quest point cape\n  examine: \"The cape worn by only the most experienced adventurers.\"\n- id: 9814 # Quest point hood\n  examine: \"Quest point cape hood.\"\n- id: 9815 # Bobble hat\n  examine: \"A woolly bobble hat.\"\n- id: 9816 # Bobble scarf\n  examine: \"A woolly scarf.\"\n- id: 9843 # Oak cape rack\n  examine: \"How does it all fit in there?\"\n- id: 9844 # Teak cape rack\n  examine: \"How does it all fit in there?\"\n- id: 9846 # Gilded cape rack\n  examine: \"How does it all fit in there?\"\n- id: 9847 # Marble cape rack\n  examine: \"How does it all fit in there?\"\n- id: 9848 # Magic cape rack\n  examine: \"How does it all fit in there?\"\n- id: 9849 # Oak toy box\n  examine: \"How does it all fit in there?\"\n- id: 9850 # Teak toy box\n  examine: \"How does it all fit in there?\"\n- id: 9851 # Mahogany toy box\n  examine: \"How does it all fit in there?\"\n- id: 9859 # Oak armour case\n  examine: \"How does it all fit in there?\"\n- id: 9860 # Teak armour case\n  examine: \"How does it all fit in there?\"\n- id: 9862 # Oak treasure chest\n  examine: \"How does it all fit in there?\"\n- id: 9864 # M. treasure chest\n  examine: \"Mahogany treasure chest.\"\n- id: 9901 # Goutweedy lump\n  examine: \"A lump that might at some point have been a gout tuber.\"\n- id: 9902 # Hardy gout tubers\n  examine: \"A pile of gout tubers suitable for use in mountainous terrain.\"\n- id: 9903 # Farming manual\n  examine: \"Farmer Gricoller's Farming Manual.\"\n- id: 9906 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a white destabiliser.\"\n- id: 9907 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a red destabiliser.\"\n- id: 9908 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a blue destabiliser.\"\n- id: 9909 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a green destabiliser.\"\n- id: 9910 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a yellow destabiliser.\"\n- id: 9911 # Ghost buster 500\n  examine: \"A Ghost Buster 500 loaded with a black destabiliser.\"\n- id: 9912 # Ghost buster 500\n  examine: \"A Ghost Buster 500 without a destabiliser loaded.\"\n- id: 9913 # White destabiliser\n  examine: \"A white destabiliser for use in a Ghost Buster.\"\n- id: 9914 # Red destabiliser\n  examine: \"A red destabiliser for use in a Ghost Buster.\"\n- id: 9915 # Blue destabiliser\n  examine: \"A blue destabiliser for use in a Ghost Buster.\"\n- id: 9916 # Green destabiliser\n  examine: \"A green destabiliser for use in a Ghost Buster.\"\n- id: 9917 # Yellow destabiliser\n  examine: \"A yellow destabiliser for use in a Ghost Buster.\"\n- id: 9918 # Black destabiliser\n  examine: \"A black destabiliser for use in a Ghost Buster.\"\n- id: 9919 # Evil root\n  examine: \"A freshly cut root of all evil.\"\n- id: 9920 # Jack lantern mask\n  examine: \"Better not light it!\"\n- id: 9921 # Skeleton boots\n  examine: \"Skeleton feet.\"\n- id: 9922 # Skeleton gloves\n  examine: \"Some skeletal gloves.\"\n- id: 9923 # Skeleton leggings\n  examine: \"Does my pelvis look big in this?\"\n- id: 9924 # Skeleton shirt\n  examine: \"The shirt of a full body skeleton costume.\"\n- id: 9925 # Skeleton mask\n  examine: \"A scary skeleton mask.\"\n"
  },
  {
    "path": "data/config/item-spawns/lumbridge/lumbridge.json",
    "content": "[\n    {\n        \"item\": \"rs:coins\",\n        \"amount\": 25,\n        \"spawn_x\": 3211,\n        \"spawn_y\": 3240,\n        \"instance\": \"global\",\n        \"respawn\": 25,\n        \"metadata\": {\n            \"hello\": \"world\"\n        }\n    },\n    {\n        \"item\": \"rs:egg\",\n        \"spawn_x\": 3191,\n        \"spawn_y\": 3276\n    },\n    {\n        \"item\": \"rs:egg\",\n        \"spawn_x\": 3226,\n        \"spawn_y\": 3300\n    },\n    {\n        \"item\": \"rs:egg\",\n        \"spawn_x\": 3228,\n        \"spawn_y\": 3299\n    },\n    {\n        \"item\": \"rs:egg\",\n        \"spawn_x\": 3231,\n        \"spawn_y\": 3301,\n        \"instance\": \"player\"\n    }\n]\n"
  },
  {
    "path": "data/config/items/barrows/dharoks.json",
    "content": "{\n    \"rs:dharoks_axe\": {\n        \"tradable\": true,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 70,\n                    \"strength\": 70\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": -4,\n                \"slash\": 103,\n                \"crush\": 95,\n                \"magic\": -4\n            },\n            \"defensive_bonuses\": {\n                \"ranged\": -1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 105\n            }\n        },\n        \"variations\": [\n            {\n                \"game_id\": 4718,\n                \"suffix\": \"0\"\n            },\n            {\n                \"tradable\": false,\n                \"game_id\": 4886,\n                \"suffix\": \"1\"\n            },\n            {\n                \"tradable\": false,\n                \"game_id\": 4887,\n                \"suffix\": \"2\"\n            },\n            {\n                \"tradable\": false,\n                \"game_id\": 4888,\n                \"suffix\": \"3\"\n            },\n            {\n                \"tradable\": false,\n                \"game_id\": 4889,\n                \"suffix\": \"4\"\n            },\n            {\n                \"game_id\": 4890,\n                \"suffix\": \"5\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/items/bolts.json",
    "content": "{\n    \"rs:bronze_bolt\": {\n        \"game_id\": 877\n    },\n    \"rs:opal_bolt\": {\n        \"game_id\": 879\n    },\n    \"rs:blurite_bolt\": {\n        \"game_id\": 9139\n    },\n    \"rs:jade_bolt\": {\n        \"game_id\": 9237\n    },\n    \"rs:iron_bolt\": {\n        \"game_id\": 9140\n    },\n    \"rs:silver_bolt\": {\n        \"game_id\": 9145\n    },\n    \"rs:steel_bolt\": {\n        \"game_id\": 9141\n    },\n    \"rs:mithril_bolt\": {\n        \"game_id\": 9142\n    },\n    \"rs:sapphire_bolt\": {\n        \"game_id\": 9337\n    },\n    \"rs:emerald_bolt\": {\n        \"game_id\": 9338\n    },\n    \"rs:adamant_bolt\": {\n        \"game_id\": 9143\n    },\n    \"rs:runite_bolt\": {\n        \"game_id\": 9144\n    },\n    \"rs:onyx_bolt\": {\n        \"game_id\": 9342\n    }\n}\n"
  },
  {
    "path": "data/config/items/bones.json",
    "content": "{\n    \"rs:bones\": {\n        \"game_id\": 526,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 4.5,\n            \"wikiId\": \"Bones\"\n        }\n    },\n    \"rs:bones_burnt\": {\n        \"game_id\": 528,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 4.5,\n            \"wikiId\": \"Burnt_bones\"\n        }\n    },\n    \"rs:bones_wolf\": {\n        \"game_id\": 2859,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 4.5,\n            \"wikiId\": \"Wolf_bones\"\n        }\n    },\n    \"rs:bones_bat\": {\n        \"game_id\": 530,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 5.3,\n            \"wikiId\": \"Bat_bones\"\n        }\n    },\n    \"rs:bones_big\": {\n        \"game_id\": 532,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 15,\n            \"wikiId\": \"Big_bones\"\n        }\n    },\n    \"rs:bones_dagannoth\": {\n        \"game_id\": 6729,\n        \"tradable\": true,\n        \"weight\": 1.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 125,\n            \"wikiId\": \"Dagannoth_bones\"\n        }\n    },\n    \"rs:bones_babydragon\": {\n        \"game_id\": 534,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 30,\n            \"wikiId\": \"Babydragon_bones\"\n        }\n    },\n    \"rs:bones_dragon\": {\n        \"game_id\": 536,\n        \"tradable\": true,\n        \"weight\": 1.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 72,\n            \"wikiId\": \"Dragon_bones\"\n        }\n    },\n    \"rs:bones_wyvern\": {\n        \"game_id\": 6812,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 72,\n            \"wikiId\": \"Wyvern_bones\"\n        }\n    },\n    \"rs:bones_monkey_normal\": {\n        \"game_id\": 3183,\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 5,\n            \"wikiId\": \"Monkey_bones\"\n        }\n    },\n    \"rs:bones_monkey_small_zombie\": {\n        \"game_id\": 3185,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 5,\n            \"wikiId\": \"Small_zombie_monkey_bones\"\n        }\n    },\n    \"rs:bones_monkey_large_zombie\": {\n        \"game_id\": 3186,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 5,\n            \"wikiId\": \"Large_zombie_monkey_bones\"\n        }\n    },\n    \"rs:bones_monkey_gorilla\": {\n        \"game_id\": 3181,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 18,\n            \"wikiId\": \"Gorilla_bones\"\n        }\n    },\n    \"rs:bones_monkey_bearded_gorilla\": {\n        \"game_id\": 3182,\n        \"tradable\": false,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 18,\n            \"wikiId\": \"Bearded_gorilla_bones\"\n        }\n    },\n    \"rs:bones_monkey_small_ninja\": {\n        \"game_id\": 3179,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 16,\n            \"wikiId\": \"Small_ninja_monkey_bones\"\n        }\n    },\n    \"rs:bones_monkey_medium_ninja\": {\n        \"game_id\": 3180,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 18,\n            \"wikiId\": \"Medium_ninja_monkey_bones\"\n        }\n    },\n    \"rs:bones_monkey_skeleton_gorilla\": {\n        \"game_id\": 3187,\n        \"tradable\": false,\n        \"weight\": 0.5,\n        \"metadata\": {\n            \"prayerBuryXp\": 3,\n            \"wikiId\": \"Bones_(Ape_Atoll)\"\n        }\n    },\n    \"rs:bones_jogre\": {\n        \"game_id\": 3152,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 15,\n            \"wikiId\": \"Jogre_bones\"\n        }\n    },\n    \"rs:bones_zogre\": {\n        \"game_id\": 4812,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 22.5,\n            \"wikiId\": \"Zogre_bones\"\n        }\n    },\n    \"rs:bones_fayrg\": {\n        \"game_id\": 4830,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 84,\n            \"wikiId\": \"Fayrg_bones\"\n        }\n    },\n    \"rs:bones_raurg\": {\n        \"game_id\": 4832,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 96,\n            \"wikiId\": \"Raurg_bones\"\n        }\n    },\n    \"rs:bones_ourg\": {\n        \"game_id\": 4834,\n        \"tradable\": true,\n        \"weight\": 0.8,\n        \"metadata\": {\n            \"prayerBuryXp\": 140,\n            \"wikiId\": \"Ourg_bones\"\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/containers.json",
    "content": "{\n    \"rs:pot\": {\n        \"game_id\": 1931,\n        \"examine\": \"This pot is empty.\",\n        \"tradable\": true,\n        \"weight\": 0.453\n    },\n    \"rs:vial\": {\n        \"game_id\": 229,\n        \"examine\": \"An empty glass vial.\",\n        \"tradable\": true,\n        \"weight\": 0.015,\n        \"variations\": [\n            {\n                \"game_id\": 227,\n                \"suffix\": \"water\",\n                \"examine\": \"A glass vial containing water.\"\n            }\n        ]\n    },\n    \"rs:jug\": {\n        \"game_id\": 1935,\n        \"examine\": \"This jug is empty.\",\n        \"tradable\": true,\n        \"weight\": 0.453\n    },\n    \"rs:bucket\": {\n        \"game_id\": 1925,\n        \"examine\": \"It's a wooden bucket.\",\n        \"tradable\": true,\n        \"weight\": 1\n    },\n    \"rs:bowl\": {\n        \"game_id\": 1923,\n        \"examine\": \"Useful for mixing things.\",\n        \"tradable\": true\n    },\n    \"rs:pie_dish\": {\n        \"game_id\": 2313,\n        \"examine\": \"Deceptively pie shaped.\",\n        \"tradable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/currency.json",
    "content": "{\n    \"rs:coins\": {\n        \"game_id\": 995,\n        \"tradable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/amulets.json",
    "content": "{\n    \"rs:amulet_of_glory\": {\n        \"game_id\": 1704,\n        \"examine\": \"A very powerful dragonstone amulet.\",\n        \"tradable\": true,\n        \"weight\": 0.01,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"neck\",\n            \"offensive_bonuses\": {\n                \"stab\": 10,\n                \"slash\": 10,\n                \"crush\": 10,\n                \"magic\": 10,\n                \"ranged\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 3,\n                \"slash\": 3,\n                \"crush\": 3,\n                \"magic\": 3,\n                \"ranged\": 3\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 3,\n                \"strength\": 6,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        },\n        \"variations\": [\n            {\n                \"game_id\": 1706,\n                \"examine\": \"A dragonstone amulet with 1 magic charge.\",\n                \"suffix\": \"charged_1\"\n            },\n            {\n                \"game_id\": 1708,\n                \"examine\": \"A dragonstone amulet with 2 magic charges.\",\n                \"suffix\": \"charged_2\"\n            },\n            {\n                \"game_id\": 1710,\n                \"examine\": \"A dragonstone amulet with 3 magic charges.\",\n                \"suffix\": \"charged_3\"\n            },\n            {\n                \"game_id\": 1712,\n                \"examine\": \"A dragonstone amulet with 4 magic charges.\",\n                \"suffix\": \"charged_4\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/axes.json",
    "content": "{\n    \"rs:dharoks_greataxe\": {\n        \"game_id\": 4718,\n        \"examine\": \"Dharok the Wretched's greataxe.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": -4,\n                \"slash\": 103,\n                \"crush\": 95,\n                \"magic\": -4\n            },\n            \"defensive_bonuses\": {\n                \"ranged\": -1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 105\n            },\n            \"weapon_info\": {\n                \"style\": \"axe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/bows.json",
    "content": "{\n    \"rs:shortbow\": {\n        \"game_id\": 50\n    },\n    \"rs:longbow\": {\n        \"game_id\": 48\n    },\n    \"rs:oak_shortbow\": {\n        \"game_id\": 54\n    },\n    \"rs:oak_longbow\": {\n        \"game_id\": 56\n    },\n    \"rs:willow_shortbow\": {\n        \"game_id\": 60\n    },\n    \"rs:willow_longbow\": {\n        \"game_id\": 58\n    },\n    \"rs:maple_shortbow\": {\n        \"game_id\": 64\n    },\n    \"rs:maple_longbow\": {\n        \"game_id\": 62\n    },\n    \"rs:yew_shortbow\": {\n        \"game_id\": 68\n    },\n    \"rs:yew_longbow\": {\n        \"game_id\": 66\n    },\n    \"rs:magic_shortbow\": {\n        \"game_id\": 72\n    },\n    \"rs:magic_longbow\": {\n        \"game_id\": 70\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/capes.json",
    "content": "{\n    \"rs:cape_of_legends\": {\n        \"game_id\": 1052,\n        \"examine\": \"The cape worn by members of the Legends Guild.\",\n        \"tradable\": false,\n        \"weight\": 1.814,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"back\",\n            \"defensive_bonuses\": {\n                \"stab\": 7,\n                \"slash\": 7,\n                \"crush\": 7,\n                \"magic\": 7,\n                \"ranged\": 7\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/daggers.json",
    "content": "{\n    \"rs:iron_dagger\": {\n        \"game_id\": 1203,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true\n    },\n    \"rs:steel_dagger\": {\n        \"game_id\": 1207,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true\n    },\n    \"rs:black_dagger\": {\n        \"game_id\": 1217,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true\n    },\n    \"rs:white_dagger\": {\n        \"game_id\": 6591,\n        \"examine\": \"A vicious white dagger.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 10,\n                \"slash\": 5,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        }\n    },\n    \"rs:mithril_dagger\": {\n        \"game_id\": 1209,\n        \"examine\": \"A dangerous dagger.\",\n        \"tradable\": true,\n        \"weight\": 0.396,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 11,\n                \"slash\": 5,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 10\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        }\n    },\n    \"rs:adamant_dagger\": {\n        \"game_id\": 1211,\n        \"examine\": \"Short and deadly.\",\n        \"tradable\": true,\n        \"weight\": 0.51,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 15,\n                \"slash\": 8,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 14\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        }\n    },\n    \"rs:rune_dagger\": {\n        \"game_id\": 1213,\n        \"examine\": \"Short and deadly.\",\n        \"tradable\": true,\n        \"weight\": 0.51,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 25,\n                \"slash\": 12,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 24\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        }\n    },\n    \"rs:dragon_dagger\": {\n        \"game_id\": 1215,\n        \"examine\": \"A powerful dagger.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/darts.json",
    "content": "{\n    \"rs:bronze_dart\": {\n        \"game_id\": 806\n    },\n    \"rs:iron_dart\": {\n        \"game_id\": 807\n    },\n    \"rs:steel_dart\": {\n        \"game_id\": 808\n    },\n    \"rs:mithril_dart\": {\n        \"game_id\": 809\n    },\n    \"rs:adamant_dart\": {\n        \"game_id\": 810\n    },\n    \"rs:rune_dart\": {\n        \"game_id\": 811\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/halberd.json",
    "content": "{\n    \"rs:iron_halberd\": {\n        \"game_id\": 3192,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 9,\n                \"slash\": 12,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 1,\n                \"crush\": 2,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 12,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:steel_halberd\": {\n        \"game_id\": 3194,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 14,\n                \"slash\": 19,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 1,\n                \"crush\": 2,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 20,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:black_halberd\": {\n        \"game_id\": 3196,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 19,\n                \"slash\": 25,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 2,\n                \"crush\": 3,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 20,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:white_halberd\": {\n        \"game_id\": 6599,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 19,\n                \"slash\": 25,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 2,\n                \"crush\": 3,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 20,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:mithril_halberd\": {\n        \"game_id\": 3198,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 22,\n                \"slash\": 28,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 2,\n                \"crush\": 4,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 29,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:adamant_halberd\": {\n        \"game_id\": 3200,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 28,\n                \"slash\": 41,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 3,\n                \"crush\": 4,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 42,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:rune_halberd\": {\n        \"game_id\": 3202,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 48,\n                \"slash\": 67,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 4,\n                \"crush\": 5,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 68,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    },\n    \"rs:dragon_halberd\": {\n        \"game_id\": 3204,\n        \"examine\": \"Short but pointy.\",\n        \"tradable\": true,\n        \"weight\": 3.175,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 70,\n                \"slash\": 95,\n                \"crush\": 0,\n                \"magic\": -4,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": -1,\n                \"slash\": 4,\n                \"crush\": 5,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"strength\": 89,\n                \"prayer\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"halberd\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/hammers.json",
    "content": "{\n    \"rs:torags_hammers\": {\n        \"game_id\": 4747,\n        \"examine\": \"Torag the Corrupted's twin hammers.\",\n        \"tradable\": true,\n        \"notable\": true,\n        \"stackable\": false,\n        \"equippable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/hatchets.json",
    "content": "{\n    \"presets\": {\n        \"rs:woodcutting_axe\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"main_hand\",\n                \"equipment_type\": \"one_handed\",\n                \"offensive_bonuses\": {\n                    \"speed\": 5,\n                    \"stab\": -2\n                },\n                \"defensive_bonuses\": {\n                    \"slash\": 1\n                },\n                \"weapon_info\": {\n                    \"style\": \"axe\"\n                }\n            }\n        }\n    },\n\n    \"rs:bronze_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1351,\n        \"weight\": 1.35,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"slash\": 4,\n                \"crush\": 2\n            },\n            \"skill_bonuses\": {\n                \"strength\": 5\n            }\n        }\n    },\n    \"rs:iron_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1349,\n        \"weight\": 1.36,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"slash\": 5,\n                \"crush\": 3\n            },\n            \"skill_bonuses\": {\n                \"strength\": 7\n            }\n        }\n    },\n    \"rs:black_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1361,\n        \"weight\": 1.36,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"slash\": 5,\n                \"crush\": 3\n            },\n            \"skill_bonuses\": {\n                \"strength\": 7\n            }\n        }\n    },\n    \"rs:dragon_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 6739,\n        \"weight\": 1.36,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"slash\": 5,\n                \"crush\": 3\n            },\n            \"skill_bonuses\": {\n                \"strength\": 7\n            }\n        }\n    },\n    \"rs:steel_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1353,\n        \"weight\": 1.36,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 5\n                }\n            },\n            \"offensive_bonuses\": {\n                \"slash\": 8,\n                \"crush\": 6\n            },\n            \"skill_bonuses\": {\n                \"strength\": 9\n            }\n        }\n    },\n    \"rs:mithril_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1355,\n        \"weight\": 1.133,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 20\n                }\n            },\n            \"offensive_bonuses\": {\n                \"slash\": 12,\n                \"crush\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 13\n            }\n        }\n    },\n    \"rs:adamant_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1357,\n        \"weight\": 1.587,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 30\n                }\n            },\n            \"offensive_bonuses\": {\n                \"slash\": 17,\n                \"crush\": 15\n            },\n            \"skill_bonuses\": {\n                \"strength\": 19\n            }\n        }\n    },\n    \"rs:rune_axe\": {\n        \"extends\": \"rs:woodcutting_axe\",\n        \"game_id\": 1359,\n        \"weight\": 1,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 40\n                }\n            },\n            \"offensive_bonuses\": {\n                \"slash\": 26,\n                \"crush\": 24\n            },\n            \"skill_bonuses\": {\n                \"strength\": 29\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/hats.json",
    "content": "{\n    \"rs:wizard_hat_(blue)\": {\n        \"game_id\": 579,\n        \"weight\": 0.453,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"magic\": 2\n            },\n            \"skill_bonuses\": {\n                \"magic\": 2\n            }\n        }\n    },\n    \"rs:wizard_hat_(black)\": {\n        \"game_id\": 1017,\n        \"weight\": 0.453,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"magic\": 2\n            },\n            \"skill_bonuses\": {\n                \"magic\": 2\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/javelins.json",
    "content": "{\n    \"rs:bronze_javelin\": {\n        \"game_id\": 52\n    },\n    \"rs:iron_javelin\": {\n        \"game_id\": 52\n    },\n    \"rs:steel_javelin\": {\n        \"game_id\": 52\n    },\n    \"rs:mithril_javelin\": {\n        \"game_id\": 52\n    },\n    \"rs:adamant_javelin\": {\n        \"game_id\": 52\n    },\n    \"rs:rune_javelin\": {\n        \"game_id\": 52\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/leather-armour.json",
    "content": "{\n    \"rs:leather_cowl\": {\n        \"game_id\": 1167,\n        \"examine\": \"Better than no armour!\",\n        \"tradable\": true,\n        \"weight\": 0.907,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 1,\n                    \"defence\": 1\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 1\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 4,\n                \"magic\": 2,\n                \"ranged\": 3\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:leather_body\": {\n        \"game_id\": 1129,\n        \"examine\": \"Better than no armour!\",\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 1,\n                    \"defence\": 1\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": -2,\n                \"ranged\": 2\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 8,\n                \"slash\": 9,\n                \"crush\": 10,\n                \"magic\": 4,\n                \"ranged\": 9\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:leather_chaps\": {\n        \"game_id\": 1095,\n        \"examine\": \"Better than no armour!\",\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 1,\n                    \"defence\": 1\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 4\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 2,\n                \"crush\": 1,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:leather_vambraces\": {\n        \"game_id\": 1063,\n        \"examine\": \"Better than no armour!\",\n        \"tradable\": true,\n        \"weight\": 0.226,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"hands\",\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 1\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 4\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 2,\n                \"crush\": 1,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:leather_boots\": {\n        \"game_id\": 1061,\n        \"examine\": \"Comfortable leather boots.\",\n        \"tradable\": true,\n        \"weight\": 0.34,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"feet\",\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 1\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 1,\n                \"crush\": 1,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/longswords.json",
    "content": "{\n    \"rs:dragon_longsword\": {\n        \"game_id\": 1305,\n        \"examine\": \"A very powerful sword.\",\n        \"tradable\": true,\n        \"weight\": 1.814,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 60\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 58,\n                \"slash\": 69,\n                \"crush\": -2,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 3,\n                \"crush\": 2,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 71,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/mauls.json",
    "content": "{\n    \"rs:granite_maul\": {\n        \"game_id\": 4153,\n        \"examine\": \"Simplicity is the best weapon.\",\n        \"tradable\": true,\n        \"weight\": 4.535,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"two_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 81,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 79\n            },\n            \"weapon_info\": {\n                \"style\": \"mace\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/pickaxes.json",
    "content": "{\n    \"presets\": {\n        \"rs:pickaxe_base\": {\n            \"examine\": \"Used for mining.\",\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"main_hand\",\n                \"equipment_type\": \"one_handed\",\n                \"offensive_bonuses\": {\n                    \"speed\": 5,\n                    \"slash\": -2\n                },\n                \"defensive_bonuses\": {\n                    \"slash\": 1\n                },\n                \"weapon_info\": {\n                    \"style\": \"pickaxe\"\n                }\n            }\n        }\n    },\n\n    \"rs:bronze_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1265,\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"stab\": 4,\n                \"crush\": 2\n            },\n            \"skill_bonuses\": {\n                \"strength\": 5\n            }\n        }\n    },\n    \"rs:iron_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1267,\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"stab\": 5,\n                \"crush\": 3\n            },\n            \"skill_bonuses\": {\n                \"strength\": 7\n            }\n        }\n    },\n    \"rs:steel_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1269,\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 5\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 8,\n                \"crush\": 6\n            },\n            \"skill_bonuses\": {\n                \"strength\": 9\n            }\n        }\n    },\n    \"rs:mithril_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1273,\n        \"weight\": 1.814,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 20\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 12,\n                \"crush\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 13\n            }\n        }\n    },\n    \"rs:adamant_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1271,\n        \"weight\": 2.721,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 30\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 17,\n                \"crush\": 15\n            },\n            \"skill_bonuses\": {\n                \"strength\": 19\n            }\n        }\n    },\n    \"rs:rune_pickaxe\": {\n        \"extends\": \"rs:pickaxe_base\",\n        \"game_id\": 1275,\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 40\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 26,\n                \"crush\": 24\n            },\n            \"skill_bonuses\": {\n                \"strength\": 29\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/shortswords.json",
    "content": "{\n    \"rs:toktz_xil_ak\": {\n        \"game_id\": 6523,\n        \"examine\": \"A razor sharp sword of obsidian.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 10,\n                \"slash\": 5,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            },\n            \"weapon_info\": {\n                \"style\": \"longsword\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/skillscapes.json",
    "content": "{\n    \"presets\": {\n        \"rs:skillcape\": {\n            \"tradable\": false,\n            \"weight\": 0.453,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"back\",\n                \"defensive_bonuses\": {\n                    \"stab\": 9,\n                    \"slash\": 9,\n                    \"crush\": 9,\n                    \"magic\": 9,\n                    \"ranged\": 9\n                }\n            }\n        },\n        \"rs:skillcape_hood\": {\n            \"tradable\": false,\n            \"weight\": 0.453,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"head\"\n            }\n        }\n    },\n\n    \"rs:attack_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9747,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 99\n                }\n            }\n        }\n    },\n    \"rs:attack_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9748,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 99\n                }\n            }\n        }\n    },\n    \"rs:attack_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9749,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 99\n                }\n            }\n        }\n    },\n    \"rs:strength_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9750,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"strength\": 99\n                }\n            }\n        }\n    },\n    \"rs:strength_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9751,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"strength\": 99\n                }\n            }\n        }\n    },\n    \"rs:strength_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9752,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"strength\": 99\n                }\n            }\n        }\n    },\n    \"rs:defence_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9753,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 99\n                }\n            }\n        }\n    },\n    \"rs:defence_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9754,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 99\n                }\n            }\n        }\n    },\n    \"rs:defence_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9755,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 99\n                }\n            }\n        }\n    },\n    \"rs:ranged_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9756,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 99\n                }\n            }\n        }\n    },\n    \"rs:ranged_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9757,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 99\n                }\n            }\n        }\n    },\n    \"rs:ranged_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9758,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"ranged\": 99\n                }\n            }\n        }\n    },\n    \"rs:prayer_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9759,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"prayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:prayer_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9760,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"prayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:prayer_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9761,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"prayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:magic_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9762,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"magic\": 99\n                }\n            }\n        }\n    },\n    \"rs:magic_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9763,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"magic\": 99\n                }\n            }\n        }\n    },\n    \"rs:magic_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9764,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"magic\": 99\n                }\n            }\n        }\n    },\n    \"rs:runecrafting_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9765,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"runecrafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:runecrafting_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9766,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"runecrafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:runecrafting_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9767,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"runecrafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:hitpoints_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9768,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"hitpoints\": 99\n                }\n            }\n        }\n    },\n    \"rs:hitpoints_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9769,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"hitpoints\": 99\n                }\n            }\n        }\n    },\n    \"rs:hitpoints_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9770,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"hitpoints\": 99\n                }\n            }\n        }\n    },\n    \"rs:agility_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9771,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"agility\": 99\n                }\n            }\n        }\n    },\n    \"rs:agility_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9772,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"agility\": 99\n                }\n            }\n        }\n    },\n    \"rs:agility_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9773,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"agility\": 99\n                }\n            }\n        }\n    },\n    \"rs:herblore_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9774,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"herblore\": 99\n                }\n            }\n        }\n    },\n    \"rs:herblore_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9775,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"herblore\": 99\n                }\n            }\n        }\n    },\n    \"rs:herblore_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9776,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"herblore\": 99\n                }\n            }\n        }\n    },\n    \"rs:thieving_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9777,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"thieving\": 99\n                }\n            }\n        }\n    },\n    \"rs:thieving_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9778,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"thieving\": 99\n                }\n            }\n        }\n    },\n    \"rs:thieving_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9779,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"thieving\": 99\n                }\n            }\n        }\n    },\n    \"rs:crafting_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9780,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"crafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:crafting_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9781,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"crafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:crafting_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9782,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"crafting\": 99\n                }\n            }\n        }\n    },\n    \"rs:fletching_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9783,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fletching\": 99\n                }\n            }\n        }\n    },\n    \"rs:fletching_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9784,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fletching\": 99\n                }\n            }\n        }\n    },\n    \"rs:fletching_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9785,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fletching\": 99\n                }\n            }\n        }\n    },\n    \"rs:slayer_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9786,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:slayer_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9787,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:slayer_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9788,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 99\n                }\n            }\n        }\n    },\n    \"rs:construction_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9789,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"construction\": 99\n                }\n            }\n        }\n    },\n    \"rs:construction_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9790,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"construction\": 99\n                }\n            }\n        }\n    },\n    \"rs:construction_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9791,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"construction\": 99\n                }\n            }\n        }\n    },\n    \"rs:mining_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9792,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"mining\": 99\n                }\n            }\n        }\n    },\n    \"rs:mining_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9793,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"mining\": 99\n                }\n            }\n        }\n    },\n    \"rs:mining_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9794,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"mining\": 99\n                }\n            }\n        }\n    },\n    \"rs:smithing_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9795,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"smithing\": 99\n                }\n            }\n        }\n    },\n    \"rs:smithing_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9796,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"smithing\": 99\n                }\n            }\n        }\n    },\n    \"rs:smithing_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9797,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"smithing\": 99\n                }\n            }\n        }\n    },\n    \"rs:fishing_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9798,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fishing\": 99\n                }\n            }\n        }\n    },\n    \"rs:fishing_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9799,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fishing\": 99\n                }\n            }\n        }\n    },\n    \"rs:fishing_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9800,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"fishing\": 99\n                }\n            }\n        }\n    },\n    \"rs:cooking_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9801,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"cooking\": 99\n                }\n            }\n        }\n    },\n    \"rs:cooking_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9802,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"cooking\": 99\n                }\n            }\n        }\n    },\n    \"rs:cooking_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9803,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"cooking\": 99\n                }\n            }\n        }\n    },\n    \"rs:firemaking_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9804,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"firemaking\": 99\n                }\n            }\n        }\n    },\n    \"rs:firemaking_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9805,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"firemaking\": 99\n                }\n            }\n        }\n    },\n    \"rs:firemaking_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9806,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"firemaking\": 99\n                }\n            }\n        }\n    },\n    \"rs:woodcutting_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9807,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"woodcutting\": 99\n                }\n            }\n        }\n    },\n    \"rs:woodcutting_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9808,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"woodcutting\": 99\n                }\n            }\n        }\n    },\n    \"rs:woodcutting_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9809,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"woodcutting\": 99\n                }\n            }\n        }\n    },\n    \"rs:farming_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9810,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"farming\": 99\n                }\n            }\n        }\n    },\n    \"rs:farming_cape_t\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9811,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"farming\": 99\n                }\n            }\n        }\n    },\n    \"rs:farming_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9812,\n        \"equipment_data\": {\n            \"requirements\": {\n                \"skills\": {\n                    \"farming\": 99\n                }\n            }\n        }\n    },\n    \"rs:quest_cape\": {\n        \"extends\": \"rs:skillcape\",\n        \"game_id\": 9813\n    },\n    \"rs:quest_hood\": {\n        \"extends\": \"rs:skillcape_hood\",\n        \"game_id\": 9814\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/slayer.json",
    "content": "{\n    \"rs:black_mask\": {\n        \"game_id\": 8921,\n        \"examine\": \"An inert-seeming cave horror mask.\",\n        \"tradable\": true,\n        \"weight\": 10,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"requirements\": {\n                \"skills\": {\n                    \"defence\": 10,\n                    \"strength\": 20,\n                    \"combat\": 40\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": -3,\n                \"ranged\": -1\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 9,\n                \"slash\": 10,\n                \"crush\": 8,\n                \"magic\": -1,\n                \"ranged\": 9\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        },\n        \"variations\": [\n            {\n                \"game_id\": 8919,\n                \"suffix\": \"1\"\n            },\n            {\n                \"game_id\": 8917,\n                \"suffix\": \"2\"\n            },\n            {\n                \"game_id\": 8915,\n                \"suffix\": \"3\"\n            },\n            {\n                \"game_id\": 8913,\n                \"suffix\": \"4\"\n            },\n            {\n                \"game_id\": 8911,\n                \"suffix\": \"5\"\n            },\n            {\n                \"game_id\": 8909,\n                \"suffix\": \"6\"\n            },\n            {\n                \"game_id\": 8907,\n                \"suffix\": \"7\"\n            },\n            {\n                \"game_id\": 8905,\n                \"suffix\": \"8\"\n            },\n            {\n                \"game_id\": 8903,\n                \"suffix\": \"9\"\n            },\n            {\n                \"game_id\": 8901,\n                \"suffix\": \"10\"\n            }\n        ]\n    },\n    \"rs:witchwood_icon\": {\n        \"game_id\": 8923,\n        \"examine\": \"A stick on a string... pure style.\",\n        \"tradable\": false,\n        \"weight\": 0.007,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"neck\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 35\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 1,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 1,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:slayer_staff\": {\n        \"game_id\": 4170,\n        \"examine\": \"An old and magical staff.\",\n        \"tradable\": true,\n        \"weight\": 1.814,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 55,\n                    \"magic\": 50\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 7,\n                \"slash\": -1,\n                \"crush\": 25,\n                \"magic\": 12,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 35,\n                \"ranged\": 0,\n                \"magic\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"magical_staff\"\n            }\n        }\n    },\n    \"rs:nose_peg\": {\n        \"game_id\": 4168,\n        \"examine\": \"Protects me from any bad smells.\",\n        \"tradable\": true,\n        \"weight\": 0.001,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 60\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:ear_muffs\": {\n        \"game_id\": 4166,\n        \"examine\": \"These will protect my ears from loud noise.\",\n        \"tradable\": true,\n        \"weight\": 0.113,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 15\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:face_mask\": {\n        \"game_id\": 4164,\n        \"examine\": \"Stops me breathing in too much dust.\",\n        \"tradable\": true,\n        \"weight\": 0.113,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 10\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:rock_hammer\": {\n        \"game_id\": 4162,\n        \"examine\": \"I can even smash stone with this.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:bag_of_salt\": {\n        \"game_id\": 4161,\n        \"examine\": \"A bag of salt.\",\n        \"tradable\": true,\n        \"weight\": 0,\n        \"equippable\": false\n    },\n    \"rs:broad_arrows\": {\n        \"game_id\": 4160,\n        \"examine\": \"Arrows with a wider than normal tip.\",\n        \"tradable\": false,\n        \"weight\": 0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"quiver\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 55,\n                    \"ranged\": 50\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 28,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:leaf_bladed_spear\": {\n        \"game_id\": 4158,\n        \"examine\": \"A spear with a leaf-shaped point.\",\n        \"tradable\": false,\n        \"weight\": 2.267,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"2h\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 55,\n                    \"attack\": 50\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 47,\n                \"slash\": 42,\n                \"crush\": 36,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 1,\n                \"slash\": 1,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 50,\n                \"ranged\": 0,\n                \"magic\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"spear\"\n            }\n        }\n    },\n    \"rs:mirror_shield\": {\n        \"game_id\": 4156,\n        \"examine\": \"I can just about see things in this shield's reflection.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"requirements\": {\n                \"skills\": {\n                    \"slayer\": 25,\n                    \"defence\": 20\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 10,\n                \"slash\": 15,\n                \"crush\": 5,\n                \"magic\": 5,\n                \"ranged\": 10\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 0,\n                \"ranged\": 0,\n                \"magic\": 0\n            }\n        }\n    },\n    \"rs:enchanted_gem\": {\n        \"game_id\": 4155,\n        \"examine\": \"I can contact the Slayer Masters with this.\",\n        \"tradable\": false,\n        \"weight\": 0.002,\n        \"equippable\": false\n    },\n    \"rs:granite_maul\": {\n        \"game_id\": 4153,\n        \"examine\": \"Simplicity is the best weapon.\",\n        \"tradable\": true,\n        \"weight\": 4.535,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"2h\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 50\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 7,\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 81,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 79,\n                \"ranged\": 0,\n                \"magic\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"hammer\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/spears.json",
    "content": "{\n    \"rs:guthans_warspear\": {\n        \"game_id\": 4726,\n        \"tradable\": true,\n        \"notable\": true,\n        \"stackable\": false,\n        \"equippable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/staffs.json",
    "content": "{\n    \"presets\": {\n        \"rs:staff_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"main_hand\",\n                \"equipment_type\": \"two_handed\",\n                \"weapon_info\": {\n                    \"style\": \"magical_staff\"\n                }\n            }\n        }\n    },\n\n    \"rs:battlestaff\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1391,\n        \"examine\": \"It's a slightly magical stick.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 7,\n                \"slash\": -1,\n                \"crush\": 25,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 32\n            },\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 30,\n                    \"magic\": 30\n                }\n            }\n        }\n    },\n\n    \"rs:staff\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1379,\n        \"examine\": \"It's a slightly magical stick.\",\n        \"weight\": 1.814,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 0,\n                \"slash\": -1,\n                \"crush\": 7,\n                \"magic\": 4\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 4\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            }\n        }\n    },\n\n    \"rs:magic_staff\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1389,\n        \"examine\": \"A Magical staff.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 2,\n                \"slash\": -1,\n                \"crush\": 10,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 7\n            }\n        }\n    },\n\n    \"rs:staff_of_air\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1381,\n        \"examine\": \"A Magical staff.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 0,\n                \"slash\": -1,\n                \"crush\": 7,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            }\n        }\n    },\n\n    \"rs:staff_of_water\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1383,\n        \"examine\": \"A Magical staff.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 0,\n                \"slash\": -1,\n                \"crush\": 7,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            }\n        }\n    },\n\n    \"rs:staff_of_earth\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1385,\n        \"examine\": \"A Magical staff.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 1,\n                \"slash\": -1,\n                \"crush\": 9,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 5\n            }\n        }\n    },\n\n    \"rs:staff_of_fire\": {\n        \"extends\": \"rs:staff_base\",\n        \"game_id\": 1387,\n        \"examine\": \"A Magical staff.\",\n        \"weight\": 2.267,\n        \"equipment_data\": {\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 3,\n                \"slash\": -1,\n                \"crush\": 9,\n                \"magic\": 10\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 2,\n                \"slash\": 3,\n                \"crush\": 1,\n                \"magic\": 10\n            },\n            \"skill_bonuses\": {\n                \"strength\": 6\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/adamant-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:adamant_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 30\n                    }\n                }\n            },\n            \"groups\": [\"adamant_metal\", \"equipment\"]\n        }\n    },\n\n    \"rs:adamant_platelegs\": {\n        \"extends\": \"rs:adamant_armour_base\",\n        \"game_id\": 1073,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 10.432,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 33,\n                \"slash\": 31,\n                \"crush\": 29,\n                \"magic\": -4,\n                \"ranged\": 31\n            }\n        },\n        \"groups\": [\"legs\"]\n    },\n\n    \"rs:adamant_plateskirt\": {\n        \"extends\": \"rs:adamant_armour_base\",\n        \"game_id\": 1091,\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 33,\n                \"slash\": 31,\n                \"crush\": 29,\n                \"magic\": -4,\n                \"ranged\": 31\n            }\n        },\n        \"groups\": [\"legs\"]\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/black-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:black_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 10\n                    }\n                }\n            },\n            \"groups\": [\"black_metal\", \"equipment\"]\n        }\n    },\n\n    \"rs:black_platelegs\": {\n        \"extends\": \"rs:black_armour_base\",\n        \"game_id\": 1077,\n        \"examine\": \"Big, black and heavy looking.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 21,\n                \"slash\": 20,\n                \"crush\": 19,\n                \"magic\": -4,\n                \"ranged\": 20\n            }\n        },\n        \"groups\": [\"legs\"]\n    },\n\n    \"rs:black_plateskirt\": {\n        \"extends\": \"rs:black_armour_base\",\n        \"game_id\": 1089,\n        \"examine\": \"Big, black and heavy looking.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 21,\n                \"slash\": 20,\n                \"crush\": 19,\n                \"magic\": -4,\n                \"ranged\": 20\n            }\n        },\n        \"groups\": [\"legs\"]\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/bronze-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:bronze_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 1\n                    }\n                }\n            },\n            \"groups\": [\"bronze_metal\", \"equipment\"]\n        }\n    },\n    \"rs:bronze_platelegs\": {\n        \"extends\": \"rs:bronze_armour_base\",\n        \"game_id\": 1075,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 9.071,\n        \"tradable\": true,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 8,\n                \"slash\": 7,\n                \"crush\": 6,\n                \"magic\": -4,\n                \"ranged\": 7\n            }\n        },\n        \"groups\": [\"legs\"]\n    },\n\n    \"rs:bronze_plateskirt\": {\n        \"game_id\": 1087,\n        \"extends\": \"rs:bronze_armour_base\",\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 8.164,\n        \"tradable\": true,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 8,\n                \"slash\": 7,\n                \"crush\": 6,\n                \"magic\": -4,\n                \"ranged\": 7\n            }\n        },\n        \"groups\": [\"legs\"]\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/bronze-weapons.json",
    "content": "{\n    \"presets\": {\n        \"rs:bronze_weapon_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 1\n                    }\n                }\n            },\n            \"groups\": [\"bronze_metal\", \"equipment\", \"weapon\"]\n        }\n    },\n    \"rs:bronze_dagger\": {\n        \"game_id\": 1205,\n        \"extends\": \"rs:bronze_weapon_base\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"requirements\": {\n                \"quests\": {\n                    \"tyn:goblin_diplomacy\": 20\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 4,\n                \"slash\": 2,\n                \"crush\": -4,\n                \"magic\": 1\n            },\n            \"defensive_bonuses\": {\n                \"magic\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 3\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        },\n        \"groups\": [\"main_hand\", \"dagger\"]\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/iron-armour.json",
    "content": "{\n    \"rs:iron_platelegs\": {\n        \"game_id\": 1067,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 9.071,\n        \"tradable\": true,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 11,\n                \"slash\": 10,\n                \"crush\": 10,\n                \"magic\": -4,\n                \"ranged\": 10\n            }\n        }\n    },\n\n    \"rs:iron_plateskirt\": {\n        \"game_id\": 1081,\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 8.164,\n        \"tradable\": true,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 11,\n                \"slash\": 10,\n                \"crush\": 10,\n                \"magic\": -4,\n                \"ranged\": 10\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/iron-weapons.json",
    "content": "{\n    \"rs:iron_battleaxe\": {\n        \"game_id\": 1363,\n        \"examine\": \"A vicious looking axe.\",\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 6,\n                \"stab\": -2,\n                \"slash\": 8,\n                \"crush\": 5\n            },\n            \"defensive_bonuses\": {\n                \"ranged\": -1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 13\n            },\n            \"weapon_info\": {\n                \"style\": \"axe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/mithril-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:mithril_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 20\n                    }\n                }\n            }\n        }\n    },\n\n    \"rs:mithril_platelegs\": {\n        \"extends\": \"rs:mithril_armour_base\",\n        \"game_id\": 1071,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 7.711,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 24,\n                \"slash\": 22,\n                \"crush\": 20,\n                \"magic\": -4,\n                \"ranged\": 22\n            }\n        }\n    },\n\n    \"rs:mithril_plateskirt\": {\n        \"extends\": \"rs:mithril_armour_base\",\n        \"game_id\": 1085,\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 7.257,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 24,\n                \"slash\": 22,\n                \"crush\": 20,\n                \"magic\": -4,\n                \"ranged\": 22\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/mithril-weapons.json",
    "content": "{\n    \"rs:mithril_battleaxe\": {\n        \"game_id\": 1369,\n        \"examine\": \"A vicious looking axe.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 20\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 6,\n                \"stab\": -2,\n                \"slash\": 22,\n                \"crush\": 17\n            },\n            \"defensive_bonuses\": {\n                \"ranged\": -1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 29\n            },\n            \"weapon_info\": {\n                \"style\": \"axe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/rune-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:rune_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 40\n                    }\n                }\n            }\n        }\n    },\n\n    \"rs:rune_gloves\": {\n        \"game_id\": 7460,\n        \"examine\": \"A pair of very nice gloves.\",\n        \"weight\": 0.226,\n        \"tradable\": false,\n        \"equipment_data\": {\n            \"equipment_slot\": \"hands\",\n            \"offensive_bonuses\": {\n                \"stab\": 8,\n                \"slash\": 8,\n                \"crush\": 8,\n                \"magic\": 4,\n                \"ranged\": 8\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 8,\n                \"slash\": 8,\n                \"crush\": 8,\n                \"magic\": 4,\n                \"ranged\": 8\n            },\n            \"skill_bonuses\": {\n                \"strength\": 8\n            }\n        }\n    },\n\n    \"rs:rune_defender\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 8850,\n        \"examine\": \"A defensive weapon.\",\n        \"weight\": 0.453,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 40\n                }\n            },\n            \"offensive_bonuses\": {\n                \"stab\": 20,\n                \"slash\": 19,\n                \"crush\": 18,\n                \"magic\": -3,\n                \"ranged\": -2\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 20,\n                \"slash\": 19,\n                \"crush\": 18,\n                \"magic\": -3,\n                \"ranged\": -2\n            },\n            \"skill_bonuses\": {\n                \"strength\": 5\n            }\n        }\n    },\n\n    \"rs:rune_boots\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 4131,\n        \"examine\": \"These will protect my feet.\",\n        \"weight\": 1.36,\n        \"equipment_data\": {\n            \"equipment_slot\": \"feet\",\n            \"offensive_bonuses\": {\n                \"magic\": -3,\n                \"ranged\": -1\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 12,\n                \"slash\": 13,\n                \"crush\": 14\n            },\n            \"skill_bonuses\": {\n                \"strength\": 2\n            }\n        }\n    },\n\n    \"rs:rune_med_helm\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1147,\n        \"examine\": \"A medium sized helmet.\",\n        \"weight\": 1.814,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"offensive_bonuses\": {\n                \"magic\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 22,\n                \"slash\": 23,\n                \"crush\": 21,\n                \"magic\": -1,\n                \"ranged\": 22\n            }\n        }\n    },\n\n    \"rs:rune_full_helm\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1163,\n        \"examine\": \"A full face helmet.\",\n        \"weight\": 2.721,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"offensive_bonuses\": {\n                \"magic\": -6,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 30,\n                \"slash\": 32,\n                \"crush\": 27,\n                \"magic\": -1,\n                \"ranged\": 30\n            }\n        }\n    },\n\n    \"rs:rune_sq_shield\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1185,\n        \"examine\": \"A medium square shield.\",\n        \"weight\": 3.628,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"offensive_bonuses\": {\n                \"magic\": -6\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 38,\n                \"slash\": 40,\n                \"crush\": 36,\n                \"ranged\": 38\n            }\n        }\n    },\n\n    \"rs:rune_kiteshield\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1201,\n        \"examine\": \"A large metal shield.\",\n        \"weight\": 5.443,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"offensive_bonuses\": {\n                \"magic\": -8,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 44,\n                \"slash\": 48,\n                \"crush\": 46,\n                \"magic\": -1,\n                \"ranged\": 46\n            }\n        }\n    },\n\n    \"rs:rune_platelegs\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1079,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:rune_plateskirt\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1093,\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:rune_chainbody\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1113,\n        \"examine\": \"A series of connected metal rings.\",\n        \"weight\": 6.803,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"offensive_bonuses\": {\n                \"magic\": -15\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 63,\n                \"slash\": 72,\n                \"crush\": 78,\n                \"magic\": -3,\n                \"ranged\": 65\n            }\n        }\n    },\n\n    \"rs:rune_platebody\": {\n        \"extends\": \"rs:rune_armour_base\",\n        \"game_id\": 1127,\n        \"examine\": \"Provides excellent protection.\",\n        \"weight\": 9.979,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"offensive_bonuses\": {\n                \"magic\": -30,\n                \"ranged\": -15\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 82,\n                \"slash\": 80,\n                \"crush\": 72,\n                \"magic\": -6,\n                \"ranged\": 80\n            },\n            \"requirements\": {\n                \"quests\": {\n                    \"rs:dragon_slayer\": \"complete\"\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/rune-god-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:rune_god_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 40\n                    }\n                },\n                \"skill_bonuses\": {\n                    \"prayer\": 1\n                }\n            }\n        }\n    },\n\n    \"rs:guthix_full_helm\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2673,\n        \"examine\": \"A rune full face helmet in the colours of Guthix.\",\n        \"weight\": 2.721,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"offensive_bonuses\": {\n                \"magic\": -6,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 30,\n                \"slash\": 32,\n                \"crush\": 27,\n                \"magic\": -1,\n                \"ranged\": 30\n            }\n        }\n    },\n\n    \"rs:guthix_kiteshield\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2675,\n        \"examine\": \"Rune kiteshield in the colours of Guthix.\",\n        \"weight\": 5.443,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"offensive_bonuses\": {\n                \"magic\": -8,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 44,\n                \"slash\": 48,\n                \"crush\": 46,\n                \"magic\": -1,\n                \"ranged\": 46\n            }\n        }\n    },\n\n    \"rs:guthix_platelegs\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2671,\n        \"examine\": \"Rune platelegs in the colours of Guthix.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:guthix_plateskirt\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 3480,\n        \"examine\": \"Rune plateskirt in the colours of Guthix.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:guthix_platebody\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2669,\n        \"examine\": \"Rune platebody in the colours of Guthix.\",\n        \"weight\": 9.979,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"offensive_bonuses\": {\n                \"magic\": -30,\n                \"ranged\": -15\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 82,\n                \"slash\": 80,\n                \"crush\": 72,\n                \"magic\": -6,\n                \"ranged\": 80\n            },\n            \"requirements\": {\n                \"quests\": {\n                    \"rs:dragon_slayer\": \"complete\"\n                }\n            }\n        }\n    },\n\n    \"rs:zamorak_full_helm\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2657,\n        \"examine\": \"A rune full face helmet in the colours of Zamorak.\",\n        \"weight\": 2.721,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"offensive_bonuses\": {\n                \"magic\": -6,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 30,\n                \"slash\": 32,\n                \"crush\": 27,\n                \"magic\": -1,\n                \"ranged\": 30\n            }\n        }\n    },\n\n    \"rs:zamorak_kiteshield\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2659,\n        \"examine\": \"Rune kiteshield in the colours of Zamorak.\",\n        \"weight\": 5.443,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"offensive_bonuses\": {\n                \"magic\": -8,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 44,\n                \"slash\": 48,\n                \"crush\": 46,\n                \"magic\": -1,\n                \"ranged\": 46\n            }\n        }\n    },\n\n    \"rs:zamorak_platelegs\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2655,\n        \"examine\": \"Rune platelegs in the colours of Zamorak.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:zamorak_plateskirt\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 3478,\n        \"examine\": \"Rune plateskirt in the colours of Zamorak.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:zamorak_platebody\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2653,\n        \"examine\": \"Rune platebody in the colours of Zamorak.\",\n        \"weight\": 9.979,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"offensive_bonuses\": {\n                \"magic\": -30,\n                \"ranged\": -15\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 82,\n                \"slash\": 80,\n                \"crush\": 72,\n                \"magic\": -6,\n                \"ranged\": 80\n            },\n            \"requirements\": {\n                \"quests\": {\n                    \"rs:dragon_slayer\": \"complete\"\n                }\n            }\n        }\n    },\n\n    \"rs:saradomin_full_helm\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2665,\n        \"examine\": \"A rune full face helmet in the colours of Saradomin.\",\n        \"weight\": 2.721,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"offensive_bonuses\": {\n                \"magic\": -6,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 30,\n                \"slash\": 32,\n                \"crush\": 27,\n                \"magic\": -1,\n                \"ranged\": 30\n            }\n        }\n    },\n\n    \"rs:saradomin_kiteshield\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2667,\n        \"examine\": \"Rune kiteshield in the colours of Saradomin.\",\n        \"weight\": 5.443,\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"offensive_bonuses\": {\n                \"magic\": -8,\n                \"ranged\": -3\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 44,\n                \"slash\": 48,\n                \"crush\": 46,\n                \"magic\": -1,\n                \"ranged\": 46\n            }\n        }\n    },\n\n    \"rs:saradomin_platelegs\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2663,\n        \"examine\": \"Rune platelegs in the colours of Saradomin.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:saradomin_plateskirt\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 3479,\n        \"examine\": \"Rune plateskirt in the colours of Saradomin.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -11\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 51,\n                \"slash\": 49,\n                \"crush\": 47,\n                \"magic\": -4,\n                \"ranged\": 49\n            }\n        }\n    },\n\n    \"rs:saradomin_platebody\": {\n        \"extends\": \"rs:rune_god_armour_base\",\n        \"game_id\": 2661,\n        \"examine\": \"Rune platebody in the colours of Saradomin.\",\n        \"weight\": 9.979,\n        \"equipment_data\": {\n            \"equipment_slot\": \"torso\",\n            \"offensive_bonuses\": {\n                \"magic\": -30,\n                \"ranged\": -15\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 82,\n                \"slash\": 80,\n                \"crush\": 72,\n                \"magic\": -6,\n                \"ranged\": 80\n            },\n            \"requirements\": {\n                \"quests\": {\n                    \"rs:dragon_slayer\": \"complete\"\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/steel-armour.json",
    "content": "{\n    \"presets\": {\n        \"rs:steel_armour_base\": {\n            \"tradable\": true,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"requirements\": {\n                    \"skills\": {\n                        \"defence\": 5\n                    }\n                }\n            }\n        }\n    },\n\n    \"rs:steel_platelegs\": {\n        \"extends\": \"rs:steel_armour_base\",\n        \"game_id\": 1069,\n        \"examine\": \"These look pretty heavy.\",\n        \"weight\": 9.071,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 17,\n                \"slash\": 16,\n                \"crush\": 15,\n                \"magic\": -4,\n                \"ranged\": 16\n            }\n        }\n    },\n\n    \"rs:steel_plateskirt\": {\n        \"extends\": \"rs:steel_armour_base\",\n        \"game_id\": 1083,\n        \"examine\": \"Designer leg protection.\",\n        \"weight\": 8.164,\n        \"equipment_data\": {\n            \"equipment_slot\": \"legs\",\n            \"offensive_bonuses\": {\n                \"magic\": -21,\n                \"ranged\": -7\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 17,\n                \"slash\": 16,\n                \"crush\": 15,\n                \"magic\": -4,\n                \"ranged\": 16\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/steel-weapons.json",
    "content": "{\n    \"rs:steel_battleaxe\": {\n        \"game_id\": 1365,\n        \"examine\": \"A vicious looking axe.\",\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 5\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 6,\n                \"stab\": -2,\n                \"slash\": 16,\n                \"crush\": 11\n            },\n            \"defensive_bonuses\": {\n                \"ranged\": -1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 20\n            },\n            \"weapon_info\": {\n                \"style\": \"axe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/standard-metals/white-armour.json",
    "content": "{\n    \"rs:white_full_helm\": {\n        \"game_id\": 6623,\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"helmet\",\n            \"requirements\": {\n                \"skills\": {\n                    \"defense\": 10\n                }\n            },\n            \"offensive_bonuses\": {\n                \"magic\": -6,\n                \"ranged\": -2\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 12,\n                \"slash\": 13,\n                \"crush\": 10,\n                \"magic\": -1,\n                \"ranged\": 12\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 1\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/tiaras.json",
    "content": "{\n    \"rs:air_tiara\": {\n        \"game_id\": 5527,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:mind_tiara\": {\n        \"game_id\": 5529,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:water_tiara\": {\n        \"game_id\": 5531,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:body_tiara\": {\n        \"game_id\": 5533,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:earth_tiara\": {\n        \"game_id\": 5535,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:fire_tiara\": {\n        \"game_id\": 5537,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:cosmic_tiara\": {\n        \"game_id\": 5539,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:nature_tiara\": {\n        \"game_id\": 5541,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:chaos_tiara\": {\n        \"game_id\": 5543,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:law_tiara\": {\n        \"game_id\": 5545,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:death_tiara\": {\n        \"game_id\": 5547,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:blood_tiara\": {\n        \"game_id\": 5549,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    },\n    \"rs:soul_tiara\": {\n        \"game_id\": 5551,\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\",\n            \"equipment_type\": \"hat\"\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/training.json",
    "content": "{\n    \"rs:training_sword\": {\n        \"game_id\": 9703,\n        \"examine\": \"Basic training sword.\",\n        \"tradable\": false,\n        \"weight\": 1.814,\n        \"equippable\": true,\n        \"destroy\": \"You can get another Training sword by talking to the Melee Combat skill tutor north of Lumbridge Castle.\",\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"equipment_type\": \"one_handed\",\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 4,\n                \"slash\": 3,\n                \"crush\": -2\n            },\n            \"defensive_bonuses\": {\n                \"slash\": 2,\n                \"crush\": 1\n            },\n            \"skill_bonuses\": {\n                \"strength\": 5\n            },\n            \"weapon_info\": {\n                \"style\": \"dagger\"\n            }\n        }\n    },\n    \"rs:training_shield\": {\n        \"game_id\": 9704,\n        \"examine\": \"Made of flimsy painted wood.\",\n        \"tradable\": false,\n        \"weight\": 5.443,\n        \"equippable\": true,\n        \"destroy\": \"You can get another Training shield by talking to the Melee Combat skill tutor north of Lumbridge Castle.\",\n        \"equipment_data\": {\n            \"equipment_slot\": \"off_hand\",\n            \"defensive_bonuses\": {\n                \"stab\": 4,\n                \"slash\": 5,\n                \"crush\": 3,\n                \"magic\": 1,\n                \"ranged\": 4\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/equipment/whips.json",
    "content": "{\n    \"rs:abyssal_whip\": {\n        \"game_id\": 4151,\n        \"examine\": \"A weapon from the Abyss.\",\n        \"tradable\": true,\n        \"weight\": 0.453,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"main_hand\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 70\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 4,\n                \"stab\": 0,\n                \"slash\": 82,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 0,\n                \"strength\": 82,\n                \"ranged\": 0,\n                \"magic\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"whip\"\n            }\n        }\n    },\n    \"rs:veracs_flail\": {\n        \"game_id\": 4755,\n        \"examine\": \"Verac the Defiled's flail.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"2h\",\n            \"requirements\": {\n                \"skills\": {\n                    \"attack\": 70\n                }\n            },\n            \"offensive_bonuses\": {\n                \"speed\": 5,\n                \"stab\": 68,\n                \"slash\": -2,\n                \"crush\": 82,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"defensive_bonuses\": {\n                \"stab\": 0,\n                \"slash\": 0,\n                \"crush\": 0,\n                \"magic\": 0,\n                \"ranged\": 0\n            },\n            \"skill_bonuses\": {\n                \"prayer\": 6,\n                \"strength\": 72,\n                \"ranged\": 0,\n                \"magic\": 0\n            },\n            \"weapon_info\": {\n                \"style\": \"mace\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/items/food.json",
    "content": "{\n    \"presets\": {\n        \"rs:food\": {\n            \"tradable\": true,\n            \"consumable\": true,\n            \"metadata\": {\n                \"consume_effects\": {\n                    \"clock\": \"food\"\n                },\n                \"special\": false\n            }\n        },\n        \"rs:beverage\": {\n            \"tradable\": true,\n            \"consumable\": true,\n            \"metadata\": {\n                \"consume_effects\": {\n                    \"clock\": \"food\"\n                },\n                \"special\": false\n            }\n        }\n    },\n    \"rs:meat\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2142,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 3\n                }\n            }\n        }\n    },\n    \"rs:shrimps\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 315,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 3\n                }\n            }\n        }\n    },\n    \"rs:chicken\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2140,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 3\n                }\n            }\n        }\n    },\n    \"rs:anchovies\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 319,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 1\n                }\n            }\n        }\n    },\n    \"rs:sardine\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 325,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 4\n                }\n            }\n        }\n    },\n    \"rs:karambwan\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3142,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"clock\": \"special_food\",\n                \"skills\": {\n                    \"hitpoints\": 18\n                }\n            }\n        }\n    },\n    \"rs:ugthanki_kebab\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 1883,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 19\n                }\n            }\n        }\n    },\n    \"rs:herring\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 345,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        }\n    },\n    \"rs:mackerel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 355,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 6\n                }\n            }\n        }\n    },\n    \"rs:thin_snail_meat\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3369,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": [5, 7]\n                }\n            }\n        }\n    },\n    \"rs:trout\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 333,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 7\n                }\n            }\n        }\n    },\n    \"rs:lean_snail_meat\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3371,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": [5, 8]\n                }\n            }\n        }\n    },\n    \"rs:cod\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 339,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 7\n                }\n            }\n        }\n    },\n    \"rs:pike\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 351,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 8\n                }\n            }\n        }\n    },\n    \"rs:salmon\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 329,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 9\n                }\n            }\n        }\n    },\n    \"rs:tuna\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 361,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 10\n                }\n            }\n        }\n    },\n    \"rs:cooked_chompy\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2878,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 10\n                }\n            }\n        }\n    },\n    \"rs:fish_cake\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7530,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        }\n    },\n    \"rs:cave_eel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 5003,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": [8, 12]\n                }\n            }\n        }\n    },\n    \"rs:lobster\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 379,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 12\n                }\n            }\n        }\n    },\n    \"rs:jubbly\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7568,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 15\n                }\n            }\n        }\n    },\n    \"rs:bass\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 365,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 13\n                }\n            }\n        }\n    },\n    \"rs:swordfish\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 373,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 14\n                }\n            }\n        }\n    },\n    \"rs:lava_eel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2149,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        }\n    },\n    \"rs:shark\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 385,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 20\n                }\n            }\n        }\n    },\n    \"rs:sea_turtle\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 397,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 21\n                }\n            }\n        }\n    },\n    \"rs:manta_ray\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 391,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 22\n                }\n            }\n        }\n    },\n    \"rs:bread\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2309,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        }\n    },\n    \"rs:uncooked_meat_pie\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2320\n    },\n    \"rs:redberry_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:redberry_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2325\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2333\n            }\n        ]\n    },\n    \"rs:meat_pie\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2327,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 6\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:meat_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2327\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2331\n            }\n        ]\n    },\n    \"rs:apple_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 7\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:apple_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2323\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2335\n            }\n        ]\n    },\n    \"rs:garden_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 6,\n                    \"farming\": 3\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:garden_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 7178\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 7180\n            }\n        ]\n    },\n    \"rs:fish_pie\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7188,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 6,\n                    \"fishing\": 3\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:fish_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 7188\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 7190\n            }\n        ]\n    },\n    \"rs:admiral_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 8,\n                    \"fishing\": 5\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:admiral_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 7198\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 7200\n            }\n        ]\n    },\n    \"rs:wild_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11,\n                    \"slayer\": 5,\n                    \"ranged\": 4\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:wild_pie:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 7208\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 7210\n            }\n        ]\n    },\n    \"rs:summer_pie\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11,\n                    \"agility\": 5\n                },\n                \"energy\": 10\n            }\n        },\n        \"variations\": [\n            {\n                \"game_id\": 7218,\n                \"suffix\": \"0\",\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:summer_pie:1\"\n                    }\n                }\n            },\n            {\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 7220,\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pie_dish\"\n                    }\n                }\n            }\n        ]\n    },\n    \"rs:stew\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2003,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"replaced_by\": \"rs:bowl\",\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        }\n    },\n    \"rs:banana_stew\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 4016,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"replaced_by\": \"rs:bowl\",\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        }\n    },\n    \"rs:spicy_stew\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7479,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"special\": true\n            }\n        }\n    },\n    \"rs:plain_pizza\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 7\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:plain_pizza:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2289\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2291\n            }\n        ]\n    },\n    \"rs:meat_pizza\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 8\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:meat_pizza:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2293\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2295\n            }\n        ]\n    },\n    \"rs:anchovy_pizza\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 9\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:anchovy_pizza:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2297\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2299\n            }\n        ]\n    },\n    \"rs:pineapple_pizza\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:pineapple_pizza:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 2301\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 2303\n            }\n        ]\n    },\n    \"rs:cake\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 4\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:cake:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 1891\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:cake:2\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 1893\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"2\",\n                \"tradable\": false,\n                \"game_id\": 1895\n            }\n        ]\n    },\n    \"rs:chocolate_cake\": {\n        \"extends\": \"rs:food\",\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        },\n        \"variations\": [\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:chocolate_cake:1\"\n                    }\n                },\n                \"suffix\": \"0\",\n                \"game_id\": 1897\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": \"rs:chocolate_cake:2\"\n                    }\n                },\n                \"suffix\": \"1\",\n                \"tradable\": false,\n                \"game_id\": 1899\n            },\n            {\n                \"metadata\": {\n                    \"consume_effects\": {\n                        \"replaced_by\": null\n                    }\n                },\n                \"suffix\": \"2\",\n                \"tradable\": false,\n                \"game_id\": 1901\n            }\n        ]\n    },\n    \"rs:wine\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 1993,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11,\n                    \"attack\": -2\n                }\n            }\n        }\n    },\n\n    \"rs:nettle_tea\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 4239,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 3\n                },\n                \"energy\": 5\n            }\n        }\n    },\n    \"rs:cider\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 5763,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 2,\n                    \"farming\": 1\n                }\n            }\n        }\n    },\n    \"rs:dwarven_stout\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 1913,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"special\": true\n            }\n        }\n    },\n    \"rs:asgarnian_ale\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 1905,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 2,\n                    \"strength\": 2,\n                    \"attack\": -4\n                }\n            }\n        }\n    },\n    \"rs:greenmans_ale\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 7746,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 1,\n                    \"herblore\": 1,\n                    \"attack\": -3,\n                    \"strength\": -3,\n                    \"defence\": -3\n                }\n            }\n        }\n    },\n    \"rs:wizards_mind_bomb\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 1907,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"special\": true\n            }\n        }\n    },\n    \"rs:dragon_bitter\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 1911,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 1,\n                    \"attack\": -3,\n                    \"strength\": 2\n                }\n            }\n        }\n    },\n    \"rs:moonlight_mead\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 2955,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 4\n                }\n            }\n        }\n    },\n    \"rs:axemans_folly\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 5751,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 1,\n                    \"woodcutting\": 1,\n                    \"strength\": -3,\n                    \"attack\": -3\n                }\n            }\n        }\n    },\n    \"rs:chefs_delight\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 5755,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"special\": true\n            }\n        }\n    },\n    \"rs:slayers_respite\": {\n        \"extends\": \"rs:beverage\",\n        \"game_id\": 5759,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 1,\n                    \"slayer\": 2,\n                    \"strength\": -2,\n                    \"attack\": -2\n                }\n            }\n        }\n    },\n\n    \"rs:spicy_sauce\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7072,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 2\n                }\n            }\n        }\n    },\n    \"rs:scrambled_egg\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7078,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        }\n    },\n    \"rs:scrambled_egg_and_tomato\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7064,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 8\n                }\n            }\n        }\n    },\n    \"rs:sweetcorn\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7088,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"special\": true\n            }\n        }\n    },\n    \"rs:baked_potato_with_butter\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 6703,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 14\n                }\n            }\n        }\n    },\n    \"rs:fried_onion\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7084,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        }\n    },\n    \"rs:fried_mushroom\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7082,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 5\n                }\n            }\n        }\n    },\n    \"rs:baked_potato_with_butter_and_cheese\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 6705,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 16\n                }\n            }\n        }\n    },\n    \"rs:baked_potato_with_egg_and_tomato\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7056,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 16\n                }\n            }\n        }\n    },\n    \"rs:fried_mushroom_and_onion\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7066,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 11\n                }\n            }\n        }\n    },\n    \"rs:baked_potato_with_mushroom_and_onion\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7058,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 20\n                }\n            }\n        }\n    },\n    \"rs:tuna_and_sweetcorn\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7068,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 13\n                }\n            }\n        }\n    },\n    \"rs:baked_potato_with_tuna_and_sweetcorn\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7060,\n        \"metadata\": {\n            \"consume_effects\": {\n                \"skills\": {\n                    \"hitpoints\": 22\n                }\n            }\n        }\n    },\n    \"rs:butter\": {\n        \"tradable\": true,\n        \"game_id\": 6697\n    },\n    \"rs:raw_shrimp\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 317\n    },\n    \"rs:raw_sardine\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 327\n    },\n    \"rs:raw_karambwanji\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3150\n    },\n    \"rs:raw_herring\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 345\n    },\n    \"rs:raw_anchovies\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 321\n    },\n    \"rs:raw_mackerel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 353\n    },\n    \"rs:raw_trout\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 335\n    },\n    \"rs:raw_cod\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 341\n    },\n    \"rs:raw_pike\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 349\n    },\n    \"rs:raw_slimy_eel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3379\n    },\n    \"rs:raw_salmon\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 331\n    },\n    \"rs:raw_tuna\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 359\n    },\n    \"rs:raw_cave_eel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 5001\n    },\n    \"rs:raw_bass\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 363\n    },\n    \"rs:raw_lobster\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 377\n    },\n    \"rs:raw_swordfish\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 371\n    },\n    \"rs:raw_lava_eel\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 2148\n    },\n    \"rs:raw_monkfish\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 7944\n    },\n    \"rs:raw_karambwan\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 3142\n    },\n    \"rs:raw_shark\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 383\n    },\n    \"rs:raw_sea_turtle\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 395\n    },\n    \"rs:raw_manta_ray\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 389\n    },\n    \"rs:raw_manta_ray\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 389\n    },\n    \"rs:raw_leaping_trout\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 389\n    },\n    \"rs:raw_leaping_salmon\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 11330\n    },\n    \"rs:raw_leaping_sturgeon\": {\n        \"extends\": \"rs:food\",\n        \"game_id\": 11332\n    },\n    \"rs:cheese\": {\n        \"tradable\": true,\n        \"game_id\": 1985\n    },\n    \"rs:pitta_bread\": {\n        \"tradable\": true,\n        \"game_id\": 1865\n    },\n    \"rs:wine_of_zamorak\": {\n        \"game_id\": 245,\n        \"tradable\": true\n    },\n    \"rs:baked_potato\": {\n        \"tradable\": \"true\",\n        \"game_id\": 6701\n    }\n}\n"
  },
  {
    "path": "data/config/items/holiday/partyhats.json",
    "content": "{\n    \"presets\": {\n        \"rs:partyhat\": {\n            \"tradable\": true,\n            \"weight\": 0.056,\n            \"equippable\": true,\n            \"equipment_data\": {\n                \"equipment_slot\": \"head\",\n                \"equipment_type\": \"hat\"\n            }\n        }\n    },\n\n    \"rs:red_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1038\n    },\n    \"rs:yellow_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1040\n    },\n    \"rs:blue_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1042\n    },\n    \"rs:green_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1034\n    },\n    \"rs:purple_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1046\n    },\n    \"rs:white_partyhat\": {\n        \"extends\": \"rs:partyhat\",\n        \"game_id\": 1048\n    }\n}\n"
  },
  {
    "path": "data/config/items/icons.json",
    "content": "{\n    \"rs:gnome_stronghold_agility_course\": {\n        \"game_id\": 2150\n    },\n    \"rs:gnomeball_game\": {\n        \"game_id\": 751\n    },\n    \"rs:werewolf_skullball_game\": {\n        \"game_id\": 1061\n    },\n    \"rs:agility_pyramid\": {\n        \"game_id\": 6970\n    },\n    \"rs:barbarian_outpost_agility_course\": {\n        \"game_id\": 1365\n    },\n    \"rs:ape_atoll_agility_course\": {\n        \"game_id\": 4024\n    },\n    \"rs:wilderness_course\": {\n        \"game_id\": 553\n    },\n    \"rs:agility_jump_green\": {\n        \"game_id\": 6518\n    },\n    \"rs:agility_contortion_green\": {\n        \"game_id\": 6520\n    },\n    \"rs:agility_balance_green\": {\n        \"game_id\": 6519\n    },\n    \"rs:agility_climb\": {\n        \"game_id\": 6517\n    },\n    \"rs:agility_jump_yellow\": {\n        \"game_id\": 6514\n    },\n    \"rs:agility_contortion_yellow\": {\n        \"game_id\": 6516\n    },\n    \"rs:agility_balance_yellow\": {\n        \"game_id\": 6515\n    }\n}\n"
  },
  {
    "path": "data/config/items/logs.json",
    "content": "{\n    \"presets\": {\n        \"rs:log\": {\n            \"tradable\": true,\n            \"weight\": 2\n        }\n    },\n\n    \"rs:logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1511\n    },\n    \"rs:achey_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 2862\n    },\n    \"rs:pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 3438\n    },\n    \"rs:oak_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1521\n    },\n    \"rs:oak_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 3440\n    },\n    \"rs:willow_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1519\n    },\n    \"rs:teak_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 6333\n    },\n    \"rs:willow_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 3442\n    },\n    \"rs:teak_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 6211\n    },\n    \"rs:maple_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1517\n    },\n    \"rs:mahogany_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 6332\n    },\n    \"rs:maple_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 3444\n    },\n    \"rs:mahogany_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 6213\n    },\n    \"rs:yew_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1515\n    },\n    \"rs:yew_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 3446\n    },\n    \"rs:magic_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1513\n    },\n    \"rs:magic_pyre_logs\": {\n        \"extends\": \"rs:log\",\n        \"game_id\": 1513\n    },\n    \"rs:bark\": {\n        \"game_id\": 3239,\n        \"examine\": \"Bark from a hollow tree.\",\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": false\n    }\n}\n"
  },
  {
    "path": "data/config/items/other.json",
    "content": "{\n    \"rs:dwarf_remains\": {\n        \"game_id\": 0,\n        \"tradable\": false\n    },\n    \"rs:rotten_potato\": {\n        \"game_id\": 5733,\n        \"tradable\": false,\n        \"examine\": \"Yuk!\"\n    }\n}\n"
  },
  {
    "path": "data/config/items/quests/lost-city.json",
    "content": "{\n    \"rs:dramen_branch\": {\n        \"game_id\": 771,\n        \"examine\": \"A limb of the fabled Dramen tree.\",\n        \"tradable\": false,\n        \"weight\": 2.267,\n        \"equippable\": false\n    }\n}\n"
  },
  {
    "path": "data/config/items/quests/witchs-potion.json",
    "content": "{\n    \"rs:rats_tail\": {\n        \"game_id\": 300,\n        \"tradable\": false,\n        \"weight\": 0.003\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/artisan-tools.json",
    "content": "{\n    \"rs:shears\": {\n        \"game_id\": 5603,\n        \"examine\": \"For shearing sheep.\",\n        \"tradable\": true,\n        \"weight\": 0.113\n    },\n    \"rs:chisel\": {\n        \"game_id\": 1755,\n        \"examine\": \"Good for detailed Crafting.\",\n        \"tradable\": true,\n        \"weight\": 0.453\n    },\n    \"rs:spade\": {\n        \"game_id\": 952,\n        \"examine\": \"A slightly muddy spade.\",\n        \"tradable\": true,\n        \"weight\": 1.814\n    },\n    \"rs:hammer\": {\n        \"game_id\": 2347,\n        \"examine\": \"Good for hitting things!\",\n        \"tradable\": true,\n        \"weight\": 0.907\n    },\n    \"rs:needle\": {\n        \"game_id\": 1733,\n        \"examine\": \"Used with a thread to make clothes.\",\n        \"tradable\": true,\n        \"weight\": 0\n    },\n    \"rs:thread\": {\n        \"game_id\": 1734,\n        \"examine\": \"Used with a needle to make clothes.\",\n        \"tradable\": true,\n        \"weight\": 0\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/baking.json",
    "content": "{\n    \"rs:cake_tin\": {\n        \"game_id\": 1887,\n        \"examine\": \"Useful for baking cakes.\",\n        \"tradable\": true,\n        \"weight\": 0.1\n    },\n    \"rs:egg\": {\n        \"game_id\": 1944,\n        \"examine\": \"A nice fresh egg.\",\n        \"tradable\": true,\n        \"weight\": 0.02\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/cooking.json",
    "content": "{\n    \"rs:raw_rat_meat\": {\n        \"game_id\": 2134\n    },\n    \"rs:cooked_meat\": {\n        \"game_id\": 2142\n    },\n    \"rs:burnt_meat\": {\n        \"game_id\": 2146\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/crafting/gems.json",
    "content": "{\n    \"presets\": {\n        \"rs:uncut_gem_base\": {\n            \"examine\": \"This would be worth more cut.\",\n            \"weight\": 0.003,\n            \"tradable\": true,\n            \"equippable\": false\n        },\n        \"rs:cut_gem_base\": {\n            \"examine\": \"A precious gem.\",\n            \"weight\": 0.002,\n            \"tradable\": true,\n            \"equippable\": false\n        }\n    },\n    \"rs:uncut_opal\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1625\n    },\n    \"rs:uncut_jade\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1627\n    },\n    \"rs:uncut_red_topaz\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1629\n    },\n    \"rs:uncut_sapphire\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1623\n    },\n    \"rs:uncut_emerald\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1621\n    },\n    \"rs:uncut_ruby\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1619\n    },\n    \"rs:uncut_diamond\": {\n        \"extends\": \"rs:uncut_gem_base\",\n        \"game_id\": 1617\n    },\n    \"rs:opal\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1609\n    },\n    \"rs:jade\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1611\n    },\n    \"rs:red_topaz\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1613\n    },\n    \"rs:sapphire\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1607\n    },\n    \"rs:emerald\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1605\n    },\n    \"rs:ruby\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1603\n    },\n    \"rs:diamond\": {\n        \"extends\": \"rs:cut_gem_base\",\n        \"game_id\": 1601\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/crafting/jewelery-moulds.json",
    "content": "{\n    \"presets\": {\n        \"rs:mould_base\": {\n            \"weight\": 0.453,\n            \"tradable\": true\n        }\n    },\n\n    \"rs:ring_mould\": {\n        \"extends\": \"rs:mould_base\",\n        \"game_id\": 1592,\n        \"examine\": \"Used to make gold rings.\"\n    },\n    \"rs:necklace_mould\": {\n        \"extends\": \"rs:mould_base\",\n        \"game_id\": 1597,\n        \"examine\": \"Used to make gold necklaces.\"\n    },\n    \"rs:amulet_mould\": {\n        \"extends\": \"rs:mould_base\",\n        \"game_id\": 1595,\n        \"examine\": \"Used to make gold amulets.\"\n    },\n    \"rs:holy_mould\": {\n        \"extends\": \"rs:mould_base\",\n        \"game_id\": 1599,\n        \"examine\": \"Used to make holy symbols of Saradomin.\"\n    },\n    \"rs:sickle_mould\": {\n        \"extends\": \"rs:mould_base\",\n        \"game_id\": 2976,\n        \"examine\": \"Used to make sickles.\"\n    },\n    \"rs:tiara_mould\": {\n        \"game_id\": 5523,\n        \"examine\": \"A mould for tiaras.\",\n        \"weight\": 1,\n        \"tradable\": true\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/firemaking.json",
    "content": "{\n    \"rs:tinderbox\": {\n        \"game_id\": 590,\n        \"tradable\": true,\n        \"weight\": 0.035\n    },\n    \"rs:normal_pyre_ships\": {\n        \"game_id\": 3438\n    },\n    \"rs:oak_pyre_ships\": {\n        \"game_id\": 3440\n    },\n    \"rs:willow_pyre_ships\": {\n        \"game_id\": 3442\n    },\n    \"rs:teak_pyre_ships\": {\n        \"game_id\": 6211\n    },\n    \"rs:maple_pyre_ships\": {\n        \"game_id\": 3444\n    },\n    \"rs:mahogany_pyre_ships\": {\n        \"game_id\": 6213\n    },\n    \"rs:yew_pyre_ships\": {\n        \"game_id\": 3446\n    },\n    \"rs:magic_pyre_ships\": {\n        \"game_id\": 3446\n    },\n    \"rs:candle\": {\n        \"game_id\": 36\n    },\n    \"rs:candle_lanterns\": {\n        \"game_id\": 4527\n    },\n    \"rs:oil_lamps\": {\n        \"game_id\": 4522\n    },\n    \"rs:iron_spits\": {\n        \"game_id\": 7225\n    },\n    \"rs:empty_oil_lantern\": {\n        \"game_id\": 4535\n    },\n    \"rs:harpie_bug_lanterns\": {\n        \"game_id\": 7051\n    },\n    \"rs:bullseye_lanterns\": {\n        \"game_id\": 4544\n    },\n    \"rs:sapphire_lanterns\": {\n        \"game_id\": 4700\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/fishing.json",
    "content": "{\n    \"rs:small_fishing_net\": {\n        \"game_id\": 303\n    },\n    \"rs:big_fishing_net\": {\n        \"game_id\": 305\n    },\n    \"rs:fishing_rod\": {\n        \"game_id\": 307\n    },\n    \"rs:fly_fishing_rod\": {\n        \"game_id\": 309\n    },\n    \"rs:harpoon\": {\n        \"game_id\": 311\n    },\n    \"rs:lobster_pot\": {\n        \"game_id\": 301\n    },\n    \"rs:oily_fishing_rod\": {\n        \"game_id\": 1585\n    },\n    \"rs:karambwan_vessel\": {\n        \"game_id\": 3157\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/fletching/arrows.json",
    "content": "{\n    \"rs:arrow_shaft\": {\n        \"game_id\": 52\n    },\n    \"rs:bronze_arrow\": {\n        \"game_id\": 882\n    },\n    \"rs:iron_arrow\": {\n        \"game_id\": 884\n    },\n    \"rs:steel_arrow\": {\n        \"game_id\": 886\n    },\n    \"rs:mithril_arrow\": {\n        \"game_id\": 888\n    },\n    \"rs:broad_arrow\": {\n        \"game_id\": 4150\n    },\n    \"rs:adamant_arrow\": {\n        \"game_id\": 890\n    },\n    \"rs:rune_arrow\": {\n        \"game_id\": 892\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/fletching/bolts.json",
    "content": "{\n    \"rs:bronze_bolt\": {\n        \"game_id\": 877\n    },\n    \"rs:opal_bolt\": {\n        \"game_id\": 879\n    },\n    \"rs:blurite_bolt\": {\n        \"game_id\": 9139\n    },\n    \"rs:jade_bolt\": {\n        \"game_id\": 9237\n    },\n    \"rs:iron_bolt\": {\n        \"game_id\": 9140\n    },\n    \"rs:silver_bolt\": {\n        \"game_id\": 9145\n    },\n    \"rs:steel_bolt\": {\n        \"game_id\": 9141\n    },\n    \"rs:mithril_bolt\": {\n        \"game_id\": 9142\n    },\n    \"rs:sapphire_bolt\": {\n        \"game_id\": 9337\n    },\n    \"rs:emerald_bolt\": {\n        \"game_id\": 9338\n    },\n    \"rs:adamant_bolt\": {\n        \"game_id\": 9143\n    },\n    \"rs:runite_bolt\": {\n        \"game_id\": 9144\n    },\n    \"rs:onyx_bolt\": {\n        \"game_id\": 9342\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/herblore/herbs.json",
    "content": "{\n    \"presets\": {\n        \"rs:grimy_herb_base\": {\n            \"examine\": \"It needs cleaning.\",\n            \"weight\": 0.007,\n            \"tradable\": true\n        },\n        \"rs:clean_herb_base\": {\n            \"weight\": 0.007,\n            \"tradable\": true\n        }\n    },\n    \"rs:grimy_guam\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 199\n    },\n    \"rs:grimy_marrentill\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 201\n    },\n    \"rs:grimy_tarromin\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 203\n    },\n    \"rs:grimy_harralander\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 205\n    },\n    \"rs:grimy_ranarr\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 207\n    },\n    \"rs:grimy_irit\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 209\n    },\n    \"rs:grimy_avantoe\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 211\n    },\n    \"rs:grimy_kwuarm\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 213\n    },\n    \"rs:grimy_cadantine\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 215\n    },\n    \"rs:grimy_dwarf_weed\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 217\n    },\n    \"rs:grimy_torstol\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 219\n    },\n    \"rs:grimy_toadflax\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 3049\n    },\n    \"rs:grimy_snapdragon\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 3051\n    },\n    \"rs:grimy_lantadyme\": {\n        \"extends\": \"rs:grimy_herb_base\",\n        \"game_id\": 2485\n    },\n    \"rs:herb_guam\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 249\n    },\n    \"rs:herb_marrentill\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 251\n    },\n    \"rs:herb_tarromin\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 253\n    },\n    \"rs:herb_harralander\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 255\n    },\n    \"rs:herb_ranarr\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 257\n    },\n    \"rs:herb_irit\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 259\n    },\n    \"rs:herb_avantoe\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 261\n    },\n    \"rs:herb_kwuarm\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 263\n    },\n    \"rs:herb_cadantine\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 265\n    },\n    \"rs:herb_dwarf_weed\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 267\n    },\n    \"rs:herb_torstol\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 269\n    },\n    \"rs:herb_toadflax\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 2998\n    },\n    \"rs:herb_snapdragon\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 3000\n    },\n    \"rs:herb_lantadyme\": {\n        \"extends\": \"rs:clean_herb_base\",\n        \"game_id\": 2481\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/herblore/ingredients.json",
    "content": "{\n    \"rs:eye_of_newt\": {\n        \"game_id\": 221\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/herblore/tools.json",
    "content": "{\n    \"rs:pestle_and_mortar\": {\n        \"game_id\": 233,\n        \"examine\": \"I can grind things for potions in this.\",\n        \"tradable\": true,\n        \"weight\": 0.056\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/herblore.json",
    "content": "{\n    \"rs:herblore_attack_potion\": {\n        \"game_id\": 221\n    },\n    \"rs:herblore_anti_poison_potion\": {\n        \"game_id\": 235\n    },\n    \"rs:herblore_relicyms_balm\": {\n        \"game_id\": 1534\n    },\n    \"rs:herblore_strength_potion\": {\n        \"game_id\": 225\n    },\n    \"rs:herblore_serum_207\": {\n        \"game_id\": 592\n    },\n    \"rs:herblore_stat_restore_potion\": {\n        \"game_id\": 223\n    },\n    \"rs:guthix_balance_potion\": {\n        \"game_id\": 592\n    },\n    \"rs:herblore_energy_potion\": {\n        \"game_id\": 1975\n    },\n    \"rs:herblore_defence_potion\": {\n        \"game_id\": 239\n    },\n    \"rs:herblore_agility_potion\": {\n        \"game_id\": 2152\n    },\n    \"rs:herblore_combat_potion\": {\n        \"game_id\": 9736\n    },\n    \"rs:herblore_prayer_restore_potion\": {\n        \"game_id\": 231\n    },\n    \"rs:herblore_super_attack_potion\": {\n        \"game_id\": 221\n    },\n    \"rs:herblore_super_anti_poison_potion\": {\n        \"game_id\": 235\n    },\n    \"rs:herblore_fishing_potion\": {\n        \"game_id\": 231\n    },\n    \"rs:herblore_super_energy_potion\": {\n        \"game_id\": 2970\n    },\n    \"rs:herblore_hunting_potion\": {\n        \"game_id\": 10109\n    },\n    \"rs:herblore_super_strength_potion\": {\n        \"game_id\": 225\n    },\n    \"rs:herblore_magic_essence_potion\": {\n        \"game_id\": 9016\n    },\n    \"rs:herblore_weapon_poison\": {\n        \"game_id\": 243\n    },\n    \"rs:herblore_super_restore_poison\": {\n        \"game_id\": 223\n    },\n    \"rs:herblore_super_defence_potion\": {\n        \"game_id\": 239\n    },\n    \"rs:herblore_antidote_plus\": {\n        \"game_id\": 6049\n    },\n    \"rs:herblore_anti_firebreath_potion\": {\n        \"game_id\": 243\n    },\n    \"rs:herblore_ranging_potion\": {\n        \"game_id\": 245\n    },\n    \"rs:herblore_weapon_poison_plus\": {\n        \"game_id\": 6016\n    },\n    \"rs:herblore_magic_potion\": {\n        \"game_id\": 3138\n    },\n    \"rs:herblore_zamorak_brew\": {\n        \"game_id\": 247\n    },\n    \"rs:herblore_antidote_plus_plus\": {\n        \"game_id\": 6051\n    },\n    \"rs:herblore_saradomin_brew\": {\n        \"game_id\": 6693\n    },\n    \"rs:herblore_weapon_poison_plus_plus\": {\n        \"game_id\": 6018\n    },\n    \"rs:guam_leaf\": {\n        \"game_id\": 199\n    },\n    \"rs:rogues_purse\": {\n        \"game_id\": 1534\n    },\n    \"rs:snake_weed\": {\n        \"game_id\": 1526\n    },\n    \"rs:marrentill\": {\n        \"game_id\": 251\n    },\n    \"rs:tarromin\": {\n        \"game_id\": 253\n    },\n    \"rs:harralander\": {\n        \"game_id\": 255\n    },\n    \"rs:ranarr_weed\": {\n        \"game_id\": 257\n    },\n    \"rs:toadflax\": {\n        \"game_id\": 2998\n    },\n    \"rs:irit_leaf\": {\n        \"game_id\": 259\n    },\n    \"rs:avantoe\": {\n        \"game_id\": 261\n    },\n    \"rs:kwuarm\": {\n        \"game_id\": 263\n    },\n    \"rs:snapdragon\": {\n        \"game_id\": 3000\n    },\n    \"rs:cadantine\": {\n        \"game_id\": 265\n    },\n    \"rs:lantadyme\": {\n        \"game_id\": 2481\n    },\n    \"rs:dwarf_weed\": {\n        \"game_id\": 267\n    },\n    \"rs:torstol\": {\n        \"game_id\": 269\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/mining.json",
    "content": "{\n    \"rs:rune_essence\": {\n        \"game_id\": 1436,\n        \"examine\": \"An unimbued rune.\",\n        \"tradable\": true,\n        \"weight\": 0.002,\n        \"equippable\": false\n    },\n    \"rs:clay\": {\n        \"game_id\": 434,\n        \"examine\": \"Some hard dry clay.\",\n        \"tradable\": true,\n        \"weight\": 1,\n        \"equippable\": false\n    },\n    \"rs:copper_ore\": {\n        \"game_id\": 436,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:tin_ore\": {\n        \"game_id\": 438,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:blurite_ore\": {\n        \"game_id\": 668,\n        \"examine\": \"Definitely blue.\",\n        \"tradable\": false,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:limestone\": {\n        \"game_id\": 3211,\n        \"examine\": \"Some limestone.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:iron_ore\": {\n        \"game_id\": 440,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:silver_ore\": {\n        \"game_id\": 442,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:coal\": {\n        \"game_id\": 453,\n        \"examine\": \"Hmm a non-renewable energy source!\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:gold_ore\": {\n        \"game_id\": 444,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:mithril_ore\": {\n        \"game_id\": 447,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 1.814,\n        \"equippable\": false\n    },\n    \"rs:adamantite_ore\": {\n        \"game_id\": 449,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.721,\n        \"equippable\": false\n    },\n    \"rs:soft_clay\": {\n        \"game_id\": 1761,\n        \"examine\": \"Clay soft enough to mould.\",\n        \"tradable\": true,\n        \"weight\": 0.907,\n        \"equippable\": false\n    },\n    \"rs:runite_ore\": {\n        \"game_id\": 451,\n        \"examine\": \"This needs refining.\",\n        \"tradable\": true,\n        \"weight\": 2.267,\n        \"equippable\": false\n    },\n    \"rs:pure_essence\": {\n        \"game_id\": 7936,\n        \"examine\": \"An unimbued rune of extra capability.\",\n        \"tradable\": true,\n        \"weight\": 0.002,\n        \"equippable\": false\n    },\n    \"rs:granite\": {\n        \"game_id\": 6979,\n        \"examine\": \"A tiny chunk of granite.\",\n        \"tradable\": true,\n        \"weight\": 0.5,\n        \"equippable\": false,\n        \"variations\": [\n            {\n                \"game_id\": 6981,\n                \"examine\": \"A small chunk of granite.\",\n                \"weight\": 2,\n                \"suffix\": \"2kg\"\n            },\n            {\n                \"game_id\": 6983,\n                \"examine\": \"A medium-sized chunk of granite.\",\n                \"weight\": 5,\n                \"suffix\": \"5kg\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/prayer.json",
    "content": "{\n    \"rs:prayer_guide\": {\n        \"game_id\": 1714\n    },\n    \"rs:herblore_anti_poison_potion\": {\n        \"game_id\": 235\n    }\n}\n"
  },
  {
    "path": "data/config/items/skills/runecrafting.json",
    "content": "{\n    \"rs:binding_necklace\": {\n        \"game_id\": 5521,\n        \"tradable\": true,\n        \"weight\": 0.01,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"neck\"\n        }\n    },\n    \"rs:pure_essence\": {\n        \"game_id\": 7936,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"An uncharged Rune Stone of extra capability.\",\n        \"weight\": 0.01\n    },\n    \"rs:rune_essence\": {\n        \"game_id\": 1436,\n        \"tradable\": true,\n        \"examine\": \"An uncharged Rune Stone.\",\n        \"weight\": 0.01\n    },\n    \"rs:air_rune\": {\n        \"game_id\": 556,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"One of the 4 basic elemental Runes.\",\n        \"weight\": 0.01\n    },\n    \"rs:mind_rune\": {\n        \"game_id\": 558,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"exmine\": \"Used for basic level missile spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:water_rune\": {\n        \"game_id\": 555,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"One of the 4 basic elemental Runes.\",\n        \"weight\": 0.01\n    },\n    \"rs:earth_rune\": {\n        \"game_id\": 557,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"One of the 4 basic elemental Runes.\",\n        \"weight\": 0.01\n    },\n    \"rs:fire_rune\": {\n        \"game_id\": 554,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"One of the 4 basic elemental Runes.\",\n        \"weight\": 0.01\n    },\n    \"rs:body_rune\": {\n        \"game_id\": 559,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for curse spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:cosmic_rune\": {\n        \"game_id\": 564,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for enchant spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:chaos_rune\": {\n        \"game_id\": 562,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for low level missile spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:nature_rune\": {\n        \"game_id\": 561,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for alchemy spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:law_rune\": {\n        \"game_id\": 563,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for teleport spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:death_rune\": {\n        \"game_id\": 560,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"Used for medium level missile spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:mist_rune\": {\n        \"game_id\": 4695,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Air and Water Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:dust_rune\": {\n        \"game_id\": 4696,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Air and Earth Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:mud_rune\": {\n        \"game_id\": 4698,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Earth and Water Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:smoke_rune\": {\n        \"game_id\": 4697,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Air and Fire Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:steam_rune\": {\n        \"game_id\": 4694,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Water and Fire Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:lava_rune\": {\n        \"game_id\": 4699,\n        \"tradable\": true,\n        \"stackable\": true,\n        \"examine\": \"A combined Earth and Fire Rune.\",\n        \"weight\": 0.01\n    },\n    \"rs:astral_rune\": {\n        \"game_id\": 9075,\n        \"examine\": \"Used for Lunar spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:blood_rune\": {\n        \"game_id\": 565,\n        \"examine\": \"Used for high level missile spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:soul_rune\": {\n        \"game_id\": 566,\n        \"examine\": \"Used for high level curse spells.\",\n        \"weight\": 0.01\n    },\n    \"rs:air_talisman\": {\n        \"game_id\": 1438,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:earth_talisman\": {\n        \"game_id\": 1440,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:fire_talisman\": {\n        \"game_id\": 1442,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:water_talisman\": {\n        \"game_id\": 1444,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:body_talisman\": {\n        \"game_id\": 1446,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:mind_talisman\": {\n        \"game_id\": 1448,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:blood_talisman\": {\n        \"game_id\": 1450,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:chaos_talisman\": {\n        \"game_id\": 1452,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:cosmic_talisman\": {\n        \"game_id\": 1454,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:death_talisman\": {\n        \"game_id\": 1456,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:law_talisman\": {\n        \"game_id\": 1458,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:soul_talisman\": {\n        \"game_id\": 1460,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:nature_talisman\": {\n        \"game_id\": 1462,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n    \"rs:elemental_talisman\": {\n        \"game_id\": 5516,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A mysterious power emanates from the talisman...\",\n        \"weight\": 0.015\n    },\n\n    \"rs:air_tiara\": {\n        \"game_id\": 5523,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of air.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:body_tiara\": {\n        \"game_id\": 5533,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of the body.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:chaos_tiara\": {\n        \"game_id\": 5543,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of chaos.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:cosmic_tiara\": {\n        \"game_id\": 5539,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of the cosmos.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:death_tiara\": {\n        \"game_id\": 5547,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of death.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:earth_tiara\": {\n        \"game_id\": 5535,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of the earth.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:fire_tiara\": {\n        \"game_id\": 5537,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of fire.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:law_tiara\": {\n        \"game_id\": 5545,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of law.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:mind_tiara\": {\n        \"game_id\": 5529,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of the mind.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:nature_tiara\": {\n        \"game_id\": 5541,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of nature.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:water_tiara\": {\n        \"game_id\": 5531,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of water.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:blood_tiara\": {\n        \"game_id\": 5549,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of blood.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:soul_tiara\": {\n        \"game_id\": 5551,\n        \"tradable\": true,\n        \"stackable\": false,\n        \"examine\": \"A tiara infused with the properties of the soul.\",\n        \"weight\": 1.0,\n        \"equippable\": true,\n        \"equipment_data\": {\n            \"equipment_slot\": \"head\"\n        }\n    },\n    \"rs:small_pouch\": {\n        \"game_id\": 5509\n    },\n    \"rs:medium_pouch\": {\n        \"game_id\": 5510\n    },\n    \"rs:large_pouch\": {\n        \"game_id\": 5512\n    },\n    \"rs:giant_pouch\": {\n        \"game_id\": 5514\n    }\n}\n"
  },
  {
    "path": "data/config/music/musicRegions.json",
    "content": "{\n    \"musicRegions\": [\n        {\n            \"songId\": 363,\n            \"songName\": \"7th Realm\",\n            \"musicTabButtonId\": 299,\n            \"regionIds\": [10645, 10644]\n        },\n        {\n            \"songId\": 177,\n            \"songName\": \"Adventure\",\n            \"musicTabButtonId\": 25,\n            \"regionIds\": [12854]\n        },\n        {\n            \"songId\": 50,\n            \"songName\": \"Al-Kharid\",\n            \"musicTabButtonId\": 26,\n            \"regionIds\": [13105, 13361]\n        },\n        {\n            \"songId\": 73,\n            \"songName\": \"All's Fairy In Love'N'War\",\n            \"musicTabButtonId\": 443,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 102,\n            \"songName\": \"Alone\",\n            \"musicTabButtonId\": 27,\n            \"regionIds\": [12086, 10390, 10134]\n        },\n        {\n            \"songId\": 90,\n            \"songName\": \"Ambient Jungle\",\n            \"musicTabButtonId\": 28,\n            \"regionIds\": [11310]\n        },\n        {\n            \"songId\": 305,\n            \"songName\": \"Anywhere\",\n            \"musicTabButtonId\": 276,\n            \"regionIds\": [10795]\n        },\n        {\n            \"songId\": 123,\n            \"songName\": \"Arabian 2\",\n            \"musicTabButtonId\": 30,\n            \"regionIds\": [13107]\n        },\n        {\n            \"songId\": 124,\n            \"songName\": \"Arabian 3\",\n            \"musicTabButtonId\": 31,\n            \"regionIds\": [12848]\n        },\n        {\n            \"songId\": 36,\n            \"songName\": \"Arabian\",\n            \"musicTabButtonId\": 29,\n            \"regionIds\": [13617, 13106]\n        },\n        {\n            \"songId\": 19,\n            \"songName\": \"Arabique\",\n            \"musicTabButtonId\": 32,\n            \"regionIds\": [11417]\n        },\n        {\n            \"songId\": 160,\n            \"songName\": \"Army of Darkness\",\n            \"musicTabButtonId\": 33,\n            \"regionIds\": [12088]\n        },\n        {\n            \"songId\": 186,\n            \"songName\": \"Arrival\",\n            \"musicTabButtonId\": 34,\n            \"regionIds\": [11572]\n        },\n        {\n            \"songId\": 247,\n            \"songName\": \"Artistry\",\n            \"musicTabButtonId\": 245,\n            \"regionIds\": [8010]\n        },\n        {\n            \"songId\": 24,\n            \"songName\": \"Attack 1\",\n            \"musicTabButtonId\": 35,\n            \"regionIds\": [10034]\n        },\n        {\n            \"songId\": 25,\n            \"songName\": \"Attack 2\",\n            \"musicTabButtonId\": 36,\n            \"regionIds\": [11414]\n        },\n        {\n            \"songId\": 26,\n            \"songName\": \"Attack 3\",\n            \"musicTabButtonId\": 37,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 27,\n            \"songName\": \"Attack 4\",\n            \"musicTabButtonId\": 38,\n            \"regionIds\": [10289]\n        },\n        {\n            \"songId\": 28,\n            \"songName\": \"Attack 5\",\n            \"musicTabButtonId\": 39,\n            \"regionIds\": [9033]\n        },\n        {\n            \"songId\": 29,\n            \"songName\": \"Attack 6\",\n            \"musicTabButtonId\": 40,\n            \"regionIds\": [10387]\n        },\n        {\n            \"songId\": 180,\n            \"songName\": \"Attention\",\n            \"musicTabButtonId\": 41,\n            \"regionIds\": [11825]\n        },\n        {\n            \"songId\": 2,\n            \"songName\": \"Autumn Voyage\",\n            \"musicTabButtonId\": 42,\n            \"regionIds\": [12851]\n        },\n        {\n            \"songId\": 497,\n            \"songName\": \"Aye Car Rum Ba\",\n            \"musicTabButtonId\": 373,\n            \"regionIds\": [8527]\n        },\n        {\n            \"songId\": 248,\n            \"songName\": \"Aztec\",\n            \"musicTabButtonId\": 217,\n            \"regionIds\": [11157, 11158, 11156, 10902]\n        },\n        {\n            \"songId\": 324,\n            \"songName\": \"Background\",\n            \"musicTabButtonId\": 43,\n            \"regionIds\": [11060, 11316, 7758]\n        },\n        {\n            \"songId\": 152,\n            \"songName\": \"Ballad of Enchantment\",\n            \"musicTabButtonId\": 44,\n            \"regionIds\": [10290]\n        },\n        {\n            \"songId\": 263,\n            \"songName\": \"Bandit Camp\",\n            \"musicTabButtonId\": 243,\n            \"regionIds\": [12590]\n        },\n        {\n            \"songId\": 141,\n            \"songName\": \"Barbarianism\",\n            \"musicTabButtonId\": 253,\n            \"regionIds\": [12341, 12441]\n        },\n        {\n            \"songId\": 345,\n            \"songName\": \"Barking Mad\",\n            \"musicTabButtonId\": 305,\n            \"regionIds\": [14234]\n        },\n        {\n            \"songId\": 99,\n            \"songName\": \"Baroque\",\n            \"musicTabButtonId\": 45,\n            \"regionIds\": [10547]\n        },\n        {\n            \"songId\": 100,\n            \"songName\": \"Beyond\",\n            \"musicTabButtonId\": 46,\n            \"regionIds\": [11675, 11419, 11418]\n        },\n        {\n            \"songId\": 83,\n            \"songName\": \"Big Chords\",\n            \"musicTabButtonId\": 47,\n            \"regionIds\": [10032, 11593]\n        },\n        {\n            \"songId\": 498,\n            \"songName\": \"Blistering Barnacles\",\n            \"musicTabButtonId\": 372,\n            \"regionIds\": [8528]\n        },\n        {\n            \"songId\": 342,\n            \"songName\": \"Body Parts\",\n            \"musicTabButtonId\": 304,\n            \"regionIds\": [13979, 14235]\n        },\n        {\n            \"songId\": 154,\n            \"songName\": \"Bone Dance\",\n            \"musicTabButtonId\": 248,\n            \"regionIds\": [13619]\n        },\n        {\n            \"songId\": 266,\n            \"songName\": \"Bone Dry\",\n            \"musicTabButtonId\": 322,\n            \"regionIds\": [12946, 13202]\n        },\n        {\n            \"songId\": 64,\n            \"songName\": \"Book of Spells\",\n            \"musicTabButtonId\": 48,\n            \"regionIds\": [12593]\n        },\n        {\n            \"songId\": 291,\n            \"songName\": \"Borderland\",\n            \"musicTabButtonId\": 258,\n            \"regionIds\": [10809, 10810]\n        },\n        {\n            \"songId\": 132,\n            \"songName\": \"Breeze\",\n            \"musicTabButtonId\": 229,\n            \"regionIds\": [9010]\n        },\n        {\n            \"songId\": 471,\n            \"songName\": \"Brew Hoo Hoo\",\n            \"musicTabButtonId\": 357,\n            \"regionIds\": [14747]\n        },\n        {\n            \"songId\": 194,\n            \"songName\": \"Brimstail's Scales\",\n            \"musicTabButtonId\": 451,\n            \"regionIds\": [9625]\n        },\n        {\n            \"songId\": 489,\n            \"songName\": \"Bubble And Squeak\",\n            \"musicTabButtonId\": 380,\n            \"regionIds\": [7753]\n        },\n        {\n            \"songId\": 545,\n            \"songName\": \"Cabin Fever\",\n            \"musicTabButtonId\": 400,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 104,\n            \"songName\": \"Camelot\",\n            \"musicTabButtonId\": 49,\n            \"regionIds\": [11062, 11063]\n        },\n        {\n            \"songId\": 314,\n            \"songName\": \"Castlewars\",\n            \"musicTabButtonId\": 285,\n            \"regionIds\": [9520, 9620]\n        },\n        {\n            \"songId\": 481,\n            \"songName\": \"Catch Me If You Can\",\n            \"musicTabButtonId\": 379,\n            \"regionIds\": [10646]\n        },\n        {\n            \"songId\": 325,\n            \"songName\": \"Cave Background\",\n            \"musicTabButtonId\": 50,\n            \"regionIds\": [12185, 11929, 12184]\n        },\n        {\n            \"songId\": 357,\n            \"songName\": \"Cave of Beasts\",\n            \"musicTabButtonId\": 311,\n            \"regionIds\": [11165]\n        },\n        {\n            \"songId\": 389,\n            \"songName\": \"Cave of The Goblins\",\n            \"musicTabButtonId\": 313,\n            \"regionIds\": [12949, 12693]\n        },\n        {\n            \"songId\": 68,\n            \"songName\": \"Cavern\",\n            \"musicTabButtonId\": 51,\n            \"regionIds\": [10388, 10389, 12193]\n        },\n        {\n            \"songId\": 330,\n            \"songName\": \"Cellar Song\",\n            \"musicTabButtonId\": 197,\n            \"regionIds\": [12697]\n        },\n        {\n            \"songId\": 63,\n            \"songName\": \"Chain of Command\",\n            \"musicTabButtonId\": 52,\n            \"regionIds\": [10905, 10651, 10648, 10649, 10650]\n        },\n        {\n            \"songId\": 282,\n            \"songName\": \"Chamber\",\n            \"musicTabButtonId\": 291,\n            \"regionIds\": [11078, 10821]\n        },\n        {\n            \"songId\": 583,\n            \"songName\": \"Chef Surprise\",\n            \"musicTabButtonId\": 406,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 575,\n            \"songName\": \"Chickened Out\",\n            \"musicTabButtonId\": 405,\n            \"regionIds\": [9796]\n        },\n        {\n            \"songId\": 71,\n            \"songName\": \"Chompy Hunt\",\n            \"musicTabButtonId\": 202,\n            \"regionIds\": [10542, 10642]\n        },\n        {\n            \"songId\": 383,\n            \"songName\": \"City of The Dead\",\n            \"musicTabButtonId\": 327,\n            \"regionIds\": [12844, 12843, 13099]\n        },\n        {\n            \"songId\": 373,\n            \"songName\": \"Claustrophobia\",\n            \"musicTabButtonId\": 316,\n            \"regionIds\": [9549, 9293]\n        },\n        {\n            \"songId\": 67,\n            \"songName\": \"Close Quarters\",\n            \"musicTabButtonId\": 199,\n            \"regionIds\": [12602]\n        },\n        {\n            \"songId\": 269,\n            \"songName\": \"Competition\",\n            \"musicTabButtonId\": 254,\n            \"regionIds\": [8781]\n        },\n        {\n            \"songId\": 142,\n            \"songName\": \"Complication\",\n            \"musicTabButtonId\": 270,\n            \"regionIds\": [9035]\n        },\n        {\n            \"songId\": 258,\n            \"songName\": \"Contest\",\n            \"musicTabButtonId\": 222,\n            \"regionIds\": [11576]\n        },\n        {\n            \"songId\": 418,\n            \"songName\": \"Corporal Punishment\",\n            \"musicTabButtonId\": 370,\n            \"regionIds\": [12619]\n        },\n        {\n            \"songId\": 509,\n            \"songName\": \"Corridors of Power\",\n            \"musicTabButtonId\": 423,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 178,\n            \"songName\": \"Courage\",\n            \"musicTabButtonId\": 256,\n            \"regionIds\": [11673, 11674]\n        },\n        {\n            \"songId\": 259,\n            \"songName\": \"Crystal Castle\",\n            \"musicTabButtonId\": 231,\n            \"regionIds\": [9011, 9012]\n        },\n        {\n            \"songId\": 181,\n            \"songName\": \"Crystal Cave\",\n            \"musicTabButtonId\": 53,\n            \"regionIds\": [9797]\n        },\n        {\n            \"songId\": 169,\n            \"songName\": \"Crystal Sword\",\n            \"musicTabButtonId\": 54,\n            \"regionIds\": [12855, 10647]\n        },\n        {\n            \"songId\": 59,\n            \"songName\": \"Cursed\",\n            \"musicTabButtonId\": 205,\n            \"regionIds\": [9879, 9623]\n        },\n        {\n            \"songId\": 198,\n            \"songName\": \"Dagannoth Dawn\",\n            \"musicTabButtonId\": 374,\n            \"regionIds\": [7748, 7236]\n        },\n        {\n            \"songId\": 560,\n            \"songName\": \"Dance of Death\",\n            \"musicTabButtonId\": 436,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 380,\n            \"songName\": \"Dance of The Undead\",\n            \"musicTabButtonId\": 334,\n            \"regionIds\": [14131]\n        },\n        {\n            \"songId\": 336,\n            \"songName\": \"Dangerous Road\",\n            \"musicTabButtonId\": 264,\n            \"regionIds\": [11413]\n        },\n        {\n            \"songId\": 381,\n            \"songName\": \"Dangerous Way\",\n            \"musicTabButtonId\": 335,\n            \"regionIds\": [14231]\n        },\n        {\n            \"songId\": 182,\n            \"songName\": \"Dangerous\",\n            \"musicTabButtonId\": 55,\n            \"regionIds\": [12343, 13115, 13371]\n        },\n        {\n            \"songId\": 326,\n            \"songName\": \"Dark\",\n            \"musicTabButtonId\": 56,\n            \"regionIds\": [13113, 13369]\n        },\n        {\n            \"songId\": 576,\n            \"songName\": \"Davy Jones Locker\",\n            \"musicTabButtonId\": 404,\n            \"regionIds\": [11924]\n        },\n        {\n            \"songId\": 476,\n            \"songName\": \"Dead Can Dance\",\n            \"musicTabButtonId\": 359,\n            \"regionIds\": [12601]\n        },\n        {\n            \"songId\": 84,\n            \"songName\": \"Dead Quiet\",\n            \"musicTabButtonId\": 216,\n            \"regionIds\": [13621, 9294, 9550]\n        },\n        {\n            \"songId\": 288,\n            \"songName\": \"Deadlands\",\n            \"musicTabButtonId\": 246,\n            \"regionIds\": [14134, 14390]\n        },\n        {\n            \"songId\": 278,\n            \"songName\": \"Deep Down\",\n            \"musicTabButtonId\": 290,\n            \"regionIds\": [11079, 10822, 10823]\n        },\n        {\n            \"songId\": 37,\n            \"songName\": \"Deep Wildy\",\n            \"musicTabButtonId\": 57,\n            \"regionIds\": [11835, 11836]\n        },\n        {\n            \"songId\": 465,\n            \"songName\": \"Desert Heat\",\n            \"musicTabButtonId\": 385,\n            \"regionIds\": [13615, 13614]\n        },\n        {\n            \"songId\": 174,\n            \"songName\": \"Desert Voyage\",\n            \"musicTabButtonId\": 58,\n            \"regionIds\": [13103, 13359, 13102]\n        },\n        {\n            \"songId\": 532,\n            \"songName\": \"Diango's Little Helpers\",\n            \"musicTabButtonId\": 392,\n            \"regionIds\": [8005]\n        },\n        {\n            \"songId\": 86,\n            \"songName\": \"Dimension X\",\n            \"musicTabButtonId\": 442,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 501,\n            \"songName\": \"Distant Land\",\n            \"musicTabButtonId\": 409,\n            \"regionIds\": [13874, 14130, 13873, 14129]\n        },\n        {\n            \"songId\": 610,\n            \"songName\": \"Distillery Hilarity\",\n            \"musicTabButtonId\": 432,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 537,\n            \"songName\": \"Dogs of War\",\n            \"musicTabButtonId\": 437,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 56,\n            \"songName\": \"Doorways\",\n            \"musicTabButtonId\": 59,\n            \"regionIds\": [12598]\n        },\n        {\n            \"songId\": 361,\n            \"songName\": \"Down Below\",\n            \"musicTabButtonId\": 326,\n            \"regionIds\": [12439, 12438]\n        },\n        {\n            \"songId\": 143,\n            \"songName\": \"Down To Earth\",\n            \"musicTabButtonId\": 255,\n            \"regionIds\": [10571]\n        },\n        {\n            \"songId\": 358,\n            \"songName\": \"Dragontooth Island\",\n            \"musicTabButtonId\": 309,\n            \"regionIds\": [15159]\n        },\n        {\n            \"songId\": 327,\n            \"songName\": \"Dream\",\n            \"musicTabButtonId\": 60,\n            \"regionIds\": [12594]\n        },\n        {\n            \"songId\": 623,\n            \"songName\": \"Dreamstate\",\n            \"musicTabButtonId\": 446,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 47,\n            \"songName\": \"Duel Arena\",\n            \"musicTabButtonId\": 189,\n            \"regionIds\": [13362]\n        },\n        {\n            \"songId\": 173,\n            \"songName\": \"Dunjun\",\n            \"musicTabButtonId\": 61,\n            \"regionIds\": [11928, 11672]\n        },\n        {\n            \"songId\": 351,\n            \"songName\": \"Dynasty\",\n            \"musicTabButtonId\": 317,\n            \"regionIds\": [13358]\n        },\n        {\n            \"songId\": 69,\n            \"songName\": \"Egypt\",\n            \"musicTabButtonId\": 62,\n            \"regionIds\": [13104, 13360]\n        },\n        {\n            \"songId\": 252,\n            \"songName\": \"Elven Mist\",\n            \"musicTabButtonId\": 239,\n            \"regionIds\": [9266]\n        },\n        {\n            \"songId\": 148,\n            \"songName\": \"Emotion\",\n            \"musicTabButtonId\": 63,\n            \"regionIds\": [10033, 10133, 10309, 11081]\n        },\n        {\n            \"songId\": 138,\n            \"songName\": \"Emperor\",\n            \"musicTabButtonId\": 179,\n            \"regionIds\": [11570, 11670]\n        },\n        {\n            \"songId\": 17,\n            \"songName\": \"Escape\",\n            \"musicTabButtonId\": 198,\n            \"regionIds\": [10903]\n        },\n        {\n            \"songId\": 285,\n            \"songName\": \"Etceteria\",\n            \"musicTabButtonId\": 273,\n            \"regionIds\": [10300]\n        },\n        {\n            \"songId\": 586,\n            \"songName\": \"Everlasting Fire\",\n            \"musicTabButtonId\": 418,\n            \"regionIds\": [13373]\n        },\n        {\n            \"songId\": 268,\n            \"songName\": \"Everywhere\",\n            \"musicTabButtonId\": 236,\n            \"regionIds\": [8499, 8755]\n        },\n        {\n            \"songId\": 411,\n            \"songName\": \"Evil Bob's Island\",\n            \"musicTabButtonId\": 358,\n            \"regionIds\": [10058]\n        },\n        {\n            \"songId\": 106,\n            \"songName\": \"Expanse\",\n            \"musicTabButtonId\": 64,\n            \"regionIds\": [12852, 12605, 12952]\n        },\n        {\n            \"songId\": 41,\n            \"songName\": \"Expecting\",\n            \"musicTabButtonId\": 65,\n            \"regionIds\": [9522, 9778, 9878]\n        },\n        {\n            \"songId\": 153,\n            \"songName\": \"Expedition\",\n            \"musicTabButtonId\": 175,\n            \"regionIds\": [11676, 9619]\n        },\n        {\n            \"songId\": 270,\n            \"songName\": \"Exposed\",\n            \"musicTabButtonId\": 232,\n            \"regionIds\": [8752]\n        },\n        {\n            \"songId\": 118,\n            \"songName\": \"Faerie\",\n            \"musicTabButtonId\": 66,\n            \"regionIds\": [9541, 9540]\n        },\n        {\n            \"songId\": 337,\n            \"songName\": \"Faithless\",\n            \"musicTabButtonId\": 265,\n            \"regionIds\": [12856, 13112]\n        },\n        {\n            \"songId\": 72,\n            \"songName\": \"Fanfare\",\n            \"musicTabButtonId\": 67,\n            \"regionIds\": [11828]\n        },\n        {\n            \"songId\": 166,\n            \"songName\": \"Fanfare2\",\n            \"musicTabButtonId\": 68,\n            \"regionIds\": [11823]\n        },\n        {\n            \"songId\": 167,\n            \"songName\": \"Fanfare3\",\n            \"musicTabButtonId\": 69,\n            \"regionIds\": [10545]\n        },\n        {\n            \"songId\": 504,\n            \"songName\": \"Fangs For The Memory\",\n            \"musicTabButtonId\": 410,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 372,\n            \"songName\": \"Far Away\",\n            \"musicTabButtonId\": 348,\n            \"regionIds\": [9265]\n        },\n        {\n            \"songId\": 602,\n            \"songName\": \"Fear And Loathing\",\n            \"musicTabButtonId\": 411,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 344,\n            \"songName\": \"Fenkenstrain's Refrain\",\n            \"musicTabButtonId\": 303,\n            \"regionIds\": [13879, 14135]\n        },\n        {\n            \"songId\": 375,\n            \"songName\": \"Fight Or Flight\",\n            \"musicTabButtonId\": 349,\n            \"regionIds\": [8008, 7752]\n        },\n        {\n            \"songId\": 312,\n            \"songName\": \"Find My Way\",\n            \"musicTabButtonId\": 284,\n            \"regionIds\": [10894, 11150]\n        },\n        {\n            \"songId\": 463,\n            \"songName\": \"Fire And Brimstone\",\n            \"musicTabButtonId\": 365,\n            \"regionIds\": [9552]\n        },\n        {\n            \"songId\": 119,\n            \"songName\": \"Fishing\",\n            \"musicTabButtonId\": 70,\n            \"regionIds\": [11317]\n        },\n        {\n            \"songId\": 163,\n            \"songName\": \"Flute Salad\",\n            \"musicTabButtonId\": 71,\n            \"regionIds\": [12595]\n        },\n        {\n            \"songId\": 558,\n            \"songName\": \"Food For Thought\",\n            \"musicTabButtonId\": 438,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 121,\n            \"songName\": \"Forbidden\",\n            \"musicTabButtonId\": 204,\n            \"regionIds\": [13111, 13367]\n        },\n        {\n            \"songId\": 251,\n            \"songName\": \"Forest\",\n            \"musicTabButtonId\": 238,\n            \"regionIds\": [9009]\n        },\n        {\n            \"songId\": 98,\n            \"songName\": \"Forever\",\n            \"musicTabButtonId\": 72,\n            \"regionIds\": [12342, 12444, 12443, 12442]\n        },\n        {\n            \"songId\": 436,\n            \"songName\": \"Forgettable Melody\",\n            \"musicTabButtonId\": 350,\n            \"regionIds\": [7501]\n        },\n        {\n            \"songId\": 378,\n            \"songName\": \"Forgotten\",\n            \"musicTabButtonId\": 320,\n            \"regionIds\": [10828]\n        },\n        {\n            \"songId\": 409,\n            \"songName\": \"Frogland\",\n            \"musicTabButtonId\": 355,\n            \"regionIds\": [9802]\n        },\n        {\n            \"songId\": 294,\n            \"songName\": \"Frostbite\",\n            \"musicTabButtonId\": 325,\n            \"regionIds\": [11323, 11579]\n        },\n        {\n            \"songId\": 347,\n            \"songName\": \"Fruits De Mer\",\n            \"musicTabButtonId\": 272,\n            \"regionIds\": [11059]\n        },\n        {\n            \"songId\": 603,\n            \"songName\": \"Funny Bunnies\",\n            \"musicTabButtonId\": 415,\n            \"regionIds\": [9810]\n        },\n        {\n            \"songId\": 159,\n            \"songName\": \"Gaol\",\n            \"musicTabButtonId\": 73,\n            \"regionIds\": [10031, 12090, 10131]\n        },\n        {\n            \"songId\": 125,\n            \"songName\": \"Garden\",\n            \"musicTabButtonId\": 74,\n            \"regionIds\": [12853, 12953]\n        },\n        {\n            \"songId\": 22,\n            \"songName\": \"Gnome King\",\n            \"musicTabButtonId\": 75,\n            \"regionIds\": [9782, 9783]\n        },\n        {\n            \"songId\": 150,\n            \"songName\": \"Gnome Theme\",\n            \"musicTabButtonId\": 76,\n            \"regionIds\": [12085]\n        },\n        {\n            \"songId\": 33,\n            \"songName\": \"Gnome Village\",\n            \"musicTabButtonId\": 77,\n            \"regionIds\": [9781]\n        },\n        {\n            \"songId\": 101,\n            \"songName\": \"Gnome Village2\",\n            \"musicTabButtonId\": 78,\n            \"regionIds\": [9269, 9525]\n        },\n        {\n            \"songId\": 23,\n            \"songName\": \"Gnome\",\n            \"musicTabButtonId\": 79,\n            \"regionIds\": [11830]\n        },\n        {\n            \"songId\": 112,\n            \"songName\": \"Gnomeball\",\n            \"musicTabButtonId\": 80,\n            \"regionIds\": [9270, 9526, 9271, 9527]\n        },\n        {\n            \"songId\": 346,\n            \"songName\": \"Goblin Game\",\n            \"musicTabButtonId\": 271,\n            \"regionIds\": [10393]\n        },\n        {\n            \"songId\": 535,\n            \"songName\": \"Golden Touch\",\n            \"musicTabButtonId\": 394,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 116,\n            \"songName\": \"Greatness\",\n            \"musicTabButtonId\": 81,\n            \"regionIds\": [12596]\n        },\n        {\n            \"songId\": 520,\n            \"songName\": \"Grip of The Talon\",\n            \"musicTabButtonId\": 377,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 246,\n            \"songName\": \"Grotto\",\n            \"musicTabButtonId\": 212,\n            \"regionIds\": [13720]\n        },\n        {\n            \"songId\": 466,\n            \"songName\": \"Ground Scape\",\n            \"musicTabButtonId\": 347,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 128,\n            \"songName\": \"Grumpy\",\n            \"musicTabButtonId\": 203,\n            \"regionIds\": [10286]\n        },\n        {\n            \"songId\": 638,\n            \"songName\": \"Ham Fisted\",\n            \"musicTabButtonId\": 430,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 76,\n            \"songName\": \"Harmony\",\n            \"musicTabButtonId\": 82,\n            \"regionIds\": [12850, 7507]\n        },\n        {\n            \"songId\": 46,\n            \"songName\": \"Harmony2\",\n            \"musicTabButtonId\": 192,\n            \"regionIds\": [12950]\n        },\n        {\n            \"songId\": 277,\n            \"songName\": \"Haunted Mine\",\n            \"musicTabButtonId\": 288,\n            \"regionIds\": [11077]\n        },\n        {\n            \"songId\": 434,\n            \"songName\": \"Have A Blast\",\n            \"musicTabButtonId\": 362,\n            \"regionIds\": [7757]\n        },\n        {\n            \"songId\": 612,\n            \"songName\": \"Head To Head\",\n            \"musicTabButtonId\": 421,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 190,\n            \"songName\": \"Heart And Mind\",\n            \"musicTabButtonId\": 200,\n            \"regionIds\": [10059]\n        },\n        {\n            \"songId\": 4,\n            \"songName\": \"Hells Bells\",\n            \"musicTabButtonId\": 293,\n            \"regionIds\": [11066, 11067]\n        },\n        {\n            \"songId\": 97,\n            \"songName\": \"Hermit\",\n            \"musicTabButtonId\": 220,\n            \"regionIds\": [9034]\n        },\n        {\n            \"songId\": 55,\n            \"songName\": \"High Seas\",\n            \"musicTabButtonId\": 83,\n            \"regionIds\": [11057]\n        },\n        {\n            \"songId\": 205,\n            \"songName\": \"High Spirits\",\n            \"musicTabButtonId\": 459,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 454,\n            \"songName\": \"Home Sweet Home\",\n            \"musicTabButtonId\": 428,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 621,\n            \"songName\": \"HomeScape\",\n            \"musicTabButtonId\": 427,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 18,\n            \"songName\": \"Horizon\",\n            \"musicTabButtonId\": 84,\n            \"regionIds\": [11573]\n        },\n        {\n            \"songId\": 384,\n            \"songName\": \"Hypnotized\",\n            \"musicTabButtonId\": 330,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 1,\n            \"songName\": \"Iban\",\n            \"musicTabButtonId\": 85,\n            \"regionIds\": [8519, 8520, 8521]\n        },\n        {\n            \"songId\": 87,\n            \"songName\": \"Ice Melody\",\n            \"musicTabButtonId\": 190,\n            \"regionIds\": [11318]\n        },\n        {\n            \"songId\": 370,\n            \"songName\": \"In Between\",\n            \"musicTabButtonId\": 315,\n            \"regionIds\": [10317, 10061]\n        },\n        {\n            \"songId\": 530,\n            \"songName\": \"In The Brine\",\n            \"musicTabButtonId\": 401,\n            \"regionIds\": [14638]\n        },\n        {\n            \"songId\": 511,\n            \"songName\": \"In The Clink\",\n            \"musicTabButtonId\": 397,\n            \"regionIds\": [8261]\n        },\n        {\n            \"songId\": 188,\n            \"songName\": \"In The Manor\",\n            \"musicTabButtonId\": 86,\n            \"regionIds\": [10287]\n        },\n        {\n            \"songId\": 469,\n            \"songName\": \"In The Pits\",\n            \"musicTabButtonId\": 366,\n            \"regionIds\": [9808]\n        },\n        {\n            \"songId\": 519,\n            \"songName\": \"Incantation\",\n            \"musicTabButtonId\": 378,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 260,\n            \"songName\": \"Insect Queen\",\n            \"musicTabButtonId\": 228,\n            \"regionIds\": [13972, 14228]\n        },\n        {\n            \"songId\": 96,\n            \"songName\": \"Inspiration\",\n            \"musicTabButtonId\": 87,\n            \"regionIds\": [12087]\n        },\n        {\n            \"songId\": 412,\n            \"songName\": \"Into The Abyss\",\n            \"musicTabButtonId\": 343,\n            \"regionIds\": [12108, 12107]\n        },\n        {\n            \"songId\": 95,\n            \"songName\": \"Intrepid\",\n            \"musicTabButtonId\": 88,\n            \"regionIds\": [9369, 9370]\n        },\n        {\n            \"songId\": 306,\n            \"songName\": \"Island Life\",\n            \"musicTabButtonId\": 280,\n            \"regionIds\": [10794, 11050]\n        },\n        {\n            \"songId\": 627,\n            \"songName\": \"Isle of Everywhere\",\n            \"musicTabButtonId\": 448,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 6,\n            \"songName\": \"Jolly-R\",\n            \"musicTabButtonId\": 89,\n            \"regionIds\": [11058]\n        },\n        {\n            \"songId\": 172,\n            \"songName\": \"Jungle Island\",\n            \"musicTabButtonId\": 90,\n            \"regionIds\": [11309, 11313]\n        },\n        {\n            \"songId\": 479,\n            \"songName\": \"Jungle Troubles\",\n            \"musicTabButtonId\": 363,\n            \"regionIds\": [11568, 10055]\n        },\n        {\n            \"songId\": 114,\n            \"songName\": \"Jungly 1\",\n            \"musicTabButtonId\": 91,\n            \"regionIds\": [11054, 11154]\n        },\n        {\n            \"songId\": 115,\n            \"songName\": \"Jungly 2\",\n            \"musicTabButtonId\": 92,\n            \"regionIds\": [10802]\n        },\n        {\n            \"songId\": 117,\n            \"songName\": \"Jungly 3\",\n            \"musicTabButtonId\": 93,\n            \"regionIds\": [11055]\n        },\n        {\n            \"songId\": 362,\n            \"songName\": \"Karamja Jam\",\n            \"musicTabButtonId\": 221,\n            \"regionIds\": [10900, 10899]\n        },\n        {\n            \"songId\": 9,\n            \"songName\": \"Kingdom\",\n            \"musicTabButtonId\": 298,\n            \"regionIds\": [11319]\n        },\n        {\n            \"songId\": 191,\n            \"songName\": \"Knightly\",\n            \"musicTabButtonId\": 94,\n            \"regionIds\": [10291]\n        },\n        {\n            \"songId\": 134,\n            \"songName\": \"La Mort\",\n            \"musicTabButtonId\": 369,\n            \"regionIds\": [8779]\n        },\n        {\n            \"songId\": 287,\n            \"songName\": \"Lair\",\n            \"musicTabButtonId\": 247,\n            \"regionIds\": [13975]\n        },\n        {\n            \"songId\": 197,\n            \"songName\": \"Lament of Meiyerditch\",\n            \"musicTabButtonId\": 452,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 542,\n            \"songName\": \"Lament\",\n            \"musicTabButtonId\": 399,\n            \"regionIds\": [12433]\n        },\n        {\n            \"songId\": 506,\n            \"songName\": \"Land Down Under\",\n            \"musicTabButtonId\": 424,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 396,\n            \"songName\": \"Land of the Dwarves\",\n            \"musicTabButtonId\": 342,\n            \"regionIds\": [11423]\n        },\n        {\n            \"songId\": 164,\n            \"songName\": \"Landlubber\",\n            \"musicTabButtonId\": 194,\n            \"regionIds\": [10801]\n        },\n        {\n            \"songId\": 546,\n            \"songName\": \"Last Stand\",\n            \"musicTabButtonId\": 419,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 60,\n            \"songName\": \"Lasting\",\n            \"musicTabButtonId\": 95,\n            \"regionIds\": [10549]\n        },\n        {\n            \"songId\": 293,\n            \"songName\": \"Legend\",\n            \"musicTabButtonId\": 241,\n            \"regionIds\": [10808, 11064]\n        },\n        {\n            \"songId\": 66,\n            \"songName\": \"Legion\",\n            \"musicTabButtonId\": 96,\n            \"regionIds\": [10039, 10295, 12089]\n        },\n        {\n            \"songId\": 631,\n            \"songName\": \"Life's A Beach!\",\n            \"musicTabButtonId\": 433,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 320,\n            \"songName\": \"Lighthouse\",\n            \"musicTabButtonId\": 267,\n            \"regionIds\": [10040, 9799]\n        },\n        {\n            \"songId\": 113,\n            \"songName\": \"Lightness\",\n            \"musicTabButtonId\": 97,\n            \"regionIds\": [12599]\n        },\n        {\n            \"songId\": 74,\n            \"songName\": \"Lightwalk\",\n            \"musicTabButtonId\": 98,\n            \"regionIds\": [11061]\n        },\n        {\n            \"songId\": 632,\n            \"songName\": \"Little Cave of Horrors\",\n            \"musicTabButtonId\": 434,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 168,\n            \"songName\": \"Lonesome\",\n            \"musicTabButtonId\": 187,\n            \"regionIds\": [13203]\n        },\n        {\n            \"songId\": 161,\n            \"songName\": \"Long Ago\",\n            \"musicTabButtonId\": 99,\n            \"regionIds\": [10544]\n        },\n        {\n            \"songId\": 12,\n            \"songName\": \"Long Way Home\",\n            \"musicTabButtonId\": 100,\n            \"regionIds\": [11826]\n        },\n        {\n            \"songId\": 253,\n            \"songName\": \"Lost Soul\",\n            \"musicTabButtonId\": 233,\n            \"regionIds\": [9008, 9264]\n        },\n        {\n            \"songId\": 20,\n            \"songName\": \"Lullaby\",\n            \"musicTabButtonId\": 102,\n            \"regionIds\": [13365, 10551]\n        },\n        {\n            \"songId\": 264,\n            \"songName\": \"Mad Eadgar\",\n            \"musicTabButtonId\": 225,\n            \"regionIds\": [11677]\n        },\n        {\n            \"songId\": 13,\n            \"songName\": \"Mage Arena\",\n            \"musicTabButtonId\": 103,\n            \"regionIds\": [12348, 12349, 10057]\n        },\n        {\n            \"songId\": 185,\n            \"songName\": \"Magic Dance\",\n            \"musicTabButtonId\": 104,\n            \"regionIds\": [10288]\n        },\n        {\n            \"songId\": 184,\n            \"songName\": \"Magical Journey\",\n            \"musicTabButtonId\": 105,\n            \"regionIds\": [10805]\n        },\n        {\n            \"songId\": 544,\n            \"songName\": \"Making Waves\",\n            \"musicTabButtonId\": 420,\n            \"regionIds\": [9272, 9528, 9273]\n        },\n        {\n            \"songId\": 559,\n            \"songName\": \"Malady\",\n            \"musicTabButtonId\": 439,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 328,\n            \"songName\": \"March\",\n            \"musicTabButtonId\": 101,\n            \"regionIds\": [10036]\n        },\n        {\n            \"songId\": 304,\n            \"songName\": \"Marooned\",\n            \"musicTabButtonId\": 279,\n            \"regionIds\": [11562, 12117]\n        },\n        {\n            \"songId\": 261,\n            \"songName\": \"Marzipan\",\n            \"musicTabButtonId\": 226,\n            \"regionIds\": [11421, 11167, 11166]\n        },\n        {\n            \"songId\": 340,\n            \"songName\": \"Masquerade\",\n            \"musicTabButtonId\": 300,\n            \"regionIds\": [10908]\n        },\n        {\n            \"songId\": 577,\n            \"songName\": \"Mastermindless\",\n            \"musicTabButtonId\": 407,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 156,\n            \"songName\": \"Mausoleum\",\n            \"musicTabButtonId\": 209,\n            \"regionIds\": [13722]\n        },\n        {\n            \"songId\": 508,\n            \"songName\": \"Meddling Kids\",\n            \"musicTabButtonId\": 425,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 157,\n            \"songName\": \"Medieval\",\n            \"musicTabButtonId\": 106,\n            \"regionIds\": [13109]\n        },\n        {\n            \"songId\": 193,\n            \"songName\": \"Mellow\",\n            \"musicTabButtonId\": 107,\n            \"regionIds\": [10293]\n        },\n        {\n            \"songId\": 317,\n            \"songName\": \"Melodrama\",\n            \"musicTabButtonId\": 286,\n            \"regionIds\": [9776]\n        },\n        {\n            \"songId\": 254,\n            \"songName\": \"Meridian\",\n            \"musicTabButtonId\": 234,\n            \"regionIds\": [8497, 8753, 9287]\n        },\n        {\n            \"songId\": 600,\n            \"songName\": \"Method of Madness\",\n            \"musicTabButtonId\": 412,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 107,\n            \"songName\": \"Miles Away\",\n            \"musicTabButtonId\": 108,\n            \"regionIds\": [11571, 10569]\n        },\n        {\n            \"songId\": 534,\n            \"songName\": \"Mind Over Matter\",\n            \"musicTabButtonId\": 395,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 65,\n            \"songName\": \"Miracle Dance\",\n            \"musicTabButtonId\": 109,\n            \"regionIds\": [11083]\n        },\n        {\n            \"songId\": 388,\n            \"songName\": \"Mirage\",\n            \"musicTabButtonId\": 331,\n            \"regionIds\": [13199]\n        },\n        {\n            \"songId\": 284,\n            \"songName\": \"Miscellania\",\n            \"musicTabButtonId\": 274,\n            \"regionIds\": [10044]\n        },\n        {\n            \"songId\": 200,\n            \"songName\": \"The Mollusc Menace\",\n            \"musicTabButtonId\": 455,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 21,\n            \"songName\": \"Monarch Waltz\",\n            \"musicTabButtonId\": 110,\n            \"regionIds\": [10807]\n        },\n        {\n            \"songId\": 303,\n            \"songName\": \"Monkey Madness\",\n            \"musicTabButtonId\": 278,\n            \"regionIds\": [11051]\n        },\n        {\n            \"songId\": 343,\n            \"songName\": \"Monster Melee\",\n            \"musicTabButtonId\": 310,\n            \"regionIds\": [12694]\n        },\n        {\n            \"songId\": 10,\n            \"songName\": \"Moody\",\n            \"musicTabButtonId\": 111,\n            \"regionIds\": [9523, 9779, 12600]\n        },\n        {\n            \"songId\": 48,\n            \"songName\": \"Morytania\",\n            \"musicTabButtonId\": 210,\n            \"regionIds\": [13622]\n        },\n        {\n            \"songId\": 515,\n            \"songName\": \"Mudskipper Melody\",\n            \"musicTabButtonId\": 371,\n            \"regionIds\": [11824]\n        },\n        {\n            \"songId\": 203,\n            \"songName\": \"My Arm's Journey\",\n            \"musicTabButtonId\": 457,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 348,\n            \"songName\": \"Narnode's Theme\",\n            \"musicTabButtonId\": 275,\n            \"regionIds\": [9882]\n        },\n        {\n            \"songId\": 245,\n            \"songName\": \"Natural\",\n            \"musicTabButtonId\": 213,\n            \"regionIds\": [13620, 9038]\n        },\n        {\n            \"songId\": 155,\n            \"songName\": \"Neverland\",\n            \"musicTabButtonId\": 112,\n            \"regionIds\": [9780]\n        },\n        {\n            \"songId\": 62,\n            \"songName\": \"Newbie Melody\",\n            \"musicTabButtonId\": 113,\n            \"regionIds\": [12080, 12336, 12592, 12079, 12335]\n        },\n        {\n            \"songId\": 646,\n            \"songName\": \"Night of The Vampyre\",\n            \"musicTabButtonId\": 454,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 127,\n            \"songName\": \"Nightfall\",\n            \"musicTabButtonId\": 114,\n            \"regionIds\": [11827, 12861]\n        },\n        {\n            \"songId\": 594,\n            \"songName\": \"No Way Out\",\n            \"musicTabButtonId\": 413,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 58,\n            \"songName\": \"Nomad\",\n            \"musicTabButtonId\": 201,\n            \"regionIds\": [11056]\n        },\n        {\n            \"songId\": 587,\n            \"songName\": \"Null And Void\",\n            \"musicTabButtonId\": 416,\n            \"regionIds\": [10537]\n        },\n        {\n            \"songId\": 633,\n            \"songName\": \"On The Wing\",\n            \"musicTabButtonId\": 440,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 103,\n            \"songName\": \"Oriental\",\n            \"musicTabButtonId\": 115,\n            \"regionIds\": [11666, 11668]\n        },\n        {\n            \"songId\": 322,\n            \"songName\": \"Out of The Deep\",\n            \"musicTabButtonId\": 268,\n            \"regionIds\": [10140, 10056]\n        },\n        {\n            \"songId\": 447,\n            \"songName\": \"Over To Nardah\",\n            \"musicTabButtonId\": 387,\n            \"regionIds\": [13613]\n        },\n        {\n            \"songId\": 256,\n            \"songName\": \"Overpass\",\n            \"musicTabButtonId\": 237,\n            \"regionIds\": [9267]\n        },\n        {\n            \"songId\": 7,\n            \"songName\": \"Overture\",\n            \"musicTabButtonId\": 116,\n            \"regionIds\": [10806]\n        },\n        {\n            \"songId\": 93,\n            \"songName\": \"Parade\",\n            \"musicTabButtonId\": 117,\n            \"regionIds\": [13110, 13366]\n        },\n        {\n            \"songId\": 393,\n            \"songName\": \"Path of Peril\",\n            \"musicTabButtonId\": 323,\n            \"regionIds\": [10575, 10831]\n        },\n        {\n            \"songId\": 364,\n            \"songName\": \"Pathways\",\n            \"musicTabButtonId\": 297,\n            \"regionIds\": [10901]\n        },\n        {\n            \"songId\": 588,\n            \"songName\": \"Pest Control\",\n            \"musicTabButtonId\": 417,\n            \"regionIds\": [10536]\n        },\n        {\n            \"songId\": 505,\n            \"songName\": \"Pharaoh's Tomb\",\n            \"musicTabButtonId\": 398,\n            \"regionIds\": [13356, 12105]\n        },\n        {\n            \"songId\": 354,\n            \"songName\": \"Phasmatys\",\n            \"musicTabButtonId\": 307,\n            \"regionIds\": [14746]\n        },\n        {\n            \"songId\": 419,\n            \"songName\": \"Pheasant Peasant\",\n            \"musicTabButtonId\": 354,\n            \"regionIds\": [10314]\n        },\n        {\n            \"songId\": 614,\n            \"songName\": \"Pinball Wizard\",\n            \"musicTabButtonId\": 422,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 149,\n            \"songName\": \"Principality\",\n            \"musicTabButtonId\": 219,\n            \"regionIds\": [11575]\n        },\n        {\n            \"songId\": 334,\n            \"songName\": \"Pirates of Peril\",\n            \"musicTabButtonId\": 263,\n            \"regionIds\": [12093]\n        },\n        {\n            \"songId\": 158,\n            \"songName\": \"Quest\",\n            \"musicTabButtonId\": 118,\n            \"regionIds\": [10315]\n        },\n        {\n            \"songId\": 482,\n            \"songName\": \"Rat A Tat Tat\",\n            \"musicTabButtonId\": 382,\n            \"regionIds\": [11599]\n        },\n        {\n            \"songId\": 491,\n            \"songName\": \"Rat Hunt\",\n            \"musicTabButtonId\": 383,\n            \"regionIds\": [11343]\n        },\n        {\n            \"songId\": 318,\n            \"songName\": \"Ready For Battle\",\n            \"musicTabButtonId\": 287,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 329,\n            \"songName\": \"Regal\",\n            \"musicTabButtonId\": 119,\n            \"regionIds\": [13117]\n        },\n        {\n            \"songId\": 78,\n            \"songName\": \"Reggae\",\n            \"musicTabButtonId\": 120,\n            \"regionIds\": [11565, 11821]\n        },\n        {\n            \"songId\": 89,\n            \"songName\": \"Reggae2\",\n            \"musicTabButtonId\": 121,\n            \"regionIds\": [11567]\n        },\n        {\n            \"songId\": 289,\n            \"songName\": \"Rellekka\",\n            \"musicTabButtonId\": 259,\n            \"regionIds\": [10297, 10553, 10554]\n        },\n        {\n            \"songId\": 44,\n            \"songName\": \"Right On Track\",\n            \"musicTabButtonId\": 351,\n            \"regionIds\": [7501]\n        },\n        {\n            \"songId\": 262,\n            \"songName\": \"Righteousness\",\n            \"musicTabButtonId\": 227,\n            \"regionIds\": [9803]\n        },\n        {\n            \"songId\": 91,\n            \"songName\": \"Riverside\",\n            \"musicTabButtonId\": 122,\n            \"regionIds\": [8496, 10803]\n        },\n        {\n            \"songId\": 204,\n            \"songName\": \"Roc and Roll\",\n            \"musicTabButtonId\": 458,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 533,\n            \"songName\": \"Roll The Bones\",\n            \"musicTabButtonId\": 396,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 335,\n            \"songName\": \"Romancing The Crone\",\n            \"musicTabButtonId\": 294,\n            \"regionIds\": [11068]\n        },\n        {\n            \"songId\": 390,\n            \"songName\": \"Romper Chomper\",\n            \"musicTabButtonId\": 338,\n            \"regionIds\": [9263, 9519]\n        },\n        {\n            \"songId\": 53,\n            \"songName\": \"Royale\",\n            \"musicTabButtonId\": 123,\n            \"regionIds\": [11671]\n        },\n        {\n            \"songId\": 57,\n            \"songName\": \"Rune Essence\",\n            \"musicTabButtonId\": 124,\n            \"regionIds\": [11595]\n        },\n        {\n            \"songId\": 5,\n            \"songName\": \"Sad Meadow\",\n            \"musicTabButtonId\": 125,\n            \"regionIds\": [10035]\n        },\n        {\n            \"songId\": 290,\n            \"songName\": \"Saga\",\n            \"musicTabButtonId\": 240,\n            \"regionIds\": [10296, 10552]\n        },\n        {\n            \"songId\": 359,\n            \"songName\": \"Sarcophagus\",\n            \"musicTabButtonId\": 324,\n            \"regionIds\": [12945]\n        },\n        {\n            \"songId\": 490,\n            \"songName\": \"Sarim's Vermin\",\n            \"musicTabButtonId\": 384,\n            \"regionIds\": [11926]\n        },\n        {\n            \"songId\": 144,\n            \"songName\": \"Scape Cave\",\n            \"musicTabButtonId\": 126,\n            \"regionIds\": [12436, 12698, 13210, 12954]\n        },\n        {\n            \"songId\": 0,\n            \"songName\": \"Scape Main\",\n            \"musicTabButtonId\": 318,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 400,\n            \"songName\": \"Scape Original\",\n            \"musicTabButtonId\": 127,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 331,\n            \"songName\": \"Scape Sad\",\n            \"musicTabButtonId\": 128,\n            \"regionIds\": [13116, 13372]\n        },\n        {\n            \"songId\": 547,\n            \"songName\": \"Scape Santa\",\n            \"musicTabButtonId\": 292,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 321,\n            \"songName\": \"Scape Scared\",\n            \"musicTabButtonId\": 262,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 54,\n            \"songName\": \"Scape Soft\",\n            \"musicTabButtonId\": 184,\n            \"regionIds\": [11829]\n        },\n        {\n            \"songId\": 332,\n            \"songName\": \"Scape Wild\",\n            \"musicTabButtonId\": 129,\n            \"regionIds\": [12857, 12604, 12860]\n        },\n        {\n            \"songId\": 352,\n            \"songName\": \"Scarab\",\n            \"musicTabButtonId\": 328,\n            \"regionIds\": [12589, 12845, 13101, 11085, 11341, 11597]\n        },\n        {\n            \"songId\": 35,\n            \"songName\": \"Sea Shanty 2\",\n            \"musicTabButtonId\": 131,\n            \"regionIds\": [12082]\n        },\n        {\n            \"songId\": 92,\n            \"songName\": \"Sea Shanty\",\n            \"musicTabButtonId\": 130,\n            \"regionIds\": [11569]\n        },\n        {\n            \"songId\": 110,\n            \"songName\": \"Serenade\",\n            \"musicTabButtonId\": 132,\n            \"regionIds\": [9521, 9777]\n        },\n        {\n            \"songId\": 52,\n            \"songName\": \"Serene\",\n            \"musicTabButtonId\": 133,\n            \"regionIds\": [11936, 11937, 11339, 11837]\n        },\n        {\n            \"songId\": 356,\n            \"songName\": \"Settlement\",\n            \"musicTabButtonId\": 312,\n            \"regionIds\": [11065]\n        },\n        {\n            \"songId\": 286,\n            \"songName\": \"Shadowland\",\n            \"musicTabButtonId\": 249,\n            \"regionIds\": [13618, 13875, 8526]\n        },\n        {\n            \"songId\": 122,\n            \"songName\": \"Shine\",\n            \"musicTabButtonId\": 134,\n            \"regionIds\": [13363]\n        },\n        {\n            \"songId\": 120,\n            \"songName\": \"Shining\",\n            \"musicTabButtonId\": 186,\n            \"regionIds\": [12858]\n        },\n        {\n            \"songId\": 353,\n            \"songName\": \"Shipwrecked\",\n            \"musicTabButtonId\": 306,\n            \"regionIds\": [14391]\n        },\n        {\n            \"songId\": 311,\n            \"songName\": \"Showdown\",\n            \"musicTabButtonId\": 283,\n            \"regionIds\": [10895]\n        },\n        {\n            \"songId\": 640,\n            \"songName\": \"Sigmund's Showdown\",\n            \"musicTabButtonId\": 431,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 257,\n            \"songName\": \"Sojourn\",\n            \"musicTabButtonId\": 224,\n            \"regionIds\": [11321, 11577]\n        },\n        {\n            \"songId\": 80,\n            \"songName\": \"Soundscape\",\n            \"musicTabButtonId\": 135,\n            \"regionIds\": [9774, 10030]\n        },\n        {\n            \"songId\": 387,\n            \"songName\": \"Sphinx\",\n            \"musicTabButtonId\": 329,\n            \"regionIds\": [13100]\n        },\n        {\n            \"songId\": 175,\n            \"songName\": \"Spirit\",\n            \"musicTabButtonId\": 136,\n            \"regionIds\": [12597]\n        },\n        {\n            \"songId\": 462,\n            \"songName\": \"Spirits of The Elid\",\n            \"musicTabButtonId\": 388,\n            \"regionIds\": [13461]\n        },\n        {\n            \"songId\": 77,\n            \"songName\": \"Splendour\",\n            \"musicTabButtonId\": 137,\n            \"regionIds\": [11574]\n        },\n        {\n            \"songId\": 129,\n            \"songName\": \"Spooky Jungle\",\n            \"musicTabButtonId\": 139,\n            \"regionIds\": [11053]\n        },\n        {\n            \"songId\": 333,\n            \"songName\": \"Spooky\",\n            \"musicTabButtonId\": 138,\n            \"regionIds\": [12340]\n        },\n        {\n            \"songId\": 11,\n            \"songName\": \"Spooky2\",\n            \"musicTabButtonId\": 289,\n            \"regionIds\": [13718, 13974]\n        },\n        {\n            \"songId\": 241,\n            \"songName\": \"Stagnant\",\n            \"musicTabButtonId\": 215,\n            \"regionIds\": [13876, 8782]\n        },\n        {\n            \"songId\": 108,\n            \"songName\": \"Starlight\",\n            \"musicTabButtonId\": 140,\n            \"regionIds\": [12181, 11925]\n        },\n        {\n            \"songId\": 151,\n            \"songName\": \"Start\",\n            \"musicTabButtonId\": 172,\n            \"regionIds\": [12339]\n        },\n        {\n            \"songId\": 111,\n            \"songName\": \"Still Night\",\n            \"musicTabButtonId\": 141,\n            \"regionIds\": [13108]\n        },\n        {\n            \"songId\": 319,\n            \"songName\": \"Stillness\",\n            \"musicTabButtonId\": 296,\n            \"regionIds\": [13977]\n        },\n        {\n            \"songId\": 292,\n            \"songName\": \"Stranded\",\n            \"musicTabButtonId\": 295,\n            \"regionIds\": [11322, 11578]\n        },\n        {\n            \"songId\": 470,\n            \"songName\": \"Strange Place\",\n            \"musicTabButtonId\": 364,\n            \"regionIds\": [7494]\n        },\n        {\n            \"songId\": 243,\n            \"songName\": \"Stratosphere\",\n            \"musicTabButtonId\": 207,\n            \"regionIds\": [8523]\n        },\n        {\n            \"songId\": 568,\n            \"songName\": \"Storm Brew\",\n            \"musicTabButtonId\": 402,\n            \"regionIds\": [10577]\n        },\n        {\n            \"songId\": 517,\n            \"songName\": \"Subterranea\",\n            \"musicTabButtonId\": 375,\n            \"regionIds\": [10142, 10398]\n        },\n        {\n            \"songId\": 267,\n            \"songName\": \"Sunburn\",\n            \"musicTabButtonId\": 242,\n            \"regionIds\": [12846, 13357]\n        },\n        {\n            \"songId\": 265,\n            \"songName\": \"Superstition\",\n            \"musicTabButtonId\": 257,\n            \"regionIds\": [11153]\n        },\n        {\n            \"songId\": 308,\n            \"songName\": \"Suspicious\",\n            \"musicTabButtonId\": 282,\n            \"regionIds\": [10567, 10311]\n        },\n        {\n            \"songId\": 395,\n            \"songName\": \"Tale of Keldagrim\",\n            \"musicTabButtonId\": 341,\n            \"regionIds\": [11678, 11679]\n        },\n        {\n            \"songId\": 140,\n            \"songName\": \"Talking Forest\",\n            \"musicTabButtonId\": 173,\n            \"regionIds\": [10550]\n        },\n        {\n            \"songId\": 397,\n            \"songName\": \"Tears of Guthix\",\n            \"musicTabButtonId\": 332,\n            \"regionIds\": [12948]\n        },\n        {\n            \"songId\": 296,\n            \"songName\": \"Technology\",\n            \"musicTabButtonId\": 277,\n            \"regionIds\": [10566, 10310, 9626]\n        },\n        {\n            \"songId\": 376,\n            \"songName\": \"Temple of Light\",\n            \"musicTabButtonId\": 368,\n            \"regionIds\": [7496]\n        },\n        {\n            \"songId\": 307,\n            \"songName\": \"Temple\",\n            \"musicTabButtonId\": 281,\n            \"regionIds\": [11151]\n        },\n        {\n            \"songId\": 478,\n            \"songName\": \"The Cellar Dwellers\",\n            \"musicTabButtonId\": 361,\n            \"regionIds\": [10135, 10391]\n        },\n        {\n            \"songId\": 425,\n            \"songName\": \"The Chosen\",\n            \"musicTabButtonId\": 346,\n            \"regionIds\": [9805]\n        },\n        {\n            \"songId\": 79,\n            \"songName\": \"The Desert\",\n            \"musicTabButtonId\": 142,\n            \"regionIds\": [12591, 12847]\n        },\n        {\n            \"songId\": 461,\n            \"songName\": \"The Desolate Isle\",\n            \"musicTabButtonId\": 352,\n            \"regionIds\": [10042]\n        },\n        {\n            \"songId\": 541,\n            \"songName\": \"The Enchanter\",\n            \"musicTabButtonId\": 393,\n            \"regionIds\": [13462]\n        },\n        {\n            \"songId\": 403,\n            \"songName\": \"The Far Side\",\n            \"musicTabButtonId\": 345,\n            \"regionIds\": [12111]\n        },\n        {\n            \"songId\": 630,\n            \"songName\": \"The Galleon\",\n            \"musicTabButtonId\": 450,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 464,\n            \"songName\": \"The Genie\",\n            \"musicTabButtonId\": 386,\n            \"regionIds\": [13457]\n        },\n        {\n            \"songId\": 377,\n            \"songName\": \"The Golem\",\n            \"musicTabButtonId\": 319,\n            \"regionIds\": [13616, 13872]\n        },\n        {\n            \"songId\": 643,\n            \"songName\": \"The Last Shanty\",\n            \"musicTabButtonId\": 453,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 407,\n            \"songName\": \"The Lost Melody\",\n            \"musicTabButtonId\": 339,\n            \"regionIds\": [13206]\n        },\n        {\n            \"songId\": 420,\n            \"songName\": \"The Lost Tribe\",\n            \"musicTabButtonId\": 340,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 625,\n            \"songName\": \"The Lunar Isle\",\n            \"musicTabButtonId\": 447,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 573,\n            \"songName\": \"The Mad Mole\",\n            \"musicTabButtonId\": 403,\n            \"regionIds\": [6992, 6993]\n        },\n        {\n            \"songId\": 448,\n            \"songName\": \"The Monsters Below\",\n            \"musicTabButtonId\": 353,\n            \"regionIds\": [9886]\n        },\n        {\n            \"songId\": 316,\n            \"songName\": \"The Navigator\",\n            \"musicTabButtonId\": 261,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 485,\n            \"songName\": \"The Noble Rodent\",\n            \"musicTabButtonId\": 381,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 355,\n            \"songName\": \"The Other Side\",\n            \"musicTabButtonId\": 308,\n            \"regionIds\": [14646, 14647]\n        },\n        {\n            \"songId\": 398,\n            \"songName\": \"The Power of Tears\",\n            \"musicTabButtonId\": 333,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 202,\n            \"songName\": \"Prime Time\",\n            \"musicTabButtonId\": 456,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 413,\n            \"songName\": \"The Quizmaster\",\n            \"musicTabButtonId\": 356,\n            \"regionIds\": [7754]\n        },\n        {\n            \"songId\": 402,\n            \"songName\": \"The Rogues Den\",\n            \"musicTabButtonId\": 344,\n            \"regionIds\": [11855, 11854, 12110, 12109]\n        },\n        {\n            \"songId\": 170,\n            \"songName\": \"The Shadow\",\n            \"musicTabButtonId\": 143,\n            \"regionIds\": [11314, 11315]\n        },\n        {\n            \"songId\": 341,\n            \"songName\": \"The Slayer\",\n            \"musicTabButtonId\": 302,\n            \"regionIds\": [11164]\n        },\n        {\n            \"songId\": 510,\n            \"songName\": \"Slither and Thither\",\n            \"musicTabButtonId\": 426,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 201,\n            \"songName\": \"Slug a bug Ball\",\n            \"musicTabButtonId\": 266,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 339,\n            \"songName\": \"The Terrible Tower\",\n            \"musicTabButtonId\": 301,\n            \"regionIds\": [13623]\n        },\n        {\n            \"songId\": 133,\n            \"songName\": \"The Tower\",\n            \"musicTabButtonId\": 144,\n            \"regionIds\": [10292, 10136, 10392]\n        },\n        {\n            \"songId\": 109,\n            \"songName\": \"Theme\",\n            \"musicTabButtonId\": 174,\n            \"regionIds\": [10294, 10138, 10394]\n        },\n        {\n            \"songId\": 379,\n            \"songName\": \"Throne of The Demon\",\n            \"musicTabButtonId\": 321,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 242,\n            \"songName\": \"Time Out\",\n            \"musicTabButtonId\": 218,\n            \"regionIds\": [11591]\n        },\n        {\n            \"songId\": 369,\n            \"songName\": \"Time To Mine\",\n            \"musicTabButtonId\": 314,\n            \"regionIds\": [11422]\n        },\n        {\n            \"songId\": 338,\n            \"songName\": \"Tiptoe\",\n            \"musicTabButtonId\": 269,\n            \"regionIds\": [12440]\n        },\n        {\n            \"songId\": 525,\n            \"songName\": \"Title Fight\",\n            \"musicTabButtonId\": 389,\n            \"regionIds\": [12696]\n        },\n        {\n            \"songId\": 591,\n            \"songName\": \"Tomb Raider\",\n            \"musicTabButtonId\": 444,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 105,\n            \"songName\": \"Tomorrow\",\n            \"musicTabButtonId\": 188,\n            \"regionIds\": [12081]\n        },\n        {\n            \"songId\": 582,\n            \"songName\": \"Too Many Cooks...\",\n            \"musicTabButtonId\": 408,\n            \"regionIds\": [11930]\n        },\n        {\n            \"songId\": 51,\n            \"songName\": \"Trawler Minor\",\n            \"musicTabButtonId\": 146,\n            \"regionIds\": [7755, 8011]\n        },\n        {\n            \"songId\": 38,\n            \"songName\": \"Trawler\",\n            \"musicTabButtonId\": 145,\n            \"regionIds\": [7499, 8012]\n        },\n        {\n            \"songId\": 130,\n            \"songName\": \"Tree Spirits\",\n            \"musicTabButtonId\": 147,\n            \"regionIds\": [9268, 9524]\n        },\n        {\n            \"songId\": 187,\n            \"songName\": \"Tremble\",\n            \"musicTabButtonId\": 223,\n            \"regionIds\": [11320]\n        },\n        {\n            \"songId\": 94,\n            \"songName\": \"Tribal 2\",\n            \"musicTabButtonId\": 150,\n            \"regionIds\": [11566, 11822]\n        },\n        {\n            \"songId\": 162,\n            \"songName\": \"Tribal Background\",\n            \"musicTabButtonId\": 148,\n            \"regionIds\": [11312, 11412]\n        },\n        {\n            \"songId\": 165,\n            \"songName\": \"Tribal\",\n            \"musicTabButtonId\": 149,\n            \"regionIds\": [11311]\n        },\n        {\n            \"songId\": 192,\n            \"songName\": \"Trinity\",\n            \"musicTabButtonId\": 151,\n            \"regionIds\": [10804, 10904]\n        },\n        {\n            \"songId\": 611,\n            \"songName\": \"Trouble Brewing\",\n            \"musicTabButtonId\": 435,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 183,\n            \"songName\": \"Troubled\",\n            \"musicTabButtonId\": 152,\n            \"regionIds\": [11833]\n        },\n        {\n            \"songId\": 88,\n            \"songName\": \"Twilight\",\n            \"musicTabButtonId\": 208,\n            \"regionIds\": [10906]\n        },\n        {\n            \"songId\": 473,\n            \"songName\": \"Tzhaar!\",\n            \"musicTabButtonId\": 367,\n            \"regionIds\": [9551]\n        },\n        {\n            \"songId\": 176,\n            \"songName\": \"Undercurrent\",\n            \"musicTabButtonId\": 195,\n            \"regionIds\": [12345]\n        },\n        {\n            \"songId\": 179,\n            \"songName\": \"Underground\",\n            \"musicTabButtonId\": 153,\n            \"regionIds\": [13368, 11416]\n        },\n        {\n            \"songId\": 323,\n            \"songName\": \"Underground Pass\",\n            \"musicTabButtonId\": 157,\n            \"regionIds\": [9622, 9621]\n        },\n        {\n            \"songId\": 131,\n            \"songName\": \"Understanding\",\n            \"musicTabButtonId\": 206,\n            \"regionIds\": [9547]\n        },\n        {\n            \"songId\": 3,\n            \"songName\": \"Unknown Land\",\n            \"musicTabButtonId\": 156,\n            \"regionIds\": [12338, 8524]\n        },\n        {\n            \"songId\": 70,\n            \"songName\": \"Upcoming\",\n            \"musicTabButtonId\": 158,\n            \"regionIds\": [10546]\n        },\n        {\n            \"songId\": 75,\n            \"songName\": \"Venture\",\n            \"musicTabButtonId\": 159,\n            \"regionIds\": [13364]\n        },\n        {\n            \"songId\": 45,\n            \"songName\": \"Venture2\",\n            \"musicTabButtonId\": 193,\n            \"regionIds\": [13465, 13464]\n        },\n        {\n            \"songId\": 528,\n            \"songName\": \"Victory Is Mine\",\n            \"musicTabButtonId\": 390,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 61,\n            \"songName\": \"Village\",\n            \"musicTabButtonId\": 211,\n            \"regionIds\": [13878]\n        },\n        {\n            \"songId\": 85,\n            \"songName\": \"Vision\",\n            \"musicTabButtonId\": 160,\n            \"regionIds\": [12337, 12437]\n        },\n        {\n            \"songId\": 30,\n            \"songName\": \"Voodoo Cult\",\n            \"musicTabButtonId\": 161,\n            \"regionIds\": [11665, 9545]\n        },\n        {\n            \"songId\": 32,\n            \"songName\": \"Voyage\",\n            \"musicTabButtonId\": 162,\n            \"regionIds\": [10038]\n        },\n        {\n            \"songId\": 622,\n            \"songName\": \"Waking Dream\",\n            \"musicTabButtonId\": 445,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 49,\n            \"songName\": \"Wander\",\n            \"musicTabButtonId\": 163,\n            \"regionIds\": [12083]\n        },\n        {\n            \"songId\": 295,\n            \"songName\": \"Warrior\",\n            \"musicTabButtonId\": 260,\n            \"regionIds\": [10653]\n        },\n        {\n            \"songId\": 634,\n            \"songName\": \"Warriors' Guild\",\n            \"musicTabButtonId\": 429,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 82,\n            \"songName\": \"Waterfall\",\n            \"musicTabButtonId\": 164,\n            \"regionIds\": [10037, 10137]\n        },\n        {\n            \"songId\": 244,\n            \"songName\": \"Waterlogged\",\n            \"musicTabButtonId\": 214,\n            \"regionIds\": [13877, 14133, 8014, 8270]\n        },\n        {\n            \"songId\": 626,\n            \"songName\": \"Way of The Enchanter\",\n            \"musicTabButtonId\": 449,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 394,\n            \"songName\": \"Wayward\",\n            \"musicTabButtonId\": 336,\n            \"regionIds\": [9875]\n        },\n        {\n            \"songId\": 126,\n            \"songName\": \"We Are The Fairies\",\n            \"musicTabButtonId\": 441,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 271,\n            \"songName\": \"Well of Voyage\",\n            \"musicTabButtonId\": 230,\n            \"regionIds\": [9366]\n        },\n        {\n            \"songId\": 475,\n            \"songName\": \"Wild Side\",\n            \"musicTabButtonId\": 360,\n            \"regionIds\": [12092, 12348]\n        },\n        {\n            \"songId\": 435,\n            \"songName\": \"Wilderness\",\n            \"musicTabButtonId\": 165,\n            \"regionIds\": [11832, 12346]\n        },\n        {\n            \"songId\": 42,\n            \"songName\": \"Wilderness2\",\n            \"musicTabButtonId\": 166,\n            \"regionIds\": [12091, 12347]\n        },\n        {\n            \"songId\": 43,\n            \"songName\": \"Wilderness3\",\n            \"musicTabButtonId\": 167,\n            \"regionIds\": [11834]\n        },\n        {\n            \"songId\": 8,\n            \"songName\": \"Wildwood\",\n            \"musicTabButtonId\": 250,\n            \"regionIds\": [12344]\n        },\n        {\n            \"songId\": 14,\n            \"songName\": \"Witching\",\n            \"musicTabButtonId\": 168,\n            \"regionIds\": [13114, 13370]\n        },\n        {\n            \"songId\": 529,\n            \"songName\": \"Woe of The Wyvern\",\n            \"musicTabButtonId\": 391,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 189,\n            \"songName\": \"Wolf Mountain\",\n            \"musicTabButtonId\": 191,\n            \"regionIds\": [12603, 12859]\n        },\n        {\n            \"songId\": 34,\n            \"songName\": \"Wonder\",\n            \"musicTabButtonId\": 169,\n            \"regionIds\": [11831]\n        },\n        {\n            \"songId\": 81,\n            \"songName\": \"Wonderous\",\n            \"musicTabButtonId\": 170,\n            \"regionIds\": [10548]\n        },\n        {\n            \"songId\": 255,\n            \"songName\": \"Woodland\",\n            \"musicTabButtonId\": 235,\n            \"regionIds\": [8498, 8754]\n        },\n        {\n            \"songId\": 15,\n            \"songName\": \"Workshop\",\n            \"musicTabButtonId\": 171,\n            \"regionIds\": [12084]\n        },\n        {\n            \"songId\": 565,\n            \"songName\": \"Wrath And Ruin\",\n            \"musicTabButtonId\": 414,\n            \"regionIds\": [0]\n        },\n        {\n            \"songId\": 524,\n            \"songName\": \"Xenophobe\",\n            \"musicTabButtonId\": 376,\n            \"regionIds\": [7492, 11589]\n        },\n        {\n            \"songId\": 145,\n            \"songName\": \"Yesteryear\",\n            \"musicTabButtonId\": 185,\n            \"regionIds\": [12849]\n        },\n        {\n            \"songId\": 146,\n            \"songName\": \"Zealot\",\n            \"musicTabButtonId\": 196,\n            \"regionIds\": [10827]\n        },\n        {\n            \"songId\": 392,\n            \"songName\": \"Zogre Dance\",\n            \"musicTabButtonId\": 337,\n            \"regionIds\": [9775]\n        }\n    ]\n}\n"
  },
  {
    "path": "data/config/npc-spawns/alkharid/alkharid-general.json",
    "content": "[\n    {\n        \"npc\": \"rs:alkharid_gem_trader\",\n        \"spawn_x\": 3288,\n        \"spawn_y\": 3212,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:alkharid_dommik\",\n        \"spawn_x\": 3320,\n        \"spawn_y\": 3194,\n        \"movement_radius\": 3\n    },\n    {\n        \"npc\": \"rs:alkharid_louie\",\n        \"spawn_x\": 3317,\n        \"spawn_y\": 3174,\n        \"movement_radius\": 3\n    },\n    {\n        \"npc\": \"rs:alkharid_ranael\",\n        \"spawn_x\": 3315,\n        \"spawn_y\": 3161,\n        \"movement_radius\": 3\n    },\n    {\n        \"npc\": \"rs:alkharid_karim\",\n        \"spawn_x\": 3272,\n        \"spawn_y\": 3182,\n        \"movement_radius\": 2\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/ardougne/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2657,\n        \"spawn_y\": 3286,\n        \"movement_radius\": 0\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2657,\n        \"spawn_y\": 3283,\n        \"movement_radius\": 0\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2657,\n        \"spawn_y\": 3280,\n        \"movement_radius\": 0\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2619,\n        \"spawn_y\": 3330,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2618,\n        \"spawn_y\": 3330,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2615,\n        \"spawn_y\": 3330,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/ardougne/guards.json",
    "content": "[\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2635,\n        \"spawn_y\": 3339,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2636,\n        \"spawn_y\": 3340,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2637,\n        \"spawn_y\": 3339,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2651,\n        \"spawn_y\": 3307,\n        \"movement_radius\": 10,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2659,\n        \"spawn_y\": 3309,\n        \"movement_radius\": 10,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2661,\n        \"spawn_y\": 3309,\n        \"movement_radius\": 10,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2663,\n        \"spawn_y\": 3301,\n        \"movement_radius\": 10,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2665,\n        \"spawn_y\": 3300,\n        \"movement_radius\": 10,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:2\",\n        \"spawn_x\": 2664,\n        \"spawn_y\": 3318,\n        \"movement_radius\": 10,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/ardougne/market.json",
    "content": "[\n    {\n        \"npc\": \"rs:ardougne_baker:0\",\n        \"spawn_x\": 2654,\n        \"spawn_y\": 3311,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:ardougne_baker:1\",\n        \"spawn_x\": 2669,\n        \"spawn_y\": 3310,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:knight:0\",\n        \"spawn_x\": 2671,\n        \"spawn_y\": 3313,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:knight:0\",\n        \"spawn_x\": 2652,\n        \"spawn_y\": 3318,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:knight:0\",\n        \"spawn_x\": 2669,\n        \"spawn_y\": 3298,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:knight:0\",\n        \"spawn_x\": 2653,\n        \"spawn_y\": 3300,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:hero\",\n        \"spawn_x\": 2667,\n        \"spawn_y\": 3316,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:hero\",\n        \"spawn_x\": 2647,\n        \"spawn_y\": 3306,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:hero\",\n        \"spawn_x\": 2630,\n        \"spawn_y\": 3288,\n        \"movement_radius\": 7,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:silver_merchant\",\n        \"spawn_x\": 2658,\n        \"spawn_y\": 3316,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:spice_seller\",\n        \"spawn_x\": 2658,\n        \"spawn_y\": 3296,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:fur_trader\",\n        \"spawn_x\": 2666,\n        \"spawn_y\": 3295,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:gem_merchant\",\n        \"spawn_x\": 2669,\n        \"spawn_y\": 3303,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:silk_merchant\",\n        \"spawn_x\": 2656,\n        \"spawn_y\": 3301,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/camelot/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2807,\n        \"spawn_y\": 3443,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2809,\n        \"spawn_y\": 3443,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2810,\n        \"spawn_y\": 3443,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2811,\n        \"spawn_y\": 3443,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/canifis/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3514,\n        \"spawn_y\": 3482,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3514,\n        \"spawn_y\": 3481,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3514,\n        \"spawn_y\": 3480,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3514,\n        \"spawn_y\": 3479,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/catherby/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2729,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2728,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2727,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2724,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2722,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2721,\n        \"spawn_y\": 3495,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/draynor/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3090,\n        \"spawn_y\": 3242,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3090,\n        \"spawn_y\": 3243,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3090,\n        \"spawn_y\": 3245,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/edgeville/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3096,\n        \"spawn_y\": 3491,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3096,\n        \"spawn_y\": 3489,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3096,\n        \"spawn_y\": 3492,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3098,\n        \"spawn_y\": 3492,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/falador/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2947,\n        \"spawn_y\": 3366,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2946,\n        \"spawn_y\": 3366,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2948,\n        \"spawn_y\": 3366,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2949,\n        \"spawn_y\": 3366,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2945,\n        \"spawn_y\": 3366,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3015,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3014,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3013,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3012,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3011,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3010,\n        \"spawn_y\": 3353,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/falador/guards.json",
    "content": "[\n    {\n        \"npc\": \"rs:guard:0\",\n        \"spawn_x\": 2967,\n        \"spawn_y\": 3388,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:0\",\n        \"spawn_x\": 2965,\n        \"spawn_y\": 3383,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:guard:1\",\n        \"spawn_x\": 2968,\n        \"spawn_y\": 3377,\n        \"movement_radius\": 5,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/keldagrim/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2836,\n        \"spawn_y\": 10205,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2838,\n        \"spawn_y\": 10205,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/lumbridge/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3209,\n        \"spawn_y\": 3222,\n        \"spawn_level\": 2,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3210,\n        \"spawn_y\": 3222,\n        \"spawn_level\": 2,\n        \"movement_radius\": 0,\n        \"face\": \"SOUTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/lumbridge/lumbridge-general.json",
    "content": "[\n    {\n        \"npc\": \"rs:hans\",\n        \"spawn_x\": 3222,\n        \"spawn_y\": 3222,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:man\",\n        \"spawn_x\": 3222,\n        \"spawn_y\": 3218,\n        \"movement_radius\": 12\n    },\n    {\n        \"npc\": \"rs:lumbridge_bob\",\n        \"spawn_x\": 3230,\n        \"spawn_y\": 3203,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:lumbridge_shop_keeper\",\n        \"spawn_x\": 3211,\n        \"spawn_y\": 3247,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:gillie_groats\",\n        \"spawn_x\": 3253,\n        \"spawn_y\": 3274,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:millie_miller\",\n        \"spawn_x\": 3168,\n        \"spawn_y\": 3306,\n        \"movement_radius\": 3\n    },\n    {\n        \"npc\": \"rs:lumbridge_castle_cook\",\n        \"spawn_x\": 3210,\n        \"spawn_y\": 3215,\n        \"movement_radius\": 4\n    },\n    {\n        \"npc\": \"rs:runescape_guide\",\n        \"spawn_x\": 3230,\n        \"spawn_y\": 3238,\n        \"movement_radius\": 1\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/lumbridge/lumbridge-goblins.json",
    "content": "[\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3263,\n        \"spawn_y\": 3232,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3262,\n        \"spawn_y\": 3235,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3259,\n        \"spawn_y\": 3225,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3254,\n        \"spawn_y\": 3231,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3249,\n        \"spawn_y\": 3229,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3251,\n        \"spawn_y\": 3237,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3246,\n        \"spawn_y\": 3238,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3252,\n        \"spawn_y\": 3243,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3263,\n        \"spawn_y\": 3239,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3259,\n        \"spawn_y\": 3235,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3257,\n        \"spawn_y\": 3245,\n        \"movement_radius\": 8\n    },\n    {\n        \"npc\": \"rs:goblin\",\n        \"spawn_x\": 3252,\n        \"spawn_y\": 3250,\n        \"movement_radius\": 8\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/lumbridge/lumbridge-sheep.json",
    "content": "[\n    {\n        \"npc\": \"rs:spy_penguin_sheep\",\n        \"spawn_x\": 3197,\n        \"spawn_y\": 3262,\n        \"movement_radius\": 10\n    },\n    {\n        \"npc\": \"rs:sheep\",\n        \"spawn_x\": 3199,\n        \"spawn_y\": 3267,\n        \"movement_radius\": 10\n    },\n    {\n        \"npc\": \"rs:sheep\",\n        \"spawn_x\": 3203,\n        \"spawn_y\": 3271,\n        \"movement_radius\": 10\n    },\n    {\n        \"npc\": \"rs:sheep\",\n        \"spawn_x\": 3199,\n        \"spawn_y\": 3273,\n        \"movement_radius\": 10\n    },\n    {\n        \"npc\": \"rs:sheep\",\n        \"spawn_x\": 3208,\n        \"spawn_y\": 3273,\n        \"movement_radius\": 10\n    },\n    {\n        \"npc\": \"rs:sheep\",\n        \"spawn_x\": 3209,\n        \"spawn_y\": 3260,\n        \"movement_radius\": 10\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/magebank/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2534,\n        \"spawn_y\": 4713,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/nardah/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3425,\n        \"spawn_y\": 2889,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3425,\n        \"spawn_y\": 2891,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3425,\n        \"spawn_y\": 2893,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3425,\n        \"spawn_y\": 2894,\n        \"movement_radius\": 0,\n        \"face\": \"EAST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/pestcontrol/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2665,\n        \"spawn_y\": 2651,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2666,\n        \"spawn_y\": 2651,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2667,\n        \"spawn_y\": 2651,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2668,\n        \"spawn_y\": 2651,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/portsarim/port-sarim-general.json",
    "content": "[\n    {\n        \"npc\": \"rs:betty\",\n        \"spawn_x\": 3012,\n        \"spawn_y\": 3259,\n        \"movement_radius\": 5\n    },\n    {\n        \"npc\": \"rs:Giant_rat\",\n        \"spawn_level\": 0,\n        \"spawn_x\": 2995,\n        \"spawn_y\": 3191,\n        \"movement_radius\": 6,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:Giant_rat_2\",\n        \"spawn_level\": 0,\n        \"spawn_x\": 2999,\n        \"spawn_y\": 3194,\n        \"movement_radius\": 6,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/rimmington/rimmington.json",
    "content": "[\n    {\n        \"npc\": \"rs:hetty\",\n        \"spawn_x\": 2968,\n        \"spawn_y\": 3205,\n        \"spawn_level\": 0,\n        \"movement_radius\": 4,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:rat\",\n        \"spawn_x\": 2953,\n        \"spawn_y\": 3204,\n        \"spawn_level\": 0,\n        \"movement_radius\": 4,\n        \"face\": \"EAST\"\n    },\n    {\n        \"npc\": \"rs:rat\",\n        \"spawn_x\": 2955,\n        \"spawn_y\": 3202,\n        \"spawn_level\": 0,\n        \"movement_radius\": 4,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:rat\",\n        \"spawn_x\": 2959,\n        \"spawn_y\": 3204,\n        \"spawn_level\": 0,\n        \"movement_radius\": 4,\n        \"face\": \"SOUTH\"\n    },\n    {\n        \"npc\": \"rs:rat\",\n        \"spawn_x\": 2958,\n        \"spawn_y\": 3202,\n        \"spawn_level\": 0,\n        \"movement_radius\": 4,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/shilovillage/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2852,\n        \"spawn_y\": 2955,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/varrock/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3252,\n        \"spawn_y\": 3418,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3253,\n        \"spawn_y\": 3418,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3254,\n        \"spawn_y\": 3418,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3256,\n        \"spawn_y\": 3418,\n        \"movement_radius\": 0,\n        \"face\": \"NORTH\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3436,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3438,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3440,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3442,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3444,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3446,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/varrock/blue-moon-inn.json",
    "content": "[\n    {\n        \"npc\": \"rs:barbarian_woman\",\n        \"spawn_x\": 3224,\n        \"spawn_y\": 3402,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:blue_moon_inn_bartender\",\n        \"spawn_x\": 3226,\n        \"spawn_y\": 3399,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:blue_moon_inn_charlie\",\n        \"spawn_x\": 3230,\n        \"spawn_y\": 3402,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:jonny_the_beard\",\n        \"spawn_x\": 3225,\n        \"spawn_y\": 3395,\n        \"movement_radius\": 5\n    },\n    {\n        \"npc\": \"rs:dr_harlow\",\n        \"spawn_x\": 3222,\n        \"spawn_y\": 3397,\n        \"movement_radius\": 3\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/varrock/varrock-general.json",
    "content": "[\n    {\n        \"npc\": \"rs:varrock_zaff\",\n        \"spawn_x\": 3203,\n        \"spawn_y\": 3432,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:varrock_horvic\",\n        \"spawn_x\": 3229,\n        \"spawn_y\": 3437,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:varrock_wilough\",\n        \"spawn_x\": 3221,\n        \"spawn_y\": 3437,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:varrock_shilop\",\n        \"spawn_x\": 3220,\n        \"spawn_y\": 3432,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:varrock_baraek\",\n        \"spawn_x\": 3220,\n        \"spawn_y\": 3433,\n        \"movement_radius\": 1\n    },\n    {\n        \"npc\": \"rs:romeo\",\n        \"spawn_x\": 3211,\n        \"spawn_y\": 3424,\n        \"movement_radius\": 4\n    },\n    {\n        \"npc\": \"rs:varrock_shop_keeper\",\n        \"spawn_x\": 3214,\n        \"spawn_y\": 3417,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:varrock_shop_assistant\",\n        \"spawn_x\": 3214,\n        \"spawn_y\": 3417,\n        \"movement_radius\": 2\n    },\n    {\n        \"npc\": \"rs:master_smithing_tutor\",\n        \"spawn_x\": 3187,\n        \"spawn_y\": 3423,\n        \"movement_radius\": 3\n    }\n]\n"
  },
  {
    "path": "data/config/npc-spawns/yanille/bankers.json",
    "content": "[\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2615,\n        \"spawn_y\": 3091,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2615,\n        \"spawn_y\": 3092,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    },\n    {\n        \"npc\": \"rs:banker\",\n        \"spawn_x\": 2615,\n        \"spawn_y\": 3094,\n        \"movement_radius\": 0,\n        \"face\": \"WEST\"\n    }\n]\n"
  },
  {
    "path": "data/config/npcs/alkharid.json",
    "content": "{\n    \"rs:alkharid_gem_trader\": {\n        \"game_id\": 540\n    },\n    \"rs:alkharid_dommik\": {\n        \"game_id\": 545\n    },\n    \"rs:alkharid_louie\": {\n        \"game_id\": 542\n    },\n    \"rs:alkharid_ranael\": {\n        \"game_id\": 544\n    },\n    \"rs:alkharid_karim\": {\n        \"game_id\": 543\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/ardougne.json",
    "content": "{\n    \"rs:ardougne_baker\": {\n        \"variations\": [\n            {\n                \"suffix\": 0,\n                \"game_id\": 571\n            },\n            {\n                \"suffix\": 1,\n                \"game_id\": 571\n            }\n        ]\n    },\n    \"rs:knight\": {\n        \"variations\": [\n            {\n                \"suffix\": 0,\n                \"game_id\": 23\n            },\n            {\n                \"suffix\": 1,\n                \"game_id\": 26\n            }\n        ]\n    },\n    \"rs:hero\": {\n        \"game_id\": 21\n    },\n    \"rs:silk_merchant\": {\n        \"game_id\": 574\n    },\n    \"rs:fur_trader\": {\n        \"game_id\": 573\n    },\n    \"rs:spice_seller\": {\n        \"game_id\": 572\n    },\n    \"rs:gem_merchant\": {\n        \"game_id\": 570\n    },\n    \"rs:silver_merchant\": {\n        \"game_id\": 569\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/bankers.json",
    "content": "{\n    \"rs:banker\": {\n        \"game_id\": 494\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/barbarians.json",
    "content": "{\n    \"rs:barbarian_woman\": {\n        \"game_id\": 17\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/general.json",
    "content": "{\n    \"rs:rat\": {\n        \"game_id\": 47,\n        \"drop_table\": [\n            {\n                \"itemKey\": \"rs:rats_tail\",\n                \"frequency\": \"always\",\n                \"amount\": 1,\n                \"amountMax\": 1,\n                \"questRequirement\": {\n                    \"questId\": \"rs:witchs_potion\",\n                    \"stage\": 50\n                }\n            },\n            { \"itemKey\": \"rs:bones\", \"frequency\": \"always\", \"amount\": 1 }\n        ],\n        \"skills\": {\n            \"hitpoints\": 1\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/generic-humans.json",
    "content": "{\n    \"presets\": {\n        \"rs:human_male\": {\n            \"killable\": true,\n            \"respawn_time\": 3,\n            \"skills\": {\n                \"hitpoints\": 7\n            },\n            \"offensive_stats\": {\n                \"speed\": 4\n            },\n            \"defensive_stats\": {\n                \"stab\": -21,\n                \"slash\": -21,\n                \"crush\": -21,\n                \"magic\": -21,\n                \"ranged\": -21\n            },\n            \"animations\": {\n                \"attack\": 422,\n                \"defend\": 424,\n                \"death\": 512\n            },\n            \"drop_table\": [\n                { \"itemKey\": \"rs:bones\", \"frequency\": \"always\", \"amount\": 1 },\n                { \"itemKey\": \"rs:bronze_dagger\", \"frequency\": \"1/128\", \"amount\": 1 }\n            ]\n        }\n    },\n\n    \"rs:man\": {\n        \"extends\": \"rs:human_male\",\n        \"game_id\": 1,\n        \"variations\": [\n            {\n                \"suffix\": \"0\",\n                \"game_id\": 2\n            },\n            {\n                \"suffix\": \"1\",\n                \"game_id\": 3\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/goblins.json",
    "content": "{\n    \"presets\": {\n        \"rs:goblin_base_l2\": {\n            \"killable\": true,\n            \"respawn_time\": 3,\n            \"skills\": {\n                \"hitpoints\": 5\n            },\n            \"offensive_stats\": {\n                \"speed\": 4,\n                \"attack\": -21,\n                \"strength\": -15,\n                \"magic\": 0,\n                \"magic_strength\": 0,\n                \"ranged\": 0,\n                \"ranged_strength\": 0\n            },\n            \"defensive_stats\": {\n                \"stab\": -15,\n                \"slash\": -15,\n                \"crush\": -15,\n                \"magic\": -15,\n                \"ranged\": -15\n            },\n            \"animations\": {\n                \"attack\": [310, 309],\n                \"defend\": 312,\n                \"death\": 313\n            },\n            \"drop_table\": [{ \"itemKey\": \"rs:bones\", \"frequency\": \"always\", \"amount\": 1 }]\n        }\n    },\n\n    \"rs:goblin\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 100,\n        \"metadata\": {\n            \"level\": 2\n        }\n    },\n    \"rs:goblin_with_spear\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 101,\n        \"metadata\": {\n            \"level\": 5\n        }\n    },\n    \"rs:goblin_with_helmet\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 102,\n        \"metadata\": {\n            \"level\": 13\n        }\n    },\n    \"rs:goblin_green_with_sprear\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 298,\n        \"metadata\": {\n            \"level\": 5\n        }\n    },\n    \"rs:goblin_red_with_sprear\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 299,\n        \"metadata\": {\n            \"level\": 5\n        }\n    },\n    \"rs:goblin_red_with_shield\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 444,\n        \"metadata\": {\n            \"level\": 5\n        }\n    },\n    \"rs:goblin_green_with_shield\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 445,\n        \"metadata\": {\n            \"level\": 5\n        }\n    },\n    \"rs:goblin_guard\": {\n        \"extends\": \"rs:goblin_base_l2\",\n        \"game_id\": 489,\n        \"metadata\": {\n            \"level\": 42\n        }\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/guards.json",
    "content": "{\n    \"rs:guard\": {\n        \"variations\": [\n            {\n                \"suffix\": 0,\n                \"game_id\": 9\n            },\n            {\n                \"suffix\": 1,\n                \"game_id\": 10\n            },\n            {\n                \"suffix\": 2,\n                \"game_id\": 32\n            },\n            {\n                \"suffix\": 3,\n                \"game_id\": 206\n            },\n            {\n                \"suffix\": 4,\n                \"game_id\": 344\n            },\n            {\n                \"suffix\": 5,\n                \"game_id\": 345\n            },\n            {\n                \"suffix\": 6,\n                \"game_id\": 346\n            },\n            {\n                \"suffix\": 7,\n                \"game_id\": 368\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/lumbridge.json",
    "content": "{\n    \"rs:hans\": {\n        \"game_id\": 0,\n        \"skills\": {\n            \"hitpoints\": 10,\n            \"herblore\": 99\n        }\n    },\n    \"rs:runescape_guide\": {\n        \"game_id\": 945\n    },\n    \"rs:melee_combat_tutor\": {\n        \"game_id\": 705\n    },\n    \"rs:lumbridge_shop_keeper\": {\n        \"game_id\": 520\n    },\n    \"rs:lumbridge_bob\": {\n        \"game_id\": 519\n    },\n    \"rs:gillie_groats\": {\n        \"game_id\": 3807\n    },\n    \"rs:millie_miller\": {\n        \"game_id\": 3806\n    },\n    \"rs:lumbridge_castle_cook\": {\n        \"game_id\": 278\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/port-sarim.json",
    "content": "{\n    \"rs:betty\": {\n        \"game_id\": 583\n    },\n    \"rs:Giant_rat\": {\n        \"game_id\": 86,\n        \"drop_table\": [\n            { \"itemKey\": \"rs:bones\", \"frequency\": \"always\", \"amount\": 1 },\n            { \"itemKey\": \"rs:raw_rat_meat\", \"frequency\": \"always\", \"amount\": 1 }\n        ]\n    },\n    \"rs:Giant_rat_2\": {\n        \"game_id\": 4924,\n        \"drop_table\": [\n            { \"itemKey\": \"rs:bones\", \"frequency\": \"always\", \"amount\": 1 },\n            { \"itemKey\": \"rs:raw_rat_meat\", \"frequency\": \"always\", \"amount\": 1 }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/rimmington.json",
    "content": "{\n    \"rs:hetty\": {\n        \"game_id\": 307\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/sheep.json",
    "content": "{\n    \"rs:spy_penguin_sheep\": {\n        \"game_id\": 3579\n    },\n    \"rs:sheep\": {\n        \"game_id\": 43\n    },\n    \"rs:naked_sheep\": {\n        \"game_id\": 42\n    }\n}\n"
  },
  {
    "path": "data/config/npcs/varrock.json",
    "content": "{\n    \"rs:varrock_zaff\": {\n        \"game_id\": 546\n    },\n    \"rs:blue_moon_inn_bartender\": {\n        \"game_id\": 734\n    },\n    \"rs:blue_moon_inn_charlie\": {\n        \"game_id\": 794\n    },\n    \"rs:jonny_the_beard\": {\n        \"game_id\": 645\n    },\n    \"rs:dr_harlow\": {\n        \"game_id\": 756\n    },\n    \"rs:varrock_horvic\": {\n        \"game_id\": 549\n    },\n    \"rs:varrock_wilough\": {\n        \"game_id\": 783\n    },\n    \"rs:varrock_shilop\": {\n        \"game_id\": 781\n    },\n    \"rs:varrock_baraek\": {\n        \"game_id\": 547\n    },\n    \"rs:romeo\": {\n        \"game_id\": 639\n    },\n    \"rs:varrock_shop_keeper\": {\n        \"game_id\": 520\n    },\n    \"rs:varrock_shop_assistant\": {\n        \"game_id\": 521\n    },\n    \"rs:master_smithing_tutor\": {\n        \"game_id\": 4905\n    }\n}\n"
  },
  {
    "path": "data/config/scenery-spawns.yaml",
    "content": ""
  },
  {
    "path": "data/config/shops/alkharid/alkharid-gem-trader.json",
    "content": "{\n    \"rs:alkharid_gem_trader\": {\n        \"name\": \"Gem Trader\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.7,\n        \"rate_modifier\": 0.03,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:uncut_sapphire\",\n                \"amount\": 1,\n                \"restock\": 25000\n            },\n            {\n                \"itemKey\": \"rs:uncut_emerald\",\n                \"amount\": 1,\n                \"restock\": 40000\n            },\n            {\n                \"itemKey\": \"rs:uncut_ruby\",\n                \"amount\": 0,\n                \"restock\": 2000\n            },\n            {\n                \"itemKey\": \"rs:uncut_diamond\",\n                \"amount\": 0,\n                \"restock\": 4000\n            },\n            {\n                \"itemKey\": \"rs:sapphire\",\n                \"amount\": 1,\n                \"restock\": 15000\n            },\n            {\n                \"itemKey\": \"rs:emerald\",\n                \"amount\": 1,\n                \"restock\": 35000\n            },\n            {\n                \"itemKey\": \"rs:ruby\",\n                \"amount\": 0,\n                \"restock\": 2000\n            },\n            {\n                \"itemKey\": \"rs:diamond\",\n                \"amount\": 0,\n                \"restock\": 4000\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/alkharid/dommiks-crafting-store.json",
    "content": "{\n    \"rs:dommiks_crafting_store\": {\n        \"name\": \"Dommik's Crafting Store\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.65,\n        \"rate_modifier\": 0.02,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:chisel\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:ring_mould\",\n                \"amount\": 4,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:necklace_mould\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:amulet_mould\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:needle\",\n                \"amount\": 3,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:thread\",\n                \"amount\": 100,\n                \"restock\": 5\n            },\n            {\n                \"itemKey\": \"rs:holy_mould\",\n                \"amount\": 3,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:sickle_mould\",\n                \"amount\": 6,\n                \"restock\": 15\n            },\n            {\n                \"itemKey\": \"rs:tiara_mould\",\n                \"amount\": 10,\n                \"restock\": 10\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/alkharid/louies-armored-legs.json",
    "content": "{\n    \"rs:louies_armored_legs\": {\n        \"name\": \"Louie's Armoured Legs Bazaar\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.65,\n        \"rate_modifier\": 0.01,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:bronze_platelegs\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:iron_platelegs\",\n                \"amount\": 3,\n                \"restock\": 400\n            },\n            {\n                \"itemKey\": \"rs:steel_platelegs\",\n                \"amount\": 2,\n                \"restock\": 900\n            },\n            {\n                \"itemKey\": \"rs:black_platelegs\",\n                \"amount\": 1,\n                \"restock\": 1200\n            },\n            {\n                \"itemKey\": \"rs:mithril_platelegs\",\n                \"amount\": 1,\n                \"restock\": 2000\n            },\n            {\n                \"itemKey\": \"rs:adamant_platelegs\",\n                \"amount\": 1,\n                \"restock\": 13000\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/alkharid/ranaels-skirt-store.json",
    "content": "{\n    \"rs:ranaels_skirt_store\": {\n        \"name\": \"Ranael's Super Skirt Store\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.65,\n        \"rate_modifier\": 0.01,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:bronze_plateskirt\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:iron_plateskirt\",\n                \"amount\": 3,\n                \"restock\": 400\n            },\n            {\n                \"itemKey\": \"rs:steel_plateskirt\",\n                \"amount\": 2,\n                \"restock\": 900\n            },\n            {\n                \"itemKey\": \"rs:black_plateskirt\",\n                \"amount\": 1,\n                \"restock\": 1200\n            },\n            {\n                \"itemKey\": \"rs:mithril_plateskirt\",\n                \"amount\": 1,\n                \"restock\": 2000\n            },\n            {\n                \"itemKey\": \"rs:adamant_plateskirt\",\n                \"amount\": 1,\n                \"restock\": 13000\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/lumbridge/bobs-axes.json",
    "content": "{\n    \"rs:lumbridge_bobs_axes\": {\n        \"name\": \"Bob's Brilliant Axes.\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.6,\n        \"rate_modifier\": 0.02,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:bronze_pickaxe\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:bronze_axe\",\n                \"amount\": 10,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:iron_axe\",\n                \"amount\": 5,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:steel_axe\",\n                \"amount\": 3,\n                \"restock\": 400\n            },\n            {\n                \"itemKey\": \"rs:iron_battleaxe\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:steel_battleaxe\",\n                \"amount\": 2,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:mithril_battleaxe\",\n                \"amount\": 1,\n                \"restock\": 3000\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/lumbridge/lumbridge-general-store.json",
    "content": "{\n    \"rs:lumbridge_general_store\": {\n        \"name\": \"Lumbridge General Store\",\n        \"general_store\": true,\n        \"shop_sell_rate\": 1.3,\n        \"shop_buy_rate\": 0.4,\n        \"rate_modifier\": 0.03,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:pot\",\n                \"amount\": 5,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:jug\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:shears\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:knife\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:bucket\",\n                \"amount\": 3,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:bowl\",\n                \"amount\": 2,\n                \"restock\": 50\n            },\n            {\n                \"itemKey\": \"rs:cake_tin\",\n                \"amount\": 2,\n                \"restock\": 50\n            },\n            {\n                \"itemKey\": \"rs:tinderbox\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:chisel\",\n                \"amount\": 2,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:spade\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:hammer\",\n                \"amount\": 5,\n                \"restock\": 100\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/portsarim/bettys-magic-emporium.json",
    "content": "{\n    \"rs:bettys_magic_emporium\": {\n        \"name\": \"Betty's Magic Emporium\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.6,\n        \"rate_modifier\": 0.001,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:fire_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:water_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:air_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:earth_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:mind_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:body_rune\",\n                \"amount\": 5000,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:chaos_rune\",\n                \"amount\": 250,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:death_rune\",\n                \"amount\": 250,\n                \"restock\": 15\n            },\n            {\n                \"itemKey\": \"rs:eye_of_newt\",\n                \"amount\": 300,\n                \"restock\": 10\n            },\n            {\n                \"itemKey\": \"rs:wizard_hat_(black)\",\n                \"amount\": 1,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:wizard_hat_(blue)\",\n                \"amount\": 1,\n                \"restock\": 100\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/shilo-village/oblis-general-store.json",
    "content": "{\n    \"rs:oblis-general-store\": {\n        \"name\": \"Obli's General Store\",\n        \"general_store\": true,\n        \"shop_sell_rate\": 1.5,\n        \"rate_modifier\": 0.02,\n        \"shop_buy_rate\": 0.5,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:tinderbox\",\n                \"amount\": 2,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:vial\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:pestle_and_mortar\",\n                \"amount\": 3,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:pot\",\n                \"amount\": 3,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:bronze_axe\",\n                \"amount\": 3,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:bronze_pickaxe\",\n                \"amount\": 2,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:iron_axe\",\n                \"amount\": 5,\n                \"restock\": 700\n            },\n            {\n                \"itemKey\": \"rs:leather_body\",\n                \"amount\": 12,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:leather_gloves\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:leather_boots\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:cooked_meat\",\n                \"amount\": 2,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:bread\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:bronze_bar\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:spade\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:candle\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:unlit_torch\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:chisel\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:hammer\",\n                \"amount\": 10,\n                \"restock\": 500\n            },\n            {\n                \"itemKey\": \"rs:papyrus\",\n                \"amount\": 50,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:charcoal\",\n                \"amount\": 50,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:vial:water\",\n                \"amount\": 50,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:machete\",\n                \"amount\": 50,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:rope\",\n                \"amount\": 10,\n                \"restock\": 200\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/shops/varrock/zaffs-staffs.json",
    "content": "{\n    \"rs:zaffs_superior_staffs\": {\n        \"name\": \"Zaff's Superior Staffs\",\n        \"shop_sell_rate\": 1.0,\n        \"shop_buy_rate\": 0.55,\n        \"rate_modifier\": 0.02,\n        \"stock\": [\n            {\n                \"itemKey\": \"rs:battlestaff\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:staff\",\n                \"amount\": 5,\n                \"restock\": 100\n            },\n            {\n                \"itemKey\": \"rs:magic_staff\",\n                \"amount\": 5,\n                \"restock\": 200\n            },\n            {\n                \"itemKey\": \"rs:staff_of_air\",\n                \"amount\": 2,\n                \"restock\": 1000\n            },\n            {\n                \"itemKey\": \"rs:staff_of_water\",\n                \"amount\": 2,\n                \"restock\": 1000\n            },\n            {\n                \"itemKey\": \"rs:staff_of_earth\",\n                \"amount\": 2,\n                \"restock\": 1000\n            },\n            {\n                \"itemKey\": \"rs:staff_of_fire\",\n                \"amount\": 2,\n                \"restock\": 1000\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "data/config/travel-locations-data.yaml",
    "content": "- name: Abandoned Mine\n  x: 3441\n  y: 3236\n  z: 0\n- name: Agility Arena\n  x: 2809\n  y: 3191\n  z: 0\n- name: Agility Pyramid\n  x: 3364\n  y: 2840\n  z: 0\n- name: Agility Training Area 1\n  x: 2481\n  y: 3424\n  z: 0\n- name: Agility Training Area 2\n  x: 2533\n  y: 3538\n  z: 0\n- name: Agility Training Area 3\n  x: 2998\n  y: 3952\n  z: 0\n- name: Al Kharid\n  x: 3293\n  y: 3151\n  z: 0\n- name: Ape Atoll\n  x: 2747\n  y: 2751\n  z: 0\n- name: Arandar\n  x: 2342\n  y: 3294\n  z: 0\n- name: Ardougne Sewers\n  x: 2567\n  y: 9682\n  z: 0\n- name: Ardougne Sewers Mine\n  x: 2655\n  y: 9677\n  z: 0\n- name: Ardougne Zoo\n  x: 2612\n  y: 3275\n  z: 0\n- name: Asgarnian Ice Dungeon\n  x: 3038\n  y: 9580\n  z: 0\n- name: Ah Za Rhoon\n  x: 2908\n  y: 9336\n  z: 0\n- name: Bandit Camp\n  x: 3037\n  y: 3699\n  z: 0\n- name: Bandit Camp\n  x: 3171\n  y: 2979\n  z: 0\n- name: Barbarian Outpost\n  x: 2552\n  y: 3561\n  z: 0\n- name: Barbarian Village\n  x: 3080\n  y: 3419\n  z: 0\n- name: Barrows\n  x: 3564\n  y: 3288\n  z: 0\n- name: Barrows Crypt\n  x: 3551\n  y: 9695\n  z: 0\n- name: Battlefield\n  x: 2520\n  y: 3232\n  z: 0\n- name: Baxtorian Falls\n  x: 2513\n  y: 3461\n  z: 0\n- name: Bear\n  x: 3285\n  y: 3838\n  z: 0\n- name: Bedabin Camp\n  x: 3169\n  y: 3036\n  z: 0\n- name: Beehives\n  x: 2759\n  y: 3442\n  z: 0\n- name: Black Knights' Fortress\n  x: 3025\n  y: 3514\n  z: 0\n- name: Bone Yard\n  x: 3236\n  y: 3746\n  z: 0\n- name: Brimhaven\n  x: 2773\n  y: 3176\n  z: 0\n- name: Brimhaven Dungeon\n  x: 2668\n  y: 9520\n  z: 0\n- name: Burgh de Rott\n  x: 3495\n  y: 3218\n  z: 0\n- name: Burthorpe\n  x: 2893\n  y: 3541\n  z: 0\n- name: Cairn Isle\n  x: 2765\n  y: 2976\n  z: 0\n- name: Cairn Island Dungeon\n  x: 2764\n  y: 9376\n  z: 0\n- name: Camelot Castle\n  x: 2758\n  y: 3507\n  z: 0\n- name: Canifis\n  x: 3495\n  y: 3487\n  z: 0\n- name: Castle Wars\n  x: 2430\n  y: 3104\n  z: 0\n- name: Catherby\n  x: 2821\n  y: 3433\n  z: 0\n- name: Champions' Guild\n  x: 3191\n  y: 3360\n  z: 0\n- name: Chaos Druid Tower Dungeon\n  x: 2580\n  y: 9743\n  z: 0\n- name: Chaos Temple\n  x: 2933\n  y: 3514\n  z: 0\n- name: Chaos Temple\n  x: 3240\n  y: 3608\n  z: 0\n- name: Clan Wars\n  x: 3371\n  y: 3162\n  z: 0\n- name: Clan Wars\n  x: 3422\n  y: 4735\n  z: 0\n- name: Clocktower\n  x: 2571\n  y: 3240\n  z: 0\n- name: Clocktower Dungeon\n  x: 2590\n  y: 9630\n  z: 0\n- name: Coal Trucks\n  x: 2598\n  y: 3489\n  z: 0\n- name: Combat Training Camp\n  x: 2515\n  y: 3369\n  z: 0\n- name: Cooks' Guild\n  x: 3143\n  y: 3447\n  z: 0\n- name: Cosmic entity's plane\n  x: 2079\n  y: 4828\n  z: 0\n- name: Crafting Guild\n  x: 2926\n  y: 3281\n  z: 0\n- name: Crandor\n  x: 2836\n  y: 3271\n  z: 0\n- name: Crandor Dungeon\n  x: 2849\n  y: 9636\n  z: 0\n- name: Crash Island\n  x: 2914\n  y: 2720\n  z: 0\n- name: Dark Warriors' Fortress\n  x: 3029\n  y: 3630\n  z: 0\n- name: Dark Wizards' Tower\n  x: 2908\n  y: 3334\n  z: 0\n- name: Death Plateau\n  x: 2863\n  y: 3590\n  z: 0\n- name: Deep Wilderness Dungeon\n  x: 3040\n  y: 10336\n  z: 0\n- name: Demonic Ruins\n  x: 3289\n  y: 3885\n  z: 0\n- name: Desert Mining Camp\n  x: 3288\n  y: 3021\n  z: 0\n- name: Deserted Keep\n  x: 3153\n  y: 3931\n  z: 0\n- name: Digsite\n  x: 3362\n  y: 3417\n  z: 0\n- name: Dragontooth Island\n  x: 3806\n  y: 3554\n  z: 0\n- name: Draynor Manor\n  x: 3104\n  y: 3341\n  z: 0\n- name: Draynor Sewers\n  x: 3107\n  y: 9672\n  z: 0\n- name: Draynor Village\n  x: 3105\n  y: 3258\n  z: 0\n- name: Druids' Circle\n  x: 2925\n  y: 3482\n  z: 0\n- name: Duel Arena\n  x: 3361\n  y: 3232\n  z: 0\n- name: Dwarven Mine\n  x: 3015\n  y: 3445\n  z: 0\n- name: Dwarven Mine Dungeon\n  x: 3024\n  y: 9791\n  z: 0\n- name: Eagles' Peak\n  x: 2332\n  y: 3486\n  z: 0\n- name: East Ardougne\n  x: 2598\n  y: 3295\n  z: 0\n- name: Ectofuntus\n  x: 3659\n  y: 3519\n  z: 0\n- name: Edgeville Dungeon\n  x: 3114\n  y: 9917\n  z: 0\n- name: Edgeville\n  x: 3086\n  y: 3497\n  z: 0\n- name: Elemental Workshop\n  x: 1963\n  y: 5149\n  z: 0\n- name: Elf Camp\n  x: 2196\n  y: 3251\n  z: 0\n- name: Enakhra's Temple Bottom Floor\n  x: 3104\n  y: 9312\n  z: 0\n- name: Entrana\n  x: 2843\n  y: 3378\n  z: 0\n- name: Etceteria\n  x: 2609\n  y: 3874\n  z: 0\n- name: Exam Centre\n  x: 3363\n  y: 3339\n  z: 0\n- name: Falador\n  x: 3004\n  y: 3361\n  z: 0\n- name: Falador Mole Lair\n  x: 1760\n  y: 5190\n  z: 0\n- name: Falconer\n  x: 2374\n  y: 3604\n  z: 0\n- name: Feldip Hills\n  x: 2556\n  y: 2982\n  z: 0\n- name: Ferox Enclave\n  x: 3141\n  y: 3629\n  z: 0\n- name: Fenkenstrain's Castle\n  x: 3548\n  y: 3554\n  z: 0\n- name: Fenkenstrain's Dungeon\n  x: 3519\n  y: 9952\n  z: 0\n- name: Fight Arena\n  x: 2592\n  y: 3161\n  z: 0\n- name: Fishing Guild\n  x: 2604\n  y: 3400\n  z: 0\n- name: Fishing Platform\n  x: 2772\n  y: 3283\n  z: 0\n- name: Flax\n  x: 2744\n  y: 3443\n  z: 0\n- name: Forinthry Dungeon\n  x: 3211\n  y: 10150\n  z: 0\n- name: Fountain of Rune\n  x: 3378\n  y: 3891\n  z: 0\n- name: Fremennik Province\n  x: 2666\n  y: 3632\n  z: 0\n- name: Fremennik Slayer Dungeon\n  x: 2805\n  y: 10001\n  z: 0\n- name: Frozen Waste Plateau\n  x: 2962\n  y: 3917\n  z: 0\n- name: 2006 Easter Event\n  x: 2464\n  y: 5280\n  z: 0\n- name: Glarial's Tomb\n  x: 2543\n  y: 9827\n  z: 0\n- name: Gnome Ball Field\n  x: 2395\n  y: 3486\n  z: 0\n- name: Goblin Cave\n  x: 2587\n  y: 9830\n  z: 0\n- name: Goblin Village\n  x: 2956\n  y: 3505\n  z: 0\n- name: Golden Apple Tree\n  x: 2766\n  y: 3607\n  z: 0\n- name: Grand Tree\n  x: 2464\n  y: 3501\n  z: 0\n- name: Grand Tree Tunnels\n  x: 2463\n  y: 9887\n  z: 0\n- name: Graveyard of Shadows\n  x: 3164\n  y: 3672\n  z: 0\n- name: Graveyard\n  x: 3569\n  y: 3404\n  z: 0\n- name: Gu'Tanoth\n  x: 2521\n  y: 3043\n  z: 0\n- name: Haunted Woods\n  x: 3564\n  y: 3490\n  z: 0\n- name: Hemenster\n  x: 2634\n  y: 3437\n  z: 0\n- name: Heroes' Guild\n  x: 2896\n  y: 3510\n  z: 0\n- name: Iban's Lair\n  x: 2335\n  y: 9855\n  z: 0\n- name: Ice Mountain\n  x: 3007\n  y: 3481\n  z: 0\n- name: Ice path\n  x: 2854\n  y: 3808\n  z: 0\n- name: Ice Queen's lair\n  x: 2865\n  y: 9954\n  z: 0\n- name: Isafdar\n  x: 2244\n  y: 3180\n  z: 0\n- name: Jail\n  x: 3125\n  y: 3242\n  z: 0\n- name: Jiggig\n  x: 2465\n  y: 3045\n  z: 0\n- name: Lower Jiggig Dungeon\n  x: 2465\n  y: 9441\n  z: 0\n- name: Middle Jiggig Dungeon\n  x: 2465\n  y: 9441\n  z: 2\n- name: Kalphite Lair\n  x: 3226\n  y: 3106\n  z: 0\n- name: Karamja\n  x: 2859\n  y: 3043\n  z: 0\n- name: Karamja Dungeon\n  x: 2840\n  y: 9571\n  z: 0\n- name: Keep Le Faye\n  x: 2769\n  y: 3399\n  z: 0\n- name: Keldagrim Entrance\n  x: 2725\n  y: 3712\n  z: 0\n- name: Keldagrim\n  x: 2855\n  y: 10175\n  z: 0\n- name: Kharazi Jungle\n  x: 2833\n  y: 2922\n  z: 0\n- name: Kharidian Desert\n  x: 3264\n  y: 2960\n  z: 0\n- name: Kingdom of Asgarnia\n  x: 2991\n  y: 3405\n  z: 0\n- name: Kingdom of Kandarin\n  x: 2572\n  y: 3445\n  z: 0\n- name: Kingdom of Misthalin\n  x: 3215\n  y: 3318\n  z: 0\n- name: Lava Dragon Isle\n  x: 3197\n  y: 3825\n  z: 0\n- name: Lava Maze\n  x: 3075\n  y: 3845\n  z: 0\n- name: Lava Maze Dungeon\n  x: 3040\n  y: 10272\n  z: 0\n- name: Legends' Guild\n  x: 2730\n  y: 3377\n  z: 0\n- name: Legends' Guild Dungeon\n  x: 2720\n  y: 9754\n  z: 0\n- name: Lighthouse\n  x: 2510\n  y: 3626\n  z: 0\n- name: Lighthouse Dungeon\n  x: 2518\n  y: 10021\n  z: 0\n- name: Lizards\n  x: 3421\n  y: 3041\n  z: 0\n- name: Lletya\n  x: 2346\n  y: 3177\n  z: 0\n- name: Lumber Yard\n  x: 3305\n  y: 3505\n  z: 0\n- name: Lumbridge Swamp Caves\n  x: 3169\n  y: 9571\n  z: 0\n- name: Lumbridge Swamp\n  x: 3184\n  y: 3179\n  z: 0\n- name: Lumbridge\n  x: 3224\n  y: 3218\n  z: 0\n- name: Mage Arena\n  x: 3105\n  y: 3932\n  z: 0\n- name: Mage Training Arena Rooms\n  x: 3357\n  y: 9666\n  z: 0\n- name: Mage Training Arena\n  x: 3363\n  y: 3304\n  z: 0\n- name: Marim\n  x: 2760\n  y: 2783\n  z: 0\n- name: Market\n  x: 3082\n  y: 3246\n  z: 0\n- name: Mausoleum\n  x: 3503\n  y: 3572\n  z: 0\n- name: McGrubor's Wood\n  x: 2641\n  y: 3480\n  z: 0\n- name: Melzar's Maze\n  x: 2933\n  y: 3248\n  z: 0\n- name: Menaphos\n  x: 3233\n  y: 2780\n  z: 0\n- name: Miscellania\n  x: 2537\n  y: 3875\n  z: 0\n- name: Miscellania dungeon\n  x: 2558\n  y: 10276\n  z: 0\n- name: Mogre Camp\n  x: 2974\n  y: 9496\n  z: 1\n- name: Monastery 1\n  x: 2602\n  y: 3215\n  z: 0\n- name: Monastery 2\n  x: 3052\n  y: 3487\n  z: 0\n- name: Mort Myre Swamp\n  x: 3440\n  y: 3380\n  z: 0\n- name: Mort'ton\n  x: 3487\n  y: 3283\n  z: 0\n- name: Morytania\n  x: 3467\n  y: 3441\n  z: 0\n- name: Mor Ul Rek\n  x: 2494\n  y: 5124\n  z: 0\n- name: Mountain Camp\n  x: 2801\n  y: 3670\n  z: 0\n- name: Mouse Hole\n  x: 2280\n  y: 5535\n  z: 0\n- name: Mudskipper Point\n  x: 2992\n  y: 3116\n  z: 0\n- name: Musa Point\n  x: 2897\n  y: 3161\n  z: 0\n- name: Nardah\n  x: 3427\n  y: 2903\n  z: 0\n- name: Necromancer\n  x: 2669\n  y: 3241\n  z: 0\n- name: Nightmare Zone\n  x: 2603\n  y: 3115\n  z: 0\n- name: Observatory\n  x: 2441\n  y: 3157\n  z: 0\n- name: Observatory Dungeon\n  x: 2398\n  y: 9441\n  z: 0\n- name: Ogre Enclave\n  x: 2592\n  y: 9444\n  z: 0\n- name: Otto's Grotto\n  x: 2502\n  y: 3488\n  z: 0\n- name: Outpost\n  x: 2441\n  y: 3345\n  z: 0\n- name: Palace\n  x: 3212\n  y: 3479\n  z: 0\n- name: Pest Control\n  x: 2656\n  y: 2593\n  z: 0\n- name: Pirates' Hideout\n  x: 3041\n  y: 3950\n  z: 0\n- name: Piscatoris Fishing Colony\n  x: 2343\n  y: 3690\n  z: 0\n- name: Poision Waste\n  x: 2232\n  y: 3096\n  z: 0\n- name: Pollnivneach\n  x: 3352\n  y: 2977\n  z: 0\n- name: Port Khazard\n  x: 2655\n  y: 3185\n  z: 0\n- name: Port Phasmatys\n  x: 3674\n  y: 3486\n  z: 0\n- name: Port Sarim\n  x: 3044\n  y: 3218\n  z: 0\n- name: Pothole Dungeon\n  x: 2845\n  y: 9505\n  z: 0\n- name: Prifddinas\n  x: 2235\n  y: 3317\n  z: 0\n- name: Pyramid\n  x: 3233\n  y: 2896\n  z: 0\n- name: Quarry\n  x: 3172\n  y: 2908\n  z: 0\n- name: Ranging Guild\n  x: 2666\n  y: 3429\n  z: 0\n- name: Ratcatchers Mansion\n  x: 2847\n  y: 5086\n  z: 0\n- name: Rellekka\n  x: 2668\n  y: 3676\n  z: 0\n- name: Resource Area\n  x: 3185\n  y: 3934\n  z: 0\n- name: Rimmington\n  x: 2957\n  y: 3215\n  z: 0\n- name: River Elid\n  x: 3372\n  y: 3074\n  z: 0\n- name: River Lum\n  x: 3167\n  y: 3346\n  z: 0\n- name: River Salve\n  x: 3403\n  y: 3442\n  z: 0\n- name: Rogue's Den\n  x: 3047\n  y: 4976\n  z: 1\n- name: Rogues' Castle\n  x: 3286\n  y: 3931\n  z: 0\n- name: Ruins of Uzer\n  x: 3479\n  y: 3098\n  z: 0\n- name: Ruins\n  x: 2967\n  y: 3695\n  z: 0\n- name: Ruins\n  x: 3164\n  y: 3734\n  z: 0\n- name: Scorpion Pit\n  x: 3232\n  y: 3942\n  z: 0\n- name: Secret Hangar\n  x: 2391\n  y: 9890\n  z: 0\n- name: Seers' Village\n  x: 2701\n  y: 3483\n  z: 0\n- name: Shadow Dungeon\n  x: 2687\n  y: 5088\n  z: 0\n- name: Shantay Pass\n  x: 3304\n  y: 3122\n  z: 0\n- name: Shilo Village\n  x: 2844\n  y: 2982\n  z: 0\n- name: Ship Yard\n  x: 2987\n  y: 3055\n  z: 0\n- name: Sinclair Mansion\n  x: 2742\n  y: 3549\n  z: 0\n- name: Slayer Tower\n  x: 3428\n  y: 3554\n  z: 0\n- name: Smoke Dungeon\n  x: 3265\n  y: 9373\n  z: 0\n- name: Sophanem\n  x: 3296\n  y: 2780\n  z: 0\n- name: Sorcerer's Tower\n  x: 2702\n  y: 3404\n  z: 0\n- name: Spider\n  x: 3320\n  y: 3756\n  z: 0\n- name: Stronghold Slayer Cave\n  x: 2435\n  y: 9806\n  z: 0\n- name: Swamp\n  x: 2418\n  y: 3511\n  z: 0\n- name: Tai Bwo Wannai\n  x: 2789\n  y: 3063\n  z: 0\n- name: Taverley Dungeon\n  x: 2886\n  y: 9811\n  z: 0\n- name: Taverly\n  x: 2896\n  y: 3455\n  z: 0\n- name: Temple\n  x: 3414\n  y: 3487\n  z: 0\n- name: Temple of Ikov\n  x: 2649\n  y: 9854\n  z: 0\n- name: Temple of Marimbo Dungeon\n  x: 2784\n  y: 9184\n  z: 0\n- name: The Forgotten Cemetery\n  x: 2976\n  y: 3750\n  z: 0\n- name: The Hollows\n  x: 3498\n  y: 3381\n  z: 0\n- name: \"The Warrens\"\n  x: 1776\n  y: 10143\n  z: 0\n- name: Tirannwn\n  x: 2240\n  y: 3263\n  z: 0\n- name: Toll Gate\n  x: 3271\n  y: 3226\n  z: 0\n- name: Tower of Life\n  x: 2648\n  y: 3215\n  z: 0\n- name: Trawler\n  x: 2683\n  y: 3166\n  z: 0\n- name: Tree Gnome Stronghold\n  x: 2430\n  y: 3447\n  z: 0\n- name: Tree Gnome Village\n  x: 2527\n  y: 3166\n  z: 0\n- name: Troll Stronghold\n  x: 2832\n  y: 3682\n  z: 0\n- name: Trollheim\n  x: 2891\n  y: 3676\n  z: 0\n- name: Trollweiss Mountain\n  x: 2782\n  y: 3862\n  z: 0\n- name: Tutorial Island\n  x: 3101\n  y: 3094\n  z: 0\n- name: Tyras Camp\n  x: 2186\n  y: 3146\n  z: 0\n- name: TzHaar City\n  x: 2451\n  y: 5146\n  z: 0\n- name: Underground Pass\n  x: 2449\n  y: 3312\n  z: 0\n- name: Underground Pass Area 1\n  x: 2464\n  y: 9700\n  z: 0\n- name: Underground Pass Area 2\n  x: 2399\n  y: 9637\n  z: 0\n- name: Underground Pass Area 3\n  x: 2398\n  y: 9710\n  z: 0\n- name: Varrock\n  x: 3213\n  y: 3449\n  z: 0\n- name: Viyeldi caves\n  x: 2398\n  y: 4717\n  z: 0\n- name: Viyeldi caves (2)\n  x: 2782\n  y: 9315\n  z: 0\n- name: Void Knights' Outpost\n  x: 2639\n  y: 2674\n  z: 0\n- name: Vultures\n  x: 3337\n  y: 2868\n  z: 0\n- name: Waterbirth Island\n  x: 2521\n  y: 3757\n  z: 0\n- name: Waterbirth Dungeon 1\n  x: 2495\n  y: 10144\n  z: 0\n- name: Waterbirth Dungeon 2\n  x: 1895\n  y: 4367\n  z: 0\n- name: Waterbirth Dungeon 3\n  x: 1895\n  y: 4367\n  z: 1\n- name: Waterbirth Dungeon 4\n  x: 1895\n  y: 4367\n  z: 2\n- name: Waterbirth Dungeon 5\n  x: 1895\n  y: 4367\n  z: 3\n- name: Waterbirth Dungeon 6\n  x: 2912\n  y: 4448\n  z: 0\n- name: Waterfall Dungeon\n  x: 2577\n  y: 9890\n  z: 0\n- name: West Ardougne\n  x: 2524\n  y: 3305\n  z: 0\n- name: White Knights' Castle\n  x: 2969\n  y: 3341\n  z: 0\n- name: White Wolf Mountain\n  x: 2847\n  y: 3494\n  z: 0\n- name: Wilderness\n  x: 3144\n  y: 3775\n  z: 0\n- name: Witchaven\n  x: 2709\n  y: 3289\n  z: 0\n- name: Witchaven Dungeon\n  x: 2722\n  y: 9689\n  z: 0\n- name: Witchaven Dungeon (2)\n  x: 2329\n  y: 5097\n  z: 0\n- name: Wizards' Guild\n  x: 2583\n  y: 3078\n  z: 0\n- name: Wizards' Tower\n  x: 3110\n  y: 3157\n  z: 0\n- name: Yanille\n  x: 2554\n  y: 3089\n  z: 0\n- name: Yanille Agility Dungeon\n  x: 2581\n  y: 9499\n  z: 0\n- name: Yanille Agility Dungeon (2)\n  x: 2580\n  y: 9577\n  z: 0\n- name: Zanaris\n  x: 2415\n  y: 4455\n  z: 0\n"
  },
  {
    "path": "data/config/widgets.json",
    "content": "{\n    \"characterDesign\": 269,\n    \"furnace\": {\n        \"widgetId\": 311,\n        \"slots\": {\n            \"slot0\": {\n                \"modelId\": 4,\n                \"titleId\": 16\n            },\n            \"slot1\": {\n                \"modelId\": 5,\n                \"titleId\": 20\n            },\n            \"slot2\": {\n                \"modelId\": 6,\n                \"titleId\": 24\n            },\n            \"slot3\": {\n                \"modelId\": 7,\n                \"titleId\": 28\n            },\n            \"slot4\": {\n                \"modelId\": 8,\n                \"titleId\": 32\n            },\n            \"slot5\": {\n                \"modelId\": 9,\n                \"titleId\": 36\n            },\n            \"slot6\": {\n                \"modelId\": 10,\n                \"titleId\": 40\n            },\n            \"slot7\": {\n                \"modelId\": 11,\n                \"titleId\": 44\n            },\n            \"slot8\": {\n                \"modelId\": 12,\n                \"titleId\": 48\n            }\n        }\n    },\n    \"anvil\": {\n        \"widgetId\": 312\n    },\n    \"inventory\": {\n        \"widgetId\": 149,\n        \"containerId\": 0\n    },\n    \"disabledTabs\": 151,\n    \"equipment\": {\n        \"widgetId\": 387,\n        \"containerId\": 25\n    },\n    \"equipmentStats\": {\n        \"widgetId\": 465,\n        \"containerId\": 103\n    },\n    \"equipmentStatsInventory\": {\n        \"widgetId\": 336,\n        \"containerId\": 0\n    },\n    \"bank\": {\n        \"depositBoxWidget\": {\n            \"widgetId\": 11,\n            \"containerId\": 61,\n            \"titleText\": 60\n        },\n        \"pinSettingsWidget\": {\n            \"widgetId\": 14\n        },\n        \"screenWidget\": {\n            \"widgetId\": 12,\n            \"containerId\": 89\n        },\n        \"tabWidget\": {\n            \"widgetId\": 266,\n            \"containerId\": 0\n        }\n    },\n    \"defaultCombatStyle\": 92,\n    \"skillGuide\": 308,\n    \"skillsTab\": 320,\n    \"friendsList\": 131,\n    \"ignoreList\": 148,\n    \"logoutTab\": 182,\n    \"settingsTab\": 261,\n    \"emotesTab\": 464,\n    \"musicPlayerTab\": 239,\n    \"prayerTab\": 271,\n    \"standardSpellbookTab\": 192,\n    \"questTab\": 274,\n    \"shop\": {\n        \"widgetId\": 300,\n        \"containerId\": 75,\n        \"title\": 76\n    },\n    \"shopPlayerInventory\": {\n        \"widgetId\": 301,\n        \"containerId\": 0\n    },\n    \"questJournal\": 275,\n    \"questReward\": 277,\n    \"welcomeScreen\": 378,\n    \"welcomeScreenChildren\": {\n        \"cogs\": 16,\n        \"question\": 17,\n        \"drama\": 18,\n        \"bankPin\": 19,\n        \"bankPinQuestion\": 20,\n        \"scamming\": 21,\n        \"bankPinKey\": 22,\n        \"christmas\": 23,\n        \"killcount\": 24\n    },\n    \"whatWouldYouLikeToSpin\": 459\n}\n"
  },
  {
    "path": "data/config/xteas/435.json",
    "content": "[\n  {\n    \"archive\": 5,\n    \"group\": 1,\n    \"name_hash\": -1153472937,\n    \"name\": \"l40_55\",\n    \"mapsquare\": 10295,\n    \"key\": [\n      -1920480496,\n      -1423914110,\n      951774544,\n      -1419269290\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 5,\n    \"name_hash\": -1155051772,\n    \"name\": \"l29_80\",\n    \"mapsquare\": 7504,\n    \"key\": [\n      1847230655,\n      1366615901,\n      817013928,\n      -639754200\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 9,\n    \"name_hash\": -1153323920,\n    \"name\": \"l45_75\",\n    \"mapsquare\": 11595,\n    \"key\": [\n      -1714471127,\n      803338900,\n      314885542,\n      1310938544\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 13,\n    \"name_hash\": -1418243906,\n    \"name\": \"l39_160\",\n    \"mapsquare\": 10144,\n    \"key\": [\n      265530509,\n      2033515489,\n      -2022406749,\n      -591072091\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 21,\n    \"name_hash\": -1153472876,\n    \"name\": \"l40_74\",\n    \"mapsquare\": 10314,\n    \"key\": [\n      -2020182865,\n      -861201399,\n      1793241744,\n      -541294677\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 23,\n    \"name_hash\": -1153472875,\n    \"name\": \"l40_75\",\n    \"mapsquare\": 10315,\n    \"key\": [\n      874541054,\n      1607050822,\n      800324422,\n      1994534030\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 25,\n    \"name_hash\": -1397926444,\n    \"name\": \"l40_160\",\n    \"mapsquare\": 10400,\n    \"key\": [\n      -1656816922,\n      1318311812,\n      -811481661,\n      1625916625\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 37,\n    \"name_hash\": -1154366605,\n    \"name\": \"l31_75\",\n    \"mapsquare\": 8011,\n    \"key\": [\n      -1287670691,\n      690712165,\n      -1180498360,\n      -813635980\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 39,\n    \"name_hash\": -1154366601,\n    \"name\": \"l31_79\",\n    \"mapsquare\": 8015,\n    \"key\": [\n      -1320874262,\n      1937053221,\n      220952674,\n      1903403210\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 41,\n    \"name_hash\": -1395155938,\n    \"name\": \"l43_145\",\n    \"mapsquare\": 11153,\n    \"key\": [\n      -483259316,\n      1922539567,\n      1755917448,\n      -1522301387\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 43,\n    \"name_hash\": -1395155937,\n    \"name\": \"l43_146\",\n    \"mapsquare\": 11154,\n    \"key\": [\n      865563030,\n      -1947773050,\n      322324580,\n      1127332895\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 49,\n    \"name_hash\": -1153472848,\n    \"name\": \"l40_81\",\n    \"mapsquare\": 10321,\n    \"key\": [\n      -1416786366,\n      -183512307,\n      774862574,\n      1306347169\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 53,\n    \"name_hash\": -1420091002,\n    \"name\": \"l37_148\",\n    \"mapsquare\": 9620,\n    \"key\": [\n      17247799,\n      -1105459916,\n      1430084566,\n      -466128103\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 57,\n    \"name_hash\": -1420090979,\n    \"name\": \"l37_150\",\n    \"mapsquare\": 9622,\n    \"key\": [\n      56315964,\n      322433230,\n      -2111187201,\n      -1087892507\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 59,\n    \"name_hash\": -1420090978,\n    \"name\": \"l37_151\",\n    \"mapsquare\": 9623,\n    \"key\": [\n      1173395984,\n      1622423564,\n      1684894916,\n      949843813\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 63,\n    \"name_hash\": -1420090976,\n    \"name\": \"l37_153\",\n    \"mapsquare\": 9625,\n    \"key\": [\n      -1160175957,\n      -906620860,\n      -585287228,\n      -1756474591\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 65,\n    \"name_hash\": -1420090975,\n    \"name\": \"l37_154\",\n    \"mapsquare\": 9626,\n    \"key\": [\n      -1086856428,\n      1097853921,\n      -94537716,\n      -1037836120\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 67,\n    \"name_hash\": -1152519655,\n    \"name\": \"l51_46\",\n    \"mapsquare\": 13102,\n    \"key\": [\n      -585304244,\n      -85656032,\n      -1672180123,\n      -889752362\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 69,\n    \"name_hash\": -1152519654,\n    \"name\": \"l51_47\",\n    \"mapsquare\": 13103,\n    \"key\": [\n      664202950,\n      9942731,\n      526981448,\n      1961317213\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 75,\n    \"name_hash\": -1152519630,\n    \"name\": \"l51_50\",\n    \"mapsquare\": 13106,\n    \"key\": [\n      -143377915,\n      -1243385951,\n      -2076474290,\n      2005128831\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 77,\n    \"name_hash\": -1152519629,\n    \"name\": \"l51_51\",\n    \"mapsquare\": 13107,\n    \"key\": [\n      282236429,\n      12837044,\n      477635096,\n      -1973557870\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 81,\n    \"name_hash\": -1152519627,\n    \"name\": \"l51_53\",\n    \"mapsquare\": 13109,\n    \"key\": [\n      1764736868,\n      -790106346,\n      -2113719433,\n      100735736\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 87,\n    \"name_hash\": -1152519624,\n    \"name\": \"l51_56\",\n    \"mapsquare\": 13112,\n    \"key\": [\n      -294331294,\n      -882184793,\n      66656303,\n      -1818669310\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 91,\n    \"name_hash\": -1152519622,\n    \"name\": \"l51_58\",\n    \"mapsquare\": 13114,\n    \"key\": [\n      -1266416096,\n      120695750,\n      -1880861340,\n      2120698563\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 93,\n    \"name_hash\": -1153264429,\n    \"name\": \"l47_47\",\n    \"mapsquare\": 12079,\n    \"key\": [\n      339960494,\n      -1350930341,\n      -272469140,\n      318766784\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 95,\n    \"name_hash\": -1152519621,\n    \"name\": \"l51_59\",\n    \"mapsquare\": 13115,\n    \"key\": [\n      1818342784,\n      -642975152,\n      -355404480,\n      132805372\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 97,\n    \"name_hash\": -1153264428,\n    \"name\": \"l47_48\",\n    \"mapsquare\": 12080,\n    \"key\": [\n      -1938428459,\n      -1838574783,\n      131585942,\n      -1297008627\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 99,\n    \"name_hash\": -1153264427,\n    \"name\": \"l47_49\",\n    \"mapsquare\": 12081,\n    \"key\": [\n      613255413,\n      -1717776939,\n      -179584559,\n      -1310165510\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 101,\n    \"name_hash\": -1152519599,\n    \"name\": \"l51_60\",\n    \"mapsquare\": 13116,\n    \"key\": [\n      1370550295,\n      -820804475,\n      1895373272,\n      1832997410\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 103,\n    \"name_hash\": -1152519598,\n    \"name\": \"l51_61\",\n    \"mapsquare\": 13117,\n    \"key\": [\n      -1752848915,\n      -1242872593,\n      1012298354,\n      -1076490243\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 105,\n    \"name_hash\": -1153264405,\n    \"name\": \"l47_50\",\n    \"mapsquare\": 12082,\n    \"key\": [\n      978817309,\n      -1069180717,\n      -1957128971,\n      -476497920\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 109,\n    \"name_hash\": -1153264403,\n    \"name\": \"l47_52\",\n    \"mapsquare\": 12084,\n    \"key\": [\n      -1358781692,\n      -1981539499,\n      -2131133338,\n      -221063772\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 111,\n    \"name_hash\": -1153264402,\n    \"name\": \"l47_53\",\n    \"mapsquare\": 12085,\n    \"key\": [\n      866634875,\n      2126690507,\n      1264568376,\n      2107515492\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 117,\n    \"name_hash\": -1153264399,\n    \"name\": \"l47_56\",\n    \"mapsquare\": 12088,\n    \"key\": [\n      344960338,\n      -452527992,\n      1340005990,\n      -1328736855\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 119,\n    \"name_hash\": -1153264398,\n    \"name\": \"l47_57\",\n    \"mapsquare\": 12089,\n    \"key\": [\n      -551791250,\n      -2032976862,\n      -1354246732,\n      1795367998\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 121,\n    \"name_hash\": -1153264397,\n    \"name\": \"l47_58\",\n    \"mapsquare\": 12090,\n    \"key\": [\n      -2012109696,\n      -607715631,\n      -468949176,\n      1368365998\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 123,\n    \"name_hash\": -1153264396,\n    \"name\": \"l47_59\",\n    \"mapsquare\": 12091,\n    \"key\": [\n      -2111919909,\n      -1125782501,\n      -1802382602,\n      2137630247\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 125,\n    \"name_hash\": -1153413382,\n    \"name\": \"l42_49\",\n    \"mapsquare\": 10801,\n    \"key\": [\n      -1831859863,\n      -1836056891,\n      342489401,\n      1312886995\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 127,\n    \"name_hash\": -1153264374,\n    \"name\": \"l47_60\",\n    \"mapsquare\": 12092,\n    \"key\": [\n      111448945,\n      1997179939,\n      -1992758838,\n      -904080104\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 129,\n    \"name_hash\": -1153264373,\n    \"name\": \"l47_61\",\n    \"mapsquare\": 12093,\n    \"key\": [\n      -901249604,\n      892308020,\n      1075683735,\n      -148430952\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 131,\n    \"name_hash\": -1153413360,\n    \"name\": \"l42_50\",\n    \"mapsquare\": 10802,\n    \"key\": [\n      1999669309,\n      1732706412,\n      1414266618,\n      -2034805718\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 133,\n    \"name_hash\": -1153413359,\n    \"name\": \"l42_51\",\n    \"mapsquare\": 10803,\n    \"key\": [\n      632564181,\n      298843585,\n      -1472601465,\n      -1065203254\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 135,\n    \"name_hash\": -1153413358,\n    \"name\": \"l42_52\",\n    \"mapsquare\": 10804,\n    \"key\": [\n      844993707,\n      -1910359056,\n      -477807005,\n      1502341650\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 137,\n    \"name_hash\": -1153413357,\n    \"name\": \"l42_53\",\n    \"mapsquare\": 10805,\n    \"key\": [\n      -381810387,\n      1628315077,\n      690097561,\n      -713577288\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 141,\n    \"name_hash\": -1153413355,\n    \"name\": \"l42_55\",\n    \"mapsquare\": 10807,\n    \"key\": [\n      698638677,\n      -1565681878,\n      635760984,\n      -1240425436\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 143,\n    \"name_hash\": -1153413354,\n    \"name\": \"l42_56\",\n    \"mapsquare\": 10808,\n    \"key\": [\n      -960669862,\n      212910530,\n      -1874628754,\n      1886016422\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 145,\n    \"name_hash\": -1369297323,\n    \"name\": \"l50_151\",\n    \"mapsquare\": 12951,\n    \"key\": [\n      -700278939,\n      1204517853,\n      912770506,\n      985515932\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 147,\n    \"name_hash\": -1154158160,\n    \"name\": \"l38_46\",\n    \"mapsquare\": 9774,\n    \"key\": [\n      -525517154,\n      -170441725,\n      -288082119,\n      -1442581115\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 149,\n    \"name_hash\": -1154158159,\n    \"name\": \"l38_47\",\n    \"mapsquare\": 9775,\n    \"key\": [\n      358467,\n      -1032890524,\n      -323905369,\n      672132700\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 153,\n    \"name_hash\": -1154158157,\n    \"name\": \"l38_49\",\n    \"mapsquare\": 9777,\n    \"key\": [\n      31736957,\n      -2137121125,\n      248765559,\n      -757570002\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 159,\n    \"name_hash\": -1154158133,\n    \"name\": \"l38_52\",\n    \"mapsquare\": 9780,\n    \"key\": [\n      23047955,\n      374207630,\n      -14357742,\n      487844353\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 161,\n    \"name_hash\": -1153264338,\n    \"name\": \"l47_75\",\n    \"mapsquare\": 12107,\n    \"key\": [\n      -90455586,\n      506470665,\n      -1991061709,\n      1699265764\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 167,\n    \"name_hash\": -1154158130,\n    \"name\": \"l38_55\",\n    \"mapsquare\": 9783,\n    \"key\": [\n      -697645230,\n      -219261751,\n      668010484,\n      -637416792\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 175,\n    \"name_hash\": -1153413293,\n    \"name\": \"l42_75\",\n    \"mapsquare\": 10827,\n    \"key\": [\n      -837303631,\n      -267210523,\n      -2043344605,\n      99880496\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 177,\n    \"name_hash\": -1368373827,\n    \"name\": \"l51_147\",\n    \"mapsquare\": 13203,\n    \"key\": [\n      -1342258113,\n      -2106603709,\n      395365404,\n      -973258459\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 179,\n    \"name_hash\": -1152370701,\n    \"name\": \"l56_45\",\n    \"mapsquare\": 14381,\n    \"key\": [\n      -534048527,\n      1614741928,\n      -469012711,\n      1064350100\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 183,\n    \"name_hash\": -1154158069,\n    \"name\": \"l38_74\",\n    \"mapsquare\": 9802,\n    \"key\": [\n      -1207560898,\n      -2121890421,\n      -1280361853,\n      1268736089\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 185,\n    \"name_hash\": -1368373799,\n    \"name\": \"l51_154\",\n    \"mapsquare\": 13210,\n    \"key\": [\n      -1504547443,\n      1455578463,\n      -1808313124,\n      -45471826\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 189,\n    \"name_hash\": -1154307027,\n    \"name\": \"l33_71\",\n    \"mapsquare\": 8519,\n    \"key\": [\n      -2051430502,\n      -746663069,\n      -593970049,\n      379050420\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 191,\n    \"name_hash\": -1154307026,\n    \"name\": \"l33_72\",\n    \"mapsquare\": 8520,\n    \"key\": [\n      576327208,\n      1659610549,\n      -1319545488,\n      -1984379615\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 193,\n    \"name_hash\": -1154307025,\n    \"name\": \"l33_73\",\n    \"mapsquare\": 8521,\n    \"key\": [\n      -1909603451,\n      -596972288,\n      -446203583,\n      -1017538492\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 199,\n    \"name_hash\": -1154307022,\n    \"name\": \"l33_76\",\n    \"mapsquare\": 8524,\n    \"key\": [\n      1989551519,\n      1836116867,\n      185458553,\n      223032295\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 201,\n    \"name_hash\": -1393308896,\n    \"name\": \"l45_145\",\n    \"mapsquare\": 11665,\n    \"key\": [\n      862638927,\n      -1128276245,\n      -2128612744,\n      77024238\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 203,\n    \"name_hash\": -1393308895,\n    \"name\": \"l45_146\",\n    \"mapsquare\": 11666,\n    \"key\": [\n      714846754,\n      200141282,\n      -207402626,\n      2133043283\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 205,\n    \"name_hash\": -1393308893,\n    \"name\": \"l45_148\",\n    \"mapsquare\": 11668,\n    \"key\": [\n      1094922095,\n      -1747387328,\n      1439302672,\n      1510179242\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 209,\n    \"name_hash\": -1393308869,\n    \"name\": \"l45_151\",\n    \"mapsquare\": 11671,\n    \"key\": [\n      517807890,\n      -107570269,\n      1542962182,\n      -1495382393\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 211,\n    \"name_hash\": -1393308868,\n    \"name\": \"l45_152\",\n    \"mapsquare\": 11672,\n    \"key\": [\n      59956468,\n      -1316944326,\n      -1514599528,\n      979714228\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 213,\n    \"name_hash\": -1393308867,\n    \"name\": \"l45_153\",\n    \"mapsquare\": 11673,\n    \"key\": [\n      343838952,\n      -181461714,\n      1044748667,\n      1241583057\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 215,\n    \"name_hash\": -1393308866,\n    \"name\": \"l45_154\",\n    \"mapsquare\": 11674,\n    \"key\": [\n      -1389259398,\n      1840036337,\n      732526621,\n      -1831580013\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 217,\n    \"name_hash\": -1155051798,\n    \"name\": \"l29_75\",\n    \"mapsquare\": 7499,\n    \"key\": [\n      389242534,\n      1468756327,\n      1739858109,\n      -397529213\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 225,\n    \"name_hash\": -1418243959,\n    \"name\": \"l39_149\",\n    \"mapsquare\": 10133,\n    \"key\": [\n      -1295008659,\n      266597781,\n      -348722649,\n      -387821353\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 227,\n    \"name_hash\": -1418243937,\n    \"name\": \"l39_150\",\n    \"mapsquare\": 10134,\n    \"key\": [\n      -169090672,\n      -418150474,\n      -663319294,\n      345797130\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 233,\n    \"name_hash\": -1418243934,\n    \"name\": \"l39_153\",\n    \"mapsquare\": 10137,\n    \"key\": [\n      309023294,\n      10701046,\n      752820323,\n      320724862\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 235,\n    \"name_hash\": -1418243933,\n    \"name\": \"l39_154\",\n    \"mapsquare\": 10138,\n    \"key\": [\n      -1266932509,\n      1159824249,\n      1189711903,\n      1903826998\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 243,\n    \"name_hash\": -1152460047,\n    \"name\": \"l53_51\",\n    \"mapsquare\": 13619,\n    \"key\": [\n      712676739,\n      -1616631519,\n      -393806187,\n      1432842778\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 245,\n    \"name_hash\": -1152460046,\n    \"name\": \"l53_52\",\n    \"mapsquare\": 13620,\n    \"key\": [\n      -1340730148,\n      -1509744228,\n      -755242060,\n      937448072\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 247,\n    \"name_hash\": -1152460045,\n    \"name\": \"l53_53\",\n    \"mapsquare\": 13621,\n    \"key\": [\n      -58207297,\n      135788372,\n      -1606570127,\n      662309804\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 249,\n    \"name_hash\": -1153204848,\n    \"name\": \"l49_46\",\n    \"mapsquare\": 12590,\n    \"key\": [\n      1057515769,\n      -334000230,\n      -1142782469,\n      1285186544\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 251,\n    \"name_hash\": -1153204847,\n    \"name\": \"l49_47\",\n    \"mapsquare\": 12591,\n    \"key\": [\n      1153167116,\n      1162748714,\n      -2111043500,\n      -1049253880\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 253,\n    \"name_hash\": -1153204846,\n    \"name\": \"l49_48\",\n    \"mapsquare\": 12592,\n    \"key\": [\n      -794001650,\n      -436110669,\n      1193837402,\n      1311524977\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 255,\n    \"name_hash\": -1153204845,\n    \"name\": \"l49_49\",\n    \"mapsquare\": 12593,\n    \"key\": [\n      -544590439,\n      -1841162484,\n      -767714061,\n      1725894803\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 257,\n    \"name_hash\": -1153204823,\n    \"name\": \"l49_50\",\n    \"mapsquare\": 12594,\n    \"key\": [\n      -120447093,\n      579654861,\n      -527409529,\n      1730758809\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 259,\n    \"name_hash\": -1153204822,\n    \"name\": \"l49_51\",\n    \"mapsquare\": 12595,\n    \"key\": [\n      -1713627476,\n      -189383107,\n      2075238616,\n      630646748\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 261,\n    \"name_hash\": -1153204821,\n    \"name\": \"l49_52\",\n    \"mapsquare\": 12596,\n    \"key\": [\n      1748061888,\n      601884795,\n      -571814788,\n      -1524221044\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 265,\n    \"name_hash\": -1152370700,\n    \"name\": \"l56_46\",\n    \"mapsquare\": 14382,\n    \"key\": [\n      -2047578817,\n      357439085,\n      332136595,\n      1757066622\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 271,\n    \"name_hash\": -1153204817,\n    \"name\": \"l49_56\",\n    \"mapsquare\": 12600,\n    \"key\": [\n      2086237847,\n      -2000241337,\n      787272168,\n      -1342875227\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 273,\n    \"name_hash\": -1153353804,\n    \"name\": \"l44_45\",\n    \"mapsquare\": 11309,\n    \"key\": [\n      349211408,\n      1529654711,\n      -1227331504,\n      531945093\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 275,\n    \"name_hash\": -1153204816,\n    \"name\": \"l49_57\",\n    \"mapsquare\": 12601,\n    \"key\": [\n      -360556539,\n      1198026803,\n      -609365963,\n      -1563087824\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 279,\n    \"name_hash\": -1153204815,\n    \"name\": \"l49_58\",\n    \"mapsquare\": 12602,\n    \"key\": [\n      -1597607397,\n      -2146719506,\n      -1185304014,\n      1704592002\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 281,\n    \"name_hash\": -1153353802,\n    \"name\": \"l44_47\",\n    \"mapsquare\": 11311,\n    \"key\": [\n      255517588,\n      -1240190218,\n      -693538893,\n      52321524\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 283,\n    \"name_hash\": -1153204814,\n    \"name\": \"l49_59\",\n    \"mapsquare\": 12603,\n    \"key\": [\n      1903637882,\n      -2078269745,\n      1154595732,\n      2025457352\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 289,\n    \"name_hash\": -1153204792,\n    \"name\": \"l49_60\",\n    \"mapsquare\": 12604,\n    \"key\": [\n      -1812120898,\n      -1662909112,\n      -1432124543,\n      571589094\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 291,\n    \"name_hash\": -1153204791,\n    \"name\": \"l49_61\",\n    \"mapsquare\": 12605,\n    \"key\": [\n      794830896,\n      -1814271030,\n      54960303,\n      1749986908\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 293,\n    \"name_hash\": -1153353778,\n    \"name\": \"l44_50\",\n    \"mapsquare\": 11314,\n    \"key\": [\n      1619729167,\n      1824886381,\n      1092421269,\n      -1188344727\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 295,\n    \"name_hash\": -1153353777,\n    \"name\": \"l44_51\",\n    \"mapsquare\": 11315,\n    \"key\": [\n      -202635393,\n      778738800,\n      432999637,\n      1992018816\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 299,\n    \"name_hash\": -1153353775,\n    \"name\": \"l44_53\",\n    \"mapsquare\": 11317,\n    \"key\": [\n      1589220603,\n      1966511687,\n      -1108893479,\n      -635825550\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 307,\n    \"name_hash\": -1391461850,\n    \"name\": \"l47_149\",\n    \"mapsquare\": 12181,\n    \"key\": [\n      -1585897411,\n      -486973222,\n      976725192,\n      587846488\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 313,\n    \"name_hash\": -1391461825,\n    \"name\": \"l47_153\",\n    \"mapsquare\": 12185,\n    \"key\": [\n      -1833444484,\n      -1358796470,\n      -2064057632,\n      1209694095\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 317,\n    \"name_hash\": -1391461797,\n    \"name\": \"l47_160\",\n    \"mapsquare\": 12192,\n    \"key\": [\n      152792546,\n      1064551985,\n      823336050,\n      -12082873\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 319,\n    \"name_hash\": -1391461796,\n    \"name\": \"l47_161\",\n    \"mapsquare\": 12193,\n    \"key\": [\n      -593888487,\n      92505902,\n      -268347225,\n      2144548258\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 325,\n    \"name_hash\": -1397926499,\n    \"name\": \"l40_147\",\n    \"mapsquare\": 10387,\n    \"key\": [\n      1215785755,\n      210499977,\n      -2093512319,\n      -625901574\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 327,\n    \"name_hash\": -1152340910,\n    \"name\": \"l57_45\",\n    \"mapsquare\": 14637,\n    \"key\": [\n      1954698915,\n      587858534,\n      -1998840957,\n      -860751267\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 329,\n    \"name_hash\": -1397926498,\n    \"name\": \"l40_148\",\n    \"mapsquare\": 10388,\n    \"key\": [\n      1415243362,\n      801135032,\n      -385381783,\n      -83383530\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 331,\n    \"name_hash\": -1153353711,\n    \"name\": \"l44_75\",\n    \"mapsquare\": 11339,\n    \"key\": [\n      -1717666269,\n      1594420520,\n      -1751131464,\n      -154240780\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 343,\n    \"name_hash\": -1397926471,\n    \"name\": \"l40_154\",\n    \"mapsquare\": 10394,\n    \"key\": [\n      -2102331685,\n      781435808,\n      1774321038,\n      -660278831\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 347,\n    \"name_hash\": -1152340908,\n    \"name\": \"l57_47\",\n    \"mapsquare\": 14639,\n    \"key\": [\n      -1842058479,\n      -1554929196,\n      440799576,\n      -1954881277\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 349,\n    \"name_hash\": -1154396396,\n    \"name\": \"l30_75\",\n    \"mapsquare\": 7755,\n    \"key\": [\n      1467763642,\n      -2057666779,\n      1692811,\n      1234049587\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 351,\n    \"name_hash\": -1152340907,\n    \"name\": \"l57_48\",\n    \"mapsquare\": 14640,\n    \"key\": [\n      -95254230,\n      -1094148730,\n      -1768319507,\n      105210786\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 357,\n    \"name_hash\": -1389614782,\n    \"name\": \"l49_154\",\n    \"mapsquare\": 12698,\n    \"key\": [\n      1313997139,\n      413705980,\n      -1786270630,\n      -1211819932\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 363,\n    \"name_hash\": -1361909181,\n    \"name\": \"l58_146\",\n    \"mapsquare\": 14994,\n    \"key\": [\n      96028866,\n      945858112,\n      1932735023,\n      1432905132\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 365,\n    \"name_hash\": -1361909180,\n    \"name\": \"l58_147\",\n    \"mapsquare\": 14995,\n    \"key\": [\n      452468371,\n      863200236,\n      1512019095,\n      410208849\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 367,\n    \"name_hash\": -1152311119,\n    \"name\": \"l58_45\",\n    \"mapsquare\": 14893,\n    \"key\": [\n      -102394113,\n      1484262111,\n      933695487,\n      -283213546\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 369,\n    \"name_hash\": -1396079432,\n    \"name\": \"l42_151\",\n    \"mapsquare\": 10903,\n    \"key\": [\n      -1260800958,\n      -1306263170,\n      319256612,\n      1478365211\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 371,\n    \"name_hash\": -1152549446,\n    \"name\": \"l50_46\",\n    \"mapsquare\": 12846,\n    \"key\": [\n      -1128107273,\n      -1379368189,\n      -1166299202,\n      1367493265\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 373,\n    \"name_hash\": -1396079431,\n    \"name\": \"l42_152\",\n    \"mapsquare\": 10904,\n    \"key\": [\n      299433082,\n      1093303265,\n      1627642083,\n      1856381978\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 377,\n    \"name_hash\": -1396079430,\n    \"name\": \"l42_153\",\n    \"mapsquare\": 10905,\n    \"key\": [\n      15962844,\n      -263608354,\n      -1698286677,\n      -1506930528\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 379,\n    \"name_hash\": -1152549444,\n    \"name\": \"l50_48\",\n    \"mapsquare\": 12848,\n    \"key\": [\n      -472467501,\n      942301843,\n      -1235756555,\n      1092807961\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 381,\n    \"name_hash\": -1152549443,\n    \"name\": \"l50_49\",\n    \"mapsquare\": 12849,\n    \"key\": [\n      856523891,\n      -869724785,\n      1850232409,\n      -674722748\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 387,\n    \"name_hash\": -1152549419,\n    \"name\": \"l50_52\",\n    \"mapsquare\": 12852,\n    \"key\": [\n      -383547364,\n      -1207189748,\n      1322204269,\n      -1173579306\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 389,\n    \"name_hash\": -1152549418,\n    \"name\": \"l50_53\",\n    \"mapsquare\": 12853,\n    \"key\": [\n      -1734454891,\n      559576609,\n      2053124820,\n      179220049\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 395,\n    \"name_hash\": -1152549415,\n    \"name\": \"l50_56\",\n    \"mapsquare\": 12856,\n    \"key\": [\n      1065837316,\n      566874993,\n      -395031456,\n      -1005765069\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 397,\n    \"name_hash\": -1153294222,\n    \"name\": \"l46_45\",\n    \"mapsquare\": 11821,\n    \"key\": [\n      -1676526445,\n      -459611821,\n      895613340,\n      -1034819584\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 399,\n    \"name_hash\": -1152549414,\n    \"name\": \"l50_57\",\n    \"mapsquare\": 12857,\n    \"key\": [\n      1315061238,\n      798718066,\n      -593804887,\n      1674278988\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 401,\n    \"name_hash\": -1153294221,\n    \"name\": \"l46_46\",\n    \"mapsquare\": 11822,\n    \"key\": [\n      1634541811,\n      1149149862,\n      -1352773374,\n      843504994\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 403,\n    \"name_hash\": -1152311118,\n    \"name\": \"l58_46\",\n    \"mapsquare\": 14894,\n    \"key\": [\n      -699464927,\n      -423502070,\n      -1690485752,\n      1901016133\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 405,\n    \"name_hash\": -1152549413,\n    \"name\": \"l50_58\",\n    \"mapsquare\": 12858,\n    \"key\": [\n      1739165090,\n      1380036717,\n      1760229212,\n      -243206298\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 407,\n    \"name_hash\": -1153294220,\n    \"name\": \"l46_47\",\n    \"mapsquare\": 11823,\n    \"key\": [\n      -256018875,\n      2059756735,\n      1142698660,\n      1129906231\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 409,\n    \"name_hash\": -1152311117,\n    \"name\": \"l58_47\",\n    \"mapsquare\": 14895,\n    \"key\": [\n      1294223700,\n      778677766,\n      -2141486577,\n      1402077049\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 411,\n    \"name_hash\": -1152549412,\n    \"name\": \"l50_59\",\n    \"mapsquare\": 12859,\n    \"key\": [\n      426800430,\n      789609597,\n      2049374976,\n      800682309\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 413,\n    \"name_hash\": -1153294219,\n    \"name\": \"l46_48\",\n    \"mapsquare\": 11824,\n    \"key\": [\n      -1116727390,\n      -659788658,\n      307002260,\n      354360287\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 417,\n    \"name_hash\": -1153294218,\n    \"name\": \"l46_49\",\n    \"mapsquare\": 11825,\n    \"key\": [\n      1525999114,\n      1162595366,\n      465333685,\n      1306030426\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 421,\n    \"name_hash\": -1152549390,\n    \"name\": \"l50_60\",\n    \"mapsquare\": 12860,\n    \"key\": [\n      189035026,\n      -14034698,\n      -394067455,\n      -734636364\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 423,\n    \"name_hash\": -1152549389,\n    \"name\": \"l50_61\",\n    \"mapsquare\": 12861,\n    \"key\": [\n      1816963481,\n      -496926322,\n      1214068433,\n      1190940799\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 427,\n    \"name_hash\": -1421014500,\n    \"name\": \"l36_150\",\n    \"mapsquare\": 9366,\n    \"key\": [\n      1443456963,\n      1791165211,\n      59698870,\n      1492849730\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 435,\n    \"name_hash\": -1153294193,\n    \"name\": \"l46_53\",\n    \"mapsquare\": 11829,\n    \"key\": [\n      1880703134,\n      1096367920,\n      -438608238,\n      -2070635879\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 437,\n    \"name_hash\": -1421014497,\n    \"name\": \"l36_153\",\n    \"mapsquare\": 9369,\n    \"key\": [\n      1046237398,\n      2034378264,\n      -2111613007,\n      1419925615\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 441,\n    \"name_hash\": -1421014496,\n    \"name\": \"l36_154\",\n    \"mapsquare\": 9370,\n    \"key\": [\n      1560310225,\n      -1912646908,\n      -21541393,\n      -690057286\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 445,\n    \"name_hash\": -1153294190,\n    \"name\": \"l46_56\",\n    \"mapsquare\": 11832,\n    \"key\": [\n      1106039923,\n      -761356647,\n      -2114299754,\n      948992246\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 453,\n    \"name_hash\": -1153294188,\n    \"name\": \"l46_58\",\n    \"mapsquare\": 11834,\n    \"key\": [\n      173215366,\n      -1499360859,\n      472196248,\n      1020719132\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 457,\n    \"name_hash\": -1153294187,\n    \"name\": \"l46_59\",\n    \"mapsquare\": 11835,\n    \"key\": [\n      1629126437,\n      1652550905,\n      841609762,\n      -317888116\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 459,\n    \"name_hash\": -1153443174,\n    \"name\": \"l41_48\",\n    \"mapsquare\": 10544,\n    \"key\": [\n      -895303896,\n      744949463,\n      -2033369513,\n      338766696\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 463,\n    \"name_hash\": -1153294165,\n    \"name\": \"l46_60\",\n    \"mapsquare\": 11836,\n    \"key\": [\n      1310410800,\n      423198468,\n      -1302324807,\n      259991487\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 465,\n    \"name_hash\": -1153294164,\n    \"name\": \"l46_61\",\n    \"mapsquare\": 11837,\n    \"key\": [\n      1906296384,\n      292432464,\n      594794920,\n      -1546603049\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 471,\n    \"name_hash\": -1153443149,\n    \"name\": \"l41_52\",\n    \"mapsquare\": 10548,\n    \"key\": [\n      1215763226,\n      2050714422,\n      -2048665259,\n      230062419\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 473,\n    \"name_hash\": -1153443148,\n    \"name\": \"l41_53\",\n    \"mapsquare\": 10549,\n    \"key\": [\n      1856240647,\n      -1307099618,\n      -1197945308,\n      -2122254516\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 475,\n    \"name_hash\": -1153443147,\n    \"name\": \"l41_54\",\n    \"mapsquare\": 10550,\n    \"key\": [\n      957349475,\n      -496721990,\n      1780878747,\n      -1948737082\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 477,\n    \"name_hash\": -1153443146,\n    \"name\": \"l41_55\",\n    \"mapsquare\": 10551,\n    \"key\": [\n      1042296612,\n      -799710697,\n      -433924400,\n      -596565458\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 479,\n    \"name_hash\": -1153443145,\n    \"name\": \"l41_56\",\n    \"mapsquare\": 10552,\n    \"key\": [\n      1428473548,\n      -513790027,\n      959922611,\n      2094550429\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 483,\n    \"name_hash\": -1154187948,\n    \"name\": \"l37_49\",\n    \"mapsquare\": 9521,\n    \"key\": [\n      -1204382617,\n      894320578,\n      -2017151095,\n      -679668405\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 485,\n    \"name_hash\": -1154187926,\n    \"name\": \"l37_50\",\n    \"mapsquare\": 9522,\n    \"key\": [\n      1252542168,\n      -1448059709,\n      1122042436,\n      -1636001244\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 487,\n    \"name_hash\": -1154187925,\n    \"name\": \"l37_51\",\n    \"mapsquare\": 9523,\n    \"key\": [\n      -1310187207,\n      -834726064,\n      -1799766719,\n      1699616161\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 495,\n    \"name_hash\": -1154187922,\n    \"name\": \"l37_54\",\n    \"mapsquare\": 9526,\n    \"key\": [\n      -2064252262,\n      -887239839,\n      1167799401,\n      858861043\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 501,\n    \"name_hash\": -1153443086,\n    \"name\": \"l41_73\",\n    \"mapsquare\": 10569,\n    \"key\": [\n      501975646,\n      1931710158,\n      -452188231,\n      -1541425293\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 505,\n    \"name_hash\": -1153443084,\n    \"name\": \"l41_75\",\n    \"mapsquare\": 10571,\n    \"key\": [\n      1446859333,\n      -1375675929,\n      -760014134,\n      1113381222\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 507,\n    \"name_hash\": -1360985659,\n    \"name\": \"l59_147\",\n    \"mapsquare\": 15251,\n    \"key\": [\n      128349827,\n      -1935626457,\n      -20863709,\n      460738721\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 509,\n    \"name_hash\": -1154187861,\n    \"name\": \"l37_73\",\n    \"mapsquare\": 9545,\n    \"key\": [\n      219605670,\n      414657294,\n      -2055023109,\n      -49887033\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 513,\n    \"name_hash\": -1154187859,\n    \"name\": \"l37_75\",\n    \"mapsquare\": 9547,\n    \"key\": [\n      -157587105,\n      -806602516,\n      1521207142,\n      -727421759\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 525,\n    \"name_hash\": -1152281328,\n    \"name\": \"l59_45\",\n    \"mapsquare\": 15149,\n    \"key\": [\n      -1200628316,\n      15132614,\n      -465260385,\n      -1525542932\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 535,\n    \"name_hash\": -1369297320,\n    \"name\": \"l50_154\",\n    \"mapsquare\": 12954,\n    \"key\": [\n      1051871996,\n      1141268606,\n      -1818400310,\n      1415407155\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 537,\n    \"name_hash\": -1152281327,\n    \"name\": \"l59_46\",\n    \"mapsquare\": 15150,\n    \"key\": [\n      -1763056537,\n      1265265926,\n      379787904,\n      598675540\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 541,\n    \"name_hash\": -1152281326,\n    \"name\": \"l59_47\",\n    \"mapsquare\": 15151,\n    \"key\": [\n      -2030556277,\n      -1247226369,\n      -1858099479,\n      -1486886413\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 543,\n    \"name_hash\": -1394232414,\n    \"name\": \"l44_148\",\n    \"mapsquare\": 11412,\n    \"key\": [\n      -119590785,\n      -577007136,\n      324812522,\n      2041325243\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 545,\n    \"name_hash\": -1394232413,\n    \"name\": \"l44_149\",\n    \"mapsquare\": 11413,\n    \"key\": [\n      -95977595,\n      -2110282911,\n      1926811747,\n      -767376929\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 547,\n    \"name_hash\": -1394232391,\n    \"name\": \"l44_150\",\n    \"mapsquare\": 11414,\n    \"key\": [\n      -460014032,\n      2024565076,\n      -2104679952,\n      -443498989\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 551,\n    \"name_hash\": -1394232389,\n    \"name\": \"l44_152\",\n    \"mapsquare\": 11416,\n    \"key\": [\n      698674506,\n      1358331305,\n      913172931,\n      765272229\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 553,\n    \"name_hash\": -1394232388,\n    \"name\": \"l44_153\",\n    \"mapsquare\": 11417,\n    \"key\": [\n      397843162,\n      248774218,\n      -228700444,\n      -531210715\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 557,\n    \"name_hash\": -1394232386,\n    \"name\": \"l44_155\",\n    \"mapsquare\": 11419,\n    \"key\": [\n      1015780380,\n      -702872379,\n      -62881570,\n      -362521171\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 561,\n    \"name_hash\": -1419167482,\n    \"name\": \"l38_147\",\n    \"mapsquare\": 9875,\n    \"key\": [\n      -266211268,\n      1666415569,\n      -594364895,\n      -521555462\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 575,\n    \"name_hash\": -1419167454,\n    \"name\": \"l38_154\",\n    \"mapsquare\": 9882,\n    \"key\": [\n      1405534886,\n      1766737228,\n      -31518279,\n      -1845608276\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 585,\n    \"name_hash\": -1152489861,\n    \"name\": \"l52_49\",\n    \"mapsquare\": 13361,\n    \"key\": [\n      1703044360,\n      924675594,\n      1161742507,\n      -1174930763\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 587,\n    \"name_hash\": -1152489839,\n    \"name\": \"l52_50\",\n    \"mapsquare\": 13362,\n    \"key\": [\n      1001397515,\n      373830398,\n      1721578513,\n      -710173574\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 589,\n    \"name_hash\": -1152489838,\n    \"name\": \"l52_51\",\n    \"mapsquare\": 13363,\n    \"key\": [\n      -1632087500,\n      -620280348,\n      22360696,\n      1386365966\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 591,\n    \"name_hash\": -1152489837,\n    \"name\": \"l52_52\",\n    \"mapsquare\": 13364,\n    \"key\": [\n      1727399192,\n      -217492117,\n      1391929576,\n      1966780437\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 595,\n    \"name_hash\": -1152489835,\n    \"name\": \"l52_54\",\n    \"mapsquare\": 13366,\n    \"key\": [\n      -1592783139,\n      -1871719576,\n      721348207,\n      67703995\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 599,\n    \"name_hash\": -1152489833,\n    \"name\": \"l52_56\",\n    \"mapsquare\": 13368,\n    \"key\": [\n      -922499048,\n      1143654396,\n      1593518953,\n      1160947087\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 601,\n    \"name_hash\": -1152489832,\n    \"name\": \"l52_57\",\n    \"mapsquare\": 13369,\n    \"key\": [\n      -857763581,\n      -71502428,\n      -1316897039,\n      -786191624\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 603,\n    \"name_hash\": -1152489831,\n    \"name\": \"l52_58\",\n    \"mapsquare\": 13370,\n    \"key\": [\n      -1259912756,\n      -640452721,\n      1196310747,\n      269878509\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 605,\n    \"name_hash\": -1153234638,\n    \"name\": \"l48_47\",\n    \"mapsquare\": 12335,\n    \"key\": [\n      -387260899,\n      -2132243418,\n      654609855,\n      -1092457792\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 607,\n    \"name_hash\": -1152489830,\n    \"name\": \"l52_59\",\n    \"mapsquare\": 13371,\n    \"key\": [\n      -398274614,\n      1285418048,\n      876964091,\n      -1818498048\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 609,\n    \"name_hash\": -1153234637,\n    \"name\": \"l48_48\",\n    \"mapsquare\": 12336,\n    \"key\": [\n      1152832719,\n      1703327384,\n      -664371384,\n      1662584632\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 611,\n    \"name_hash\": -1153234636,\n    \"name\": \"l48_49\",\n    \"mapsquare\": 12337,\n    \"key\": [\n      -750073424,\n      748796796,\n      -1188784945,\n      127988493\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 613,\n    \"name_hash\": -1152489808,\n    \"name\": \"l52_60\",\n    \"mapsquare\": 13372,\n    \"key\": [\n      865568678,\n      -2124731942,\n      491055641,\n      1024391843\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 615,\n    \"name_hash\": -1152489807,\n    \"name\": \"l52_61\",\n    \"mapsquare\": 13373,\n    \"key\": [\n      -2063060546,\n      1762500570,\n      940695155,\n      -1216823417\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 617,\n    \"name_hash\": -1153234614,\n    \"name\": \"l48_50\",\n    \"mapsquare\": 12338,\n    \"key\": [\n      -314247680,\n      -1975556448,\n      815744465,\n      686683170\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 623,\n    \"name_hash\": -1153234611,\n    \"name\": \"l48_53\",\n    \"mapsquare\": 12341,\n    \"key\": [\n      771717693,\n      2109827194,\n      2070396643,\n      121903263\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 629,\n    \"name_hash\": -1153234608,\n    \"name\": \"l48_56\",\n    \"mapsquare\": 12344,\n    \"key\": [\n      1004655261,\n      1112441676,\n      -1346215742,\n      -481727651\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 631,\n    \"name_hash\": -1153383595,\n    \"name\": \"l43_45\",\n    \"mapsquare\": 11053,\n    \"key\": [\n      347789951,\n      -648108134,\n      -480504172,\n      -1661473430\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 633,\n    \"name_hash\": -1153234607,\n    \"name\": \"l48_57\",\n    \"mapsquare\": 12345,\n    \"key\": [\n      -1123116470,\n      -1886379076,\n      75321832,\n      214948598\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 637,\n    \"name_hash\": -1153234606,\n    \"name\": \"l48_58\",\n    \"mapsquare\": 12346,\n    \"key\": [\n      -715432683,\n      -1936681920,\n      -1020543828,\n      1309349058\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 641,\n    \"name_hash\": -1153234605,\n    \"name\": \"l48_59\",\n    \"mapsquare\": 12347,\n    \"key\": [\n      -1936094430,\n      -1392774395,\n      -458238889,\n      -1734657503\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 647,\n    \"name_hash\": -1153234583,\n    \"name\": \"l48_60\",\n    \"mapsquare\": 12348,\n    \"key\": [\n      1498945330,\n      -1007588865,\n      989046743,\n      -1246005481\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 651,\n    \"name_hash\": -1153383569,\n    \"name\": \"l43_50\",\n    \"mapsquare\": 11058,\n    \"key\": [\n      1248059648,\n      -909154233,\n      -2017783073,\n      -1490858018\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 657,\n    \"name_hash\": -1153383566,\n    \"name\": \"l43_53\",\n    \"mapsquare\": 11061,\n    \"key\": [\n      -1835613132,\n      -798666747,\n      2068157339,\n      -2010441483\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 659,\n    \"name_hash\": -1153383565,\n    \"name\": \"l43_54\",\n    \"mapsquare\": 11062,\n    \"key\": [\n      341057778,\n      537106684,\n      410932276,\n      -1693151115\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 661,\n    \"name_hash\": -1153383564,\n    \"name\": \"l43_55\",\n    \"mapsquare\": 11063,\n    \"key\": [\n      230190523,\n      -541989310,\n      756088519,\n      651538143\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 663,\n    \"name_hash\": -1153383563,\n    \"name\": \"l43_56\",\n    \"mapsquare\": 11064,\n    \"key\": [\n      -1650524835,\n      -1814768390,\n      -97991881,\n      -243448484\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 665,\n    \"name_hash\": -1152281325,\n    \"name\": \"l59_48\",\n    \"mapsquare\": 15152,\n    \"key\": [\n      608495786,\n      -1850745649,\n      1481459068,\n      44047411\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 673,\n    \"name_hash\": -1154128366,\n    \"name\": \"l39_49\",\n    \"mapsquare\": 10033,\n    \"key\": [\n      1965668434,\n      1587425243,\n      966917721,\n      2010341526\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 677,\n    \"name_hash\": -1154128343,\n    \"name\": \"l39_51\",\n    \"mapsquare\": 10035,\n    \"key\": [\n      -1407868267,\n      -2023338060,\n      1465071532,\n      -1638236197\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 683,\n    \"name_hash\": -1154128340,\n    \"name\": \"l39_54\",\n    \"mapsquare\": 10038,\n    \"key\": [\n      -1492144861,\n      1907137836,\n      -518005672,\n      990043883\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 685,\n    \"name_hash\": -1367450280,\n    \"name\": \"l52_152\",\n    \"mapsquare\": 13464,\n    \"key\": [\n      -1367805857,\n      1879344111,\n      -360193890,\n      -1649253702\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 695,\n    \"name_hash\": -1153383504,\n    \"name\": \"l43_73\",\n    \"mapsquare\": 11081,\n    \"key\": [\n      -1262440825,\n      -866171020,\n      -669660830,\n      1117529492\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 697,\n    \"name_hash\": -1153383503,\n    \"name\": \"l43_74\",\n    \"mapsquare\": 11082,\n    \"key\": [\n      -1048566284,\n      -1888694855,\n      1387988701,\n      1385807517\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 701,\n    \"name_hash\": -1392385371,\n    \"name\": \"l46_149\",\n    \"mapsquare\": 11925,\n    \"key\": [\n      -302404963,\n      277321688,\n      1579550568,\n      112114901\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 703,\n    \"name_hash\": -1392385349,\n    \"name\": \"l46_150\",\n    \"mapsquare\": 11926,\n    \"key\": [\n      1068449903,\n      -150593625,\n      336172166,\n      -629999737\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 705,\n    \"name_hash\": -1154128280,\n    \"name\": \"l39_72\",\n    \"mapsquare\": 10056,\n    \"key\": [\n      -1242668132,\n      243125991,\n      408826199,\n      -1580449429\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 707,\n    \"name_hash\": -1392385347,\n    \"name\": \"l46_152\",\n    \"mapsquare\": 11928,\n    \"key\": [\n      926476326,\n      -742526377,\n      -1211115608,\n      -1389832255\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 709,\n    \"name_hash\": -1154128279,\n    \"name\": \"l39_73\",\n    \"mapsquare\": 10057,\n    \"key\": [\n      2098955587,\n      1915815888,\n      -1487119512,\n      2000380500\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 711,\n    \"name_hash\": -1392385346,\n    \"name\": \"l46_153\",\n    \"mapsquare\": 11929,\n    \"key\": [\n      1557493911,\n      -1795871852,\n      842977119,\n      -1273925544\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 713,\n    \"name_hash\": -1154128278,\n    \"name\": \"l39_74\",\n    \"mapsquare\": 10058,\n    \"key\": [\n      -1722103890,\n      -365638741,\n      -1142855167,\n      2038282466\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 717,\n    \"name_hash\": -1154128277,\n    \"name\": \"l39_75\",\n    \"mapsquare\": 10059,\n    \"key\": [\n      888791770,\n      446863035,\n      1420300298,\n      1689801625\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 719,\n    \"name_hash\": -1155051771,\n    \"name\": \"l29_81\",\n    \"mapsquare\": 7505,\n    \"key\": [\n      1128826986,\n      -1208716500,\n      1252781850,\n      1529005118\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 733,\n    \"name_hash\": -1154277232,\n    \"name\": \"l34_75\",\n    \"mapsquare\": 8779,\n    \"key\": [\n      -678956803,\n      2085783757,\n      650059580,\n      1450034986\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 735,\n    \"name_hash\": -1154366578,\n    \"name\": \"l31_81\",\n    \"mapsquare\": 8017,\n    \"key\": [\n      1937423379,\n      797654757,\n      -418006613,\n      -1412491464\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 737,\n    \"name_hash\": -1390538330,\n    \"name\": \"l48_148\",\n    \"mapsquare\": 12436,\n    \"key\": [\n      399007290,\n      768017220,\n      1423287782,\n      -1886380141\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 739,\n    \"name_hash\": -1390538329,\n    \"name\": \"l48_149\",\n    \"mapsquare\": 12437,\n    \"key\": [\n      -1441809522,\n      719788383,\n      401674935,\n      735960337\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 743,\n    \"name_hash\": -1390538304,\n    \"name\": \"l48_153\",\n    \"mapsquare\": 12441,\n    \"key\": [\n      -2117208237,\n      -719237621,\n      1574397150,\n      760135387\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 745,\n    \"name_hash\": -1390538303,\n    \"name\": \"l48_154\",\n    \"mapsquare\": 12442,\n    \"key\": [\n      1291079634,\n      -1775973138,\n      -22379340,\n      -1049278279\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 747,\n    \"name_hash\": -1390538302,\n    \"name\": \"l48_155\",\n    \"mapsquare\": 12443,\n    \"key\": [\n      185729295,\n      -1473640650,\n      785055601,\n      1540127286\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 753,\n    \"name_hash\": -1153324012,\n    \"name\": \"l45_46\",\n    \"mapsquare\": 11566,\n    \"key\": [\n      1440870269,\n      1497945697,\n      -1109133632,\n      -2052747028\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 755,\n    \"name_hash\": -1153324011,\n    \"name\": \"l45_47\",\n    \"mapsquare\": 11567,\n    \"key\": [\n      1608156752,\n      653692997,\n      -1552888712,\n      -68059986\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 757,\n    \"name_hash\": -1153324010,\n    \"name\": \"l45_48\",\n    \"mapsquare\": 11568,\n    \"key\": [\n      1434739536,\n      -965821980,\n      1477351656,\n      -1116923723\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 761,\n    \"name_hash\": -1153323987,\n    \"name\": \"l45_50\",\n    \"mapsquare\": 11570,\n    \"key\": [\n      -1161155387,\n      1152701504,\n      -1530532036,\n      -216336018\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 763,\n    \"name_hash\": -1397002979,\n    \"name\": \"l41_146\",\n    \"mapsquare\": 10642,\n    \"key\": [\n      -1862930636,\n      -112759523,\n      -1776313742,\n      286385794\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 771,\n    \"name_hash\": -1397002976,\n    \"name\": \"l41_149\",\n    \"mapsquare\": 10645,\n    \"key\": [\n      2023797650,\n      819145234,\n      -2109392672,\n      1648146732\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 775,\n    \"name_hash\": -1153323982,\n    \"name\": \"l45_55\",\n    \"mapsquare\": 11575,\n    \"key\": [\n      -945127161,\n      -1910820945,\n      -1457442680,\n      -838340972\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 777,\n    \"name_hash\": -1153323981,\n    \"name\": \"l45_56\",\n    \"mapsquare\": 11576,\n    \"key\": [\n      -653186907,\n      1743653193,\n      1590707166,\n      841079490\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 781,\n    \"name_hash\": -1397002953,\n    \"name\": \"l41_151\",\n    \"mapsquare\": 10647,\n    \"key\": [\n      -805201729,\n      122084157,\n      -1599110597,\n      -1319888179\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 785,\n    \"name_hash\": -1153472967,\n    \"name\": \"l40_46\",\n    \"mapsquare\": 10286,\n    \"key\": [\n      153699565,\n      -613131576,\n      -377838747,\n      -1493212656\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 789,\n    \"name_hash\": -1153323979,\n    \"name\": \"l45_58\",\n    \"mapsquare\": 11578,\n    \"key\": [\n      899841295,\n      -1673793442,\n      -444221245,\n      -1412324058\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 793,\n    \"name_hash\": -1397002951,\n    \"name\": \"l41_153\",\n    \"mapsquare\": 10649,\n    \"key\": [\n      -42874604,\n      1755845042,\n      -28694142,\n      -1223794235\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 795,\n    \"name_hash\": -1153323978,\n    \"name\": \"l45_59\",\n    \"mapsquare\": 11579,\n    \"key\": [\n      -1812176752,\n      -317754254,\n      -1196521232,\n      -764437892\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 799,\n    \"name_hash\": -1397002950,\n    \"name\": \"l41_154\",\n    \"mapsquare\": 10650,\n    \"key\": [\n      688979136,\n      1749672831,\n      630260962,\n      1478721348\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 801,\n    \"name_hash\": -1153472964,\n    \"name\": \"l40_49\",\n    \"mapsquare\": 10289,\n    \"key\": [\n      661370115,\n      1720102,\n      -155777581,\n      1516736681\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 803,\n    \"name_hash\": -1154217623,\n    \"name\": \"l36_81\",\n    \"mapsquare\": 9297,\n    \"key\": [\n      -713608515,\n      -67472639,\n      -1826048194,\n      544718437\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 815,\n    \"name_hash\": -1153472938,\n    \"name\": \"l40_54\",\n    \"mapsquare\": 10294,\n    \"key\": [\n      -1453542294,\n      -12979792,\n      -48294325,\n      -2009299224\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 817,\n    \"name_hash\": -1396079429,\n    \"name\": \"l42_154\",\n    \"mapsquare\": 10906,\n    \"key\": [\n      578750625,\n      -1699758530,\n      981674787,\n      -1430062823\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 869,\n    \"name_hash\": -1152460043,\n    \"name\": \"l53_55\",\n    \"mapsquare\": 13623,\n    \"key\": [\n      1573282650,\n      -1418634195,\n      -1148095035,\n      -1144615238\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 871,\n    \"name_hash\": -1152400462,\n    \"name\": \"l55_54\",\n    \"mapsquare\": 14134,\n    \"key\": [\n      -1102558063,\n      817524163,\n      -892019492,\n      -1799662614\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 875,\n    \"name_hash\": -1152430252,\n    \"name\": \"l54_55\",\n    \"mapsquare\": 13879,\n    \"key\": [\n      -489064370,\n      2145976597,\n      -1121899197,\n      1908605309\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 877,\n    \"name_hash\": -1152430254,\n    \"name\": \"l54_53\",\n    \"mapsquare\": 13877,\n    \"key\": [\n      -2102225396,\n      -2039402279,\n      -1361955467,\n      -263013399\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 887,\n    \"name_hash\": -1152430256,\n    \"name\": \"l54_51\",\n    \"mapsquare\": 13875,\n    \"key\": [\n      1430738652,\n      -42941568,\n      -775986981,\n      1000814725\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 889,\n    \"name_hash\": -1152430255,\n    \"name\": \"l54_52\",\n    \"mapsquare\": 13876,\n    \"key\": [\n      -225305190,\n      2101410213,\n      -13023121,\n      617681501\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 891,\n    \"name_hash\": -1396079456,\n    \"name\": \"l42_148\",\n    \"mapsquare\": 10900,\n    \"key\": [\n      305961345,\n      -1339000220,\n      2053506211,\n      -1898778662\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 899,\n    \"name_hash\": -1395155934,\n    \"name\": \"l43_149\",\n    \"mapsquare\": 11157,\n    \"key\": [\n      1464071264,\n      -103928758,\n      1681823375,\n      -1178129652\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 907,\n    \"name_hash\": -1154247442,\n    \"name\": \"l35_74\",\n    \"mapsquare\": 9034,\n    \"key\": [\n      385694264,\n      2091635410,\n      1191598764,\n      -2130030706\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 909,\n    \"name_hash\": -1153353772,\n    \"name\": \"l44_56\",\n    \"mapsquare\": 11320,\n    \"key\": [\n      -1608050593,\n      -413428259,\n      2059848881,\n      -2038252682\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 911,\n    \"name_hash\": -1394232384,\n    \"name\": \"l44_157\",\n    \"mapsquare\": 11421,\n    \"key\": [\n      1786548109,\n      -823015733,\n      1963056537,\n      -1997978489\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 913,\n    \"name_hash\": -1153353771,\n    \"name\": \"l44_57\",\n    \"mapsquare\": 11321,\n    \"key\": [\n      1281968466,\n      -998167738,\n      684914723,\n      -1121178555\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 915,\n    \"name_hash\": -1393308864,\n    \"name\": \"l45_156\",\n    \"mapsquare\": 11676,\n    \"key\": [\n      568565427,\n      1504215833,\n      1399137388,\n      792018103\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 921,\n    \"name_hash\": -1369297347,\n    \"name\": \"l50_148\",\n    \"mapsquare\": 12948,\n    \"key\": [\n      1629198458,\n      696032791,\n      -1386317415,\n      445054408\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 925,\n    \"name_hash\": -1154128281,\n    \"name\": \"l39_71\",\n    \"mapsquare\": 10055,\n    \"key\": [\n      -1765314579,\n      -920999711,\n      703810353,\n      554270505\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 931,\n    \"name_hash\": -1154307113,\n    \"name\": \"l33_48\",\n    \"mapsquare\": 8496,\n    \"key\": [\n      1343253631,\n      1982572295,\n      -1601521319,\n      2107012341\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 933,\n    \"name_hash\": -1154307112,\n    \"name\": \"l33_49\",\n    \"mapsquare\": 8497,\n    \"key\": [\n      2127424498,\n      1622469596,\n      -1889017351,\n      -1927651722\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 935,\n    \"name_hash\": -1154307090,\n    \"name\": \"l33_50\",\n    \"mapsquare\": 8498,\n    \"key\": [\n      -1323684593,\n      -1102390619,\n      1456317314,\n      -2027993898\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 937,\n    \"name_hash\": -1154307089,\n    \"name\": \"l33_51\",\n    \"mapsquare\": 8499,\n    \"key\": [\n      1158999293,\n      2131770333,\n      -861122128,\n      1149793504\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 939,\n    \"name_hash\": -1154277322,\n    \"name\": \"l34_48\",\n    \"mapsquare\": 8752,\n    \"key\": [\n      50858443,\n      684152471,\n      698071810,\n      -175585203\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 941,\n    \"name_hash\": -1154277321,\n    \"name\": \"l34_49\",\n    \"mapsquare\": 8753,\n    \"key\": [\n      1402352663,\n      -1285733714,\n      396097692,\n      -321602106\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 943,\n    \"name_hash\": -1154277299,\n    \"name\": \"l34_50\",\n    \"mapsquare\": 8754,\n    \"key\": [\n      1210656247,\n      128694686,\n      -574675206,\n      -409220905\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 945,\n    \"name_hash\": -1154277298,\n    \"name\": \"l34_51\",\n    \"mapsquare\": 8755,\n    \"key\": [\n      -1165798584,\n      772244190,\n      -402783978,\n      541755309\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 947,\n    \"name_hash\": -1154247531,\n    \"name\": \"l35_48\",\n    \"mapsquare\": 9008,\n    \"key\": [\n      2084752401,\n      -1394447522,\n      2084945842,\n      1474596929\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 949,\n    \"name_hash\": -1154247530,\n    \"name\": \"l35_49\",\n    \"mapsquare\": 9009,\n    \"key\": [\n      259679100,\n      1332627111,\n      610006293,\n      1556652072\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 951,\n    \"name_hash\": -1154247508,\n    \"name\": \"l35_50\",\n    \"mapsquare\": 9010,\n    \"key\": [\n      1975408960,\n      888933152,\n      -1564560503,\n      1102048305\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 953,\n    \"name_hash\": -1154247507,\n    \"name\": \"l35_51\",\n    \"mapsquare\": 9011,\n    \"key\": [\n      -135370293,\n      700991137,\n      -1302596410,\n      -1183796338\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 955,\n    \"name_hash\": -1154247506,\n    \"name\": \"l35_52\",\n    \"mapsquare\": 9012,\n    \"key\": [\n      1204696185,\n      -486414605,\n      -1016110497,\n      134649113\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 957,\n    \"name_hash\": -1154217740,\n    \"name\": \"l36_48\",\n    \"mapsquare\": 9264,\n    \"key\": [\n      1166653059,\n      1243560308,\n      851316348,\n      -2052894389\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 961,\n    \"name_hash\": -1154217717,\n    \"name\": \"l36_50\",\n    \"mapsquare\": 9266,\n    \"key\": [\n      -829139011,\n      1967146062,\n      396751774,\n      842207093\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 963,\n    \"name_hash\": -1154217716,\n    \"name\": \"l36_51\",\n    \"mapsquare\": 9267,\n    \"key\": [\n      245074215,\n      -415791406,\n      175943925,\n      572779390\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 965,\n    \"name_hash\": -1154217654,\n    \"name\": \"l36_71\",\n    \"mapsquare\": 9287,\n    \"key\": [\n      765657606,\n      140057268,\n      871191797,\n      -1188018037\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 969,\n    \"name_hash\": -1394232383,\n    \"name\": \"l44_158\",\n    \"mapsquare\": 11422,\n    \"key\": [\n      1604300407,\n      2105717333,\n      -1276306766,\n      -1021710559\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 971,\n    \"name_hash\": -1152370671,\n    \"name\": \"l56_54\",\n    \"mapsquare\": 14390,\n    \"key\": [\n      -1631704360,\n      1281079278,\n      -1549067757,\n      -2034268623\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 973,\n    \"name_hash\": -1153472936,\n    \"name\": \"l40_56\",\n    \"mapsquare\": 10296,\n    \"key\": [\n      352471788,\n      -1609938193,\n      -266114897,\n      -1904210854\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 977,\n    \"name_hash\": -1153413353,\n    \"name\": \"l42_57\",\n    \"mapsquare\": 10809,\n    \"key\": [\n      -256458821,\n      -522789093,\n      -547929725,\n      1236572696\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 981,\n    \"name_hash\": -1153383562,\n    \"name\": \"l43_57\",\n    \"mapsquare\": 11065,\n    \"key\": [\n      -1487802839,\n      856470179,\n      1889283583,\n      1157865105\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 987,\n    \"name_hash\": -1153472935,\n    \"name\": \"l40_57\",\n    \"mapsquare\": 10297,\n    \"key\": [\n      -465349039,\n      -896523889,\n      319217459,\n      -1898915679\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 991,\n    \"name_hash\": -1153443143,\n    \"name\": \"l41_58\",\n    \"mapsquare\": 10554,\n    \"key\": [\n      1188457126,\n      -1729374406,\n      -799893645,\n      1217674507\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 995,\n    \"name_hash\": -1418243931,\n    \"name\": \"l39_156\",\n    \"mapsquare\": 10140,\n    \"key\": [\n      1403932265,\n      753452087,\n      1731472685,\n      -767209354\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1001,\n    \"name_hash\": -1154158129,\n    \"name\": \"l38_56\",\n    \"mapsquare\": 9784,\n    \"key\": [\n      1874487660,\n      1635569708,\n      -1949316626,\n      -1920350613\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1003,\n    \"name_hash\": -1154158128,\n    \"name\": \"l38_57\",\n    \"mapsquare\": 9785,\n    \"key\": [\n      -65486938,\n      -1485596203,\n      -1327236566,\n      408455718\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1005,\n    \"name_hash\": -1154128337,\n    \"name\": \"l39_57\",\n    \"mapsquare\": 10041,\n    \"key\": [\n      1187862613,\n      -117754960,\n      130411769,\n      -1192628982\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1007,\n    \"name_hash\": -1154277230,\n    \"name\": \"l34_77\",\n    \"mapsquare\": 8781,\n    \"key\": [\n      -1424107598,\n      264936559,\n      -56759783,\n      -902529771\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1011,\n    \"name_hash\": -1154158104,\n    \"name\": \"l38_60\",\n    \"mapsquare\": 9788,\n    \"key\": [\n      432135203,\n      92254316,\n      -1619550296,\n      76065412\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1015,\n    \"name_hash\": -1154128335,\n    \"name\": \"l39_59\",\n    \"mapsquare\": 10043,\n    \"key\": [\n      1912880022,\n      -1723964516,\n      2062562401,\n      -1162129761\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1017,\n    \"name_hash\": -1154128313,\n    \"name\": \"l39_60\",\n    \"mapsquare\": 10044,\n    \"key\": [\n      -1224862407,\n      1865311910,\n      1786772636,\n      18123079\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1023,\n    \"name_hash\": -1153472911,\n    \"name\": \"l40_60\",\n    \"mapsquare\": 10300,\n    \"key\": [\n      1607882720,\n      243924351,\n      1945561299,\n      -898345204\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1025,\n    \"name_hash\": -1153472910,\n    \"name\": \"l40_61\",\n    \"mapsquare\": 10301,\n    \"key\": [\n      -158146381,\n      1698585677,\n      -910760378,\n      815929788\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1031,\n    \"name_hash\": -1153443119,\n    \"name\": \"l41_61\",\n    \"mapsquare\": 10557,\n    \"key\": [\n      -807815903,\n      958215082,\n      -647601710,\n      1350072419\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1033,\n    \"name_hash\": -1153472879,\n    \"name\": \"l40_71\",\n    \"mapsquare\": 10311,\n    \"key\": [\n      1791115,\n      -578135422,\n      542207831,\n      -1947800181\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1035,\n    \"name_hash\": -1396079462,\n    \"name\": \"l42_142\",\n    \"mapsquare\": 10894,\n    \"key\": [\n      513365075,\n      1947090381,\n      355292589,\n      1088691299\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1037,\n    \"name_hash\": -1396079461,\n    \"name\": \"l42_143\",\n    \"mapsquare\": 10895,\n    \"key\": [\n      -444031659,\n      -2140928581,\n      -1156609421,\n      -1171551448\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1039,\n    \"name_hash\": -1153413389,\n    \"name\": \"l42_42\",\n    \"mapsquare\": 10794,\n    \"key\": [\n      311561230,\n      1919568586,\n      -437925589,\n      -1702919060\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1041,\n    \"name_hash\": -1153413388,\n    \"name\": \"l42_43\",\n    \"mapsquare\": 10795,\n    \"key\": [\n      -725090005,\n      -117997344,\n      1120373857,\n      260003830\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1043,\n    \"name_hash\": -1395155941,\n    \"name\": \"l43_142\",\n    \"mapsquare\": 11150,\n    \"key\": [\n      -2076855267,\n      798384451,\n      -78177982,\n      -255258967\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1045,\n    \"name_hash\": -1395155940,\n    \"name\": \"l43_143\",\n    \"mapsquare\": 11151,\n    \"key\": [\n      755494593,\n      -1531347130,\n      657780541,\n      1078696226\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1047,\n    \"name_hash\": -1153383598,\n    \"name\": \"l43_42\",\n    \"mapsquare\": 11050,\n    \"key\": [\n      -2135414572,\n      787126361,\n      -721453549,\n      -544629872\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1049,\n    \"name_hash\": -1153383597,\n    \"name\": \"l43_43\",\n    \"mapsquare\": 11051,\n    \"key\": [\n      -509769132,\n      -2037740887,\n      -214716357,\n      1020989120\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1051,\n    \"name_hash\": -1153324016,\n    \"name\": \"l45_42\",\n    \"mapsquare\": 11562,\n    \"key\": [\n      -353749627,\n      319334308,\n      -1999130422,\n      435767930\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1053,\n    \"name_hash\": -1153472880,\n    \"name\": \"l40_70\",\n    \"mapsquare\": 10310,\n    \"key\": [\n      -1690006852,\n      1771615113,\n      -1889368758,\n      2144832231\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1055,\n    \"name_hash\": -1153443089,\n    \"name\": \"l41_70\",\n    \"mapsquare\": 10566,\n    \"key\": [\n      -1919693075,\n      -1761530062,\n      -740698891,\n      -697859240\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1057,\n    \"name_hash\": -1153443088,\n    \"name\": \"l41_71\",\n    \"mapsquare\": 10567,\n    \"key\": [\n      693572165,\n      -414346822,\n      966267694,\n      -1410395721\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1059,\n    \"name_hash\": -1154187950,\n    \"name\": \"l37_47\",\n    \"mapsquare\": 9519,\n    \"key\": [\n      859829242,\n      1044182946,\n      -1333414478,\n      1254064132\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1069,\n    \"name_hash\": -1154128336,\n    \"name\": \"l39_58\",\n    \"mapsquare\": 10042,\n    \"key\": [\n      736722071,\n      75967325,\n      -775764394,\n      106769937\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1075,\n    \"name_hash\": -1153413297,\n    \"name\": \"l42_71\",\n    \"mapsquare\": 10823,\n    \"key\": [\n      -626951011,\n      -1729338061,\n      -758591782,\n      439072305\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1087,\n    \"name_hash\": -1395155904,\n    \"name\": \"l43_158\",\n    \"mapsquare\": 11166,\n    \"key\": [\n      431616711,\n      -1774935949,\n      -1016388232,\n      -1018749983\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1089,\n    \"name_hash\": -1395155903,\n    \"name\": \"l43_159\",\n    \"mapsquare\": 11167,\n    \"key\": [\n      -1959015492,\n      -1104071,\n      -1016386588,\n      1365103001\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1091,\n    \"name_hash\": -1153383561,\n    \"name\": \"l43_58\",\n    \"mapsquare\": 11066,\n    \"key\": [\n      291008624,\n      -1992710142,\n      -1831766032,\n      1925022398\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1093,\n    \"name_hash\": -1153383560,\n    \"name\": \"l43_59\",\n    \"mapsquare\": 11067,\n    \"key\": [\n      1192664190,\n      -341042618,\n      -820464572,\n      602716622\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1095,\n    \"name_hash\": -1153383538,\n    \"name\": \"l43_60\",\n    \"mapsquare\": 11068,\n    \"key\": [\n      1956851882,\n      1582504141,\n      -1090835806,\n      1413275925\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1097,\n    \"name_hash\": -1153353770,\n    \"name\": \"l44_58\",\n    \"mapsquare\": 11322,\n    \"key\": [\n      -631747543,\n      -196712359,\n      -1853026593,\n      -1746530974\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1101,\n    \"name_hash\": -1153413329,\n    \"name\": \"l42_60\",\n    \"mapsquare\": 10812,\n    \"key\": [\n      90198727,\n      258723745,\n      748197903,\n      -426406231\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1103,\n    \"name_hash\": -1365603237,\n    \"name\": \"l54_153\",\n    \"mapsquare\": 13977,\n    \"key\": [\n      692126287,\n      1949546323,\n      -1604362290,\n      -896929747\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1105,\n    \"name_hash\": -1152400465,\n    \"name\": \"l55_51\",\n    \"mapsquare\": 14131,\n    \"key\": [\n      1016975630,\n      -301160039,\n      -1036603507,\n      114074749\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1107,\n    \"name_hash\": -1397002977,\n    \"name\": \"l41_148\",\n    \"mapsquare\": 10644,\n    \"key\": [\n      -1850188218,\n      -1521064184,\n      1993643055,\n      -554430753\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1109,\n    \"name_hash\": -1396079457,\n    \"name\": \"l42_147\",\n    \"mapsquare\": 10899,\n    \"key\": [\n      997083034,\n      1445099511,\n      -1372563312,\n      -1611746786\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1111,\n    \"name_hash\": -1396079427,\n    \"name\": \"l42_156\",\n    \"mapsquare\": 10908,\n    \"key\": [\n      -1337354744,\n      405036483,\n      751544363,\n      -1235603400\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1113,\n    \"name_hash\": -1395155906,\n    \"name\": \"l43_156\",\n    \"mapsquare\": 11164,\n    \"key\": [\n      1660453528,\n      243788278,\n      -653472483,\n      -435200336\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1115,\n    \"name_hash\": -1395155905,\n    \"name\": \"l43_157\",\n    \"mapsquare\": 11165,\n    \"key\": [\n      -1356346572,\n      -511238576,\n      1759371669,\n      874983287\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1117,\n    \"name_hash\": -1365603235,\n    \"name\": \"l54_155\",\n    \"mapsquare\": 13979,\n    \"key\": [\n      1489416025,\n      -1142280467,\n      1808198885,\n      137033499\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1119,\n    \"name_hash\": -1364679714,\n    \"name\": \"l55_155\",\n    \"mapsquare\": 14235,\n    \"key\": [\n      2114439316,\n      -345963084,\n      -1331796597,\n      275182841\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1121,\n    \"name_hash\": -1364679715,\n    \"name\": \"l55_154\",\n    \"mapsquare\": 14234,\n    \"key\": [\n      237295045,\n      867264725,\n      -1732973561,\n      -1010980224\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1123,\n    \"name_hash\": -1153472902,\n    \"name\": \"l40_69\",\n    \"mapsquare\": 10309,\n    \"key\": [\n      1450418080,\n      -2118438156,\n      -1409971045,\n      -1573015729\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1125,\n    \"name_hash\": -1152370670,\n    \"name\": \"l56_55\",\n    \"mapsquare\": 14391,\n    \"key\": [\n      956732210,\n      1669781219,\n      1907068552,\n      -1303552057\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1129,\n    \"name_hash\": -1152340880,\n    \"name\": \"l57_54\",\n    \"mapsquare\": 14646,\n    \"key\": [\n      -1240067889,\n      -383570142,\n      -558581485,\n      786219832\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1131,\n    \"name_hash\": -1152340879,\n    \"name\": \"l57_55\",\n    \"mapsquare\": 14647,\n    \"key\": [\n      -1420591871,\n      -1781801975,\n      1517176402,\n      -1599286074\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1133,\n    \"name_hash\": -1154187887,\n    \"name\": \"l37_68\",\n    \"mapsquare\": 9540,\n    \"key\": [\n      2133656685,\n      -1448323734,\n      -310319541,\n      -725382856\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1135,\n    \"name_hash\": -1154187886,\n    \"name\": \"l37_69\",\n    \"mapsquare\": 9541,\n    \"key\": [\n      478180212,\n      -332955947,\n      -540535501,\n      2054288254\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1139,\n    \"name_hash\": -1152281297,\n    \"name\": \"l59_55\",\n    \"mapsquare\": 15159,\n    \"key\": [\n      -1251872791,\n      -1612592239,\n      23799253,\n      2059004542\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1141,\n    \"name_hash\": -1389614786,\n    \"name\": \"l49_150\",\n    \"mapsquare\": 12694,\n    \"key\": [\n      1038510617,\n      -584206625,\n      1955760201,\n      602453049\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1145,\n    \"name_hash\": -1154217648,\n    \"name\": \"l36_77\",\n    \"mapsquare\": 9293,\n    \"key\": [\n      -1806573381,\n      -426990884,\n      -1261881306,\n      1666660032\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1147,\n    \"name_hash\": -1154187857,\n    \"name\": \"l37_77\",\n    \"mapsquare\": 9549,\n    \"key\": [\n      1895759075,\n      -843340286,\n      1429492719,\n      295020002\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1149,\n    \"name_hash\": -1154128275,\n    \"name\": \"l39_77\",\n    \"mapsquare\": 10061,\n    \"key\": [\n      -99111554,\n      1883772725,\n      1465462119,\n      1523468364\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1151,\n    \"name_hash\": -1153472873,\n    \"name\": \"l40_77\",\n    \"mapsquare\": 10317,\n    \"key\": [\n      -289613151,\n      -1921022679,\n      1386892226,\n      145070936\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1153,\n    \"name_hash\": -1394232382,\n    \"name\": \"l44_159\",\n    \"mapsquare\": 11423,\n    \"key\": [\n      1414693295,\n      -863955294,\n      -2034093411,\n      1818980885\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1155,\n    \"name_hash\": -1393308862,\n    \"name\": \"l45_158\",\n    \"mapsquare\": 11678,\n    \"key\": [\n      350404025,\n      770298785,\n      1215100269,\n      -965606935\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1165,\n    \"name_hash\": -1152430279,\n    \"name\": \"l54_49\",\n    \"mapsquare\": 13873,\n    \"key\": [\n      -1098656620,\n      -1288920207,\n      1465590763,\n      1004731135\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1167,\n    \"name_hash\": -1153443080,\n    \"name\": \"l41_79\",\n    \"mapsquare\": 10575,\n    \"key\": [\n      -5261221,\n      -598782629,\n      -390496163,\n      -1439990955\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1169,\n    \"name_hash\": -1153413289,\n    \"name\": \"l42_79\",\n    \"mapsquare\": 10831,\n    \"key\": [\n      -1201758536,\n      -1084481964,\n      -461943093,\n      -1283229064\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1173,\n    \"name_hash\": -1153353769,\n    \"name\": \"l44_59\",\n    \"mapsquare\": 11323,\n    \"key\": [\n      -1788142439,\n      -1669893367,\n      1168059730,\n      -14615580\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1179,\n    \"name_hash\": -1390538307,\n    \"name\": \"l48_150\",\n    \"mapsquare\": 12438,\n    \"key\": [\n      1545165254,\n      945298048,\n      244672985,\n      -2058714291\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1181,\n    \"name_hash\": -1390538306,\n    \"name\": \"l48_151\",\n    \"mapsquare\": 12439,\n    \"key\": [\n      737806768,\n      -1467792108,\n      1644942216,\n      -2040381961\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1187,\n    \"name_hash\": -1152549448,\n    \"name\": \"l50_44\",\n    \"mapsquare\": 12844,\n    \"key\": [\n      2093476291,\n      -208179048,\n      86110726,\n      -3825057\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1189,\n    \"name_hash\": -1152549447,\n    \"name\": \"l50_45\",\n    \"mapsquare\": 12845,\n    \"key\": [\n      -1421717474,\n      -733669692,\n      192925225,\n      1604038145\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1193,\n    \"name_hash\": -1152519657,\n    \"name\": \"l51_44\",\n    \"mapsquare\": 13100,\n    \"key\": [\n      -1830823075,\n      1215145937,\n      -525456385,\n      -257417215\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1195,\n    \"name_hash\": -1369297349,\n    \"name\": \"l50_146\",\n    \"mapsquare\": 12946,\n    \"key\": [\n      14919176,\n      -1004409181,\n      -1076064226,\n      261553907\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1197,\n    \"name_hash\": -1368373828,\n    \"name\": \"l51_146\",\n    \"mapsquare\": 13202,\n    \"key\": [\n      1474806838,\n      1338118442,\n      -1146258577,\n      -655181935\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1199,\n    \"name_hash\": -1152519656,\n    \"name\": \"l51_45\",\n    \"mapsquare\": 13101,\n    \"key\": [\n      -1137100843,\n      1099082163,\n      -1318648712,\n      1999295524\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1205,\n    \"name_hash\": -1154247443,\n    \"name\": \"l35_73\",\n    \"mapsquare\": 9033,\n    \"key\": [\n      -340750931,\n      -1897990035,\n      1835646040,\n      1466647905\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1209,\n    \"name_hash\": -1364679718,\n    \"name\": \"l55_151\",\n    \"mapsquare\": 14231,\n    \"key\": [\n      1658757346,\n      -2019485271,\n      -1955365006,\n      895906099\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1211,\n    \"name_hash\": -1154217741,\n    \"name\": \"l36_47\",\n    \"mapsquare\": 9263,\n    \"key\": [\n      -937938813,\n      1777088166,\n      1291736808,\n      1827292190\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1229,\n    \"name_hash\": -1362832672,\n    \"name\": \"l57_155\",\n    \"mapsquare\": 14747,\n    \"key\": [\n      702153225,\n      1229706904,\n      725926382,\n      -1037645198\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1231,\n    \"name_hash\": -1154366608,\n    \"name\": \"l31_72\",\n    \"mapsquare\": 8008,\n    \"key\": [\n      -661798367,\n      -1057089809,\n      -575371589,\n      936390357\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1233,\n    \"name_hash\": -1155051796,\n    \"name\": \"l29_77\",\n    \"mapsquare\": 7501,\n    \"key\": [\n      1108184632,\n      2136992911,\n      -832195021,\n      495111280\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1235,\n    \"name_hash\": -1418243929,\n    \"name\": \"l39_158\",\n    \"mapsquare\": 10142,\n    \"key\": [\n      1872120942,\n      -765842959,\n      449655176,\n      -1168971827\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1237,\n    \"name_hash\": -1419167450,\n    \"name\": \"l38_158\",\n    \"mapsquare\": 9886,\n    \"key\": [\n      -1621103539,\n      1580907615,\n      -2010887870,\n      683082929\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1241,\n    \"name_hash\": -1154396394,\n    \"name\": \"l30_77\",\n    \"mapsquare\": 7757,\n    \"key\": [\n      -1928468960,\n      1136494639,\n      -1967058052,\n      602105509\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1243,\n    \"name_hash\": -1154187833,\n    \"name\": \"l37_80\",\n    \"mapsquare\": 9552,\n    \"key\": [\n      1401891457,\n      -1899495202,\n      -773917688,\n      1780158441\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1245,\n    \"name_hash\": -1154158042,\n    \"name\": \"l38_80\",\n    \"mapsquare\": 9808,\n    \"key\": [\n      -923585668,\n      -574638285,\n      500509980,\n      -960811077\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1247,\n    \"name_hash\": -1154366604,\n    \"name\": \"l31_76\",\n    \"mapsquare\": 8012,\n    \"key\": [\n      -1309713498,\n      82935011,\n      -1681291522,\n      -685742249\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1249,\n    \"name_hash\": -1155051803,\n    \"name\": \"l29_70\",\n    \"mapsquare\": 7494,\n    \"key\": [\n      -462058213,\n      -2060727055,\n      781172327,\n      1942354602\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1251,\n    \"name_hash\": -1154187855,\n    \"name\": \"l37_79\",\n    \"mapsquare\": 9551,\n    \"key\": [\n      1566715405,\n      -1011565479,\n      -979871653,\n      -2050870795\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1255,\n    \"name_hash\": -1154396399,\n    \"name\": \"l30_72\",\n    \"mapsquare\": 7752,\n    \"key\": [\n      -1840222511,\n      -1889016815,\n      -1769816321,\n      1649680459\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1259,\n    \"name_hash\": -1152460072,\n    \"name\": \"l53_47\",\n    \"mapsquare\": 13615,\n    \"key\": [\n      1478333842,\n      -146576907,\n      -285634093,\n      361637473\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1261,\n    \"name_hash\": -1154307019,\n    \"name\": \"l33_79\",\n    \"mapsquare\": 8527,\n    \"key\": [\n      -2043864193,\n      484228359,\n      -673534993,\n      1382486523\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1263,\n    \"name_hash\": -1154306997,\n    \"name\": \"l33_80\",\n    \"mapsquare\": 8528,\n    \"key\": [\n      -1141049958,\n      1626814655,\n      206252937,\n      924426084\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1267,\n    \"name_hash\": -1155051826,\n    \"name\": \"l29_68\",\n    \"mapsquare\": 7492,\n    \"key\": [\n      -342030979,\n      1222986839,\n      511788549,\n      910172876\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1269,\n    \"name_hash\": -1154396424,\n    \"name\": \"l30_68\",\n    \"mapsquare\": 7748,\n    \"key\": [\n      1860227470,\n      -290409954,\n      274550911,\n      -1155783956\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1273,\n    \"name_hash\": -1154396398,\n    \"name\": \"l30_73\",\n    \"mapsquare\": 7753,\n    \"key\": [\n      1896168736,\n      -1223044138,\n      779149869,\n      -1759332197\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1275,\n    \"name_hash\": -1397002954,\n    \"name\": \"l41_150\",\n    \"mapsquare\": 10646,\n    \"key\": [\n      1341282074,\n      345412696,\n      -2043666158,\n      -457216576\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1277,\n    \"name_hash\": -1153353707,\n    \"name\": \"l44_79\",\n    \"mapsquare\": 11343,\n    \"key\": [\n      1133993629,\n      697001923,\n      34692910,\n      944315660\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1279,\n    \"name_hash\": -1153323916,\n    \"name\": \"l45_79\",\n    \"mapsquare\": 11599,\n    \"key\": [\n      858055497,\n      -1049380333,\n      537304023,\n      -1427503453\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1283,\n    \"name_hash\": -1152460073,\n    \"name\": \"l53_46\",\n    \"mapsquare\": 13614,\n    \"key\": [\n      -1661089893,\n      60198635,\n      829308397,\n      -1835163720\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1287,\n    \"name_hash\": -1367450308,\n    \"name\": \"l52_145\",\n    \"mapsquare\": 13457,\n    \"key\": [\n      -445447674,\n      886051920,\n      1838544120,\n      -1401595505\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1289,\n    \"name_hash\": -1367450304,\n    \"name\": \"l52_149\",\n    \"mapsquare\": 13461,\n    \"key\": [\n      -126183647,\n      -25151941,\n      868809662,\n      1423445530\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1291,\n    \"name_hash\": -1389614784,\n    \"name\": \"l49_152\",\n    \"mapsquare\": 12696,\n    \"key\": [\n      1955953194,\n      -1456762745,\n      -1903871849,\n      -1325043037\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1299,\n    \"name_hash\": -1154336841,\n    \"name\": \"l32_69\",\n    \"mapsquare\": 8261,\n    \"key\": [\n      -67002792,\n      1244037268,\n      -1488682033,\n      -924470690\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1301,\n    \"name_hash\": -1153264340,\n    \"name\": \"l47_73\",\n    \"mapsquare\": 12105,\n    \"key\": [\n      1153497516,\n      -449169462,\n      -635419798,\n      -1471828315\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1303,\n    \"name_hash\": -1152489866,\n    \"name\": \"l52_44\",\n    \"mapsquare\": 13356,\n    \"key\": [\n      -62403158,\n      -259993960,\n      1312599048,\n      602862976\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1307,\n    \"name_hash\": -1153204849,\n    \"name\": \"l49_45\",\n    \"mapsquare\": 12589,\n    \"key\": [\n      1867222552,\n      -1279268543,\n      219776982,\n      -1133356510\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1315,\n    \"name_hash\": -1154396393,\n    \"name\": \"l30_78\",\n    \"mapsquare\": 7758,\n    \"key\": [\n      -623741461,\n      -108703380,\n      1436176787,\n      -834745239\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1319,\n    \"name_hash\": -1153294225,\n    \"name\": \"l46_42\",\n    \"mapsquare\": 11818,\n    \"key\": [\n      -2034608097,\n      1147002393,\n      -720701760,\n      -1045127369\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1323,\n    \"name_hash\": -1155111353,\n    \"name\": \"l27_81\",\n    \"mapsquare\": 6993,\n    \"key\": [\n      487810245,\n      -1442562976,\n      -1813756859,\n      201393142\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1325,\n    \"name_hash\": -1155081594,\n    \"name\": \"l28_70\",\n    \"mapsquare\": 7238,\n    \"key\": [\n      -853120516,\n      1697831503,\n      -12188711,\n      770411534\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1329,\n    \"name_hash\": -1392385372,\n    \"name\": \"l46_148\",\n    \"mapsquare\": 11924,\n    \"key\": [\n      1132625310,\n      132067456,\n      -328236024,\n      510687418\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1333,\n    \"name_hash\": -1154158096,\n    \"name\": \"l38_68\",\n    \"mapsquare\": 9796,\n    \"key\": [\n      -392461938,\n      562137949,\n      1796705877,\n      -650819019\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1335,\n    \"name_hash\": -1152400488,\n    \"name\": \"l55_49\",\n    \"mapsquare\": 14129,\n    \"key\": [\n      2079314312,\n      -89031344,\n      495299597,\n      -2042107512\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1337,\n    \"name_hash\": -1152400466,\n    \"name\": \"l55_50\",\n    \"mapsquare\": 14130,\n    \"key\": [\n      1237228106,\n      -349369148,\n      -1811607373,\n      -882226393\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1339,\n    \"name_hash\": -1365603240,\n    \"name\": \"l54_150\",\n    \"mapsquare\": 13974,\n    \"key\": [\n      -606147120,\n      1392509835,\n      -1748925993,\n      -2005068147\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1341,\n    \"name_hash\": -1153353715,\n    \"name\": \"l44_71\",\n    \"mapsquare\": 11335,\n    \"key\": [\n      -133492081,\n      -915358326,\n      -714649892,\n      -2093709476\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1343,\n    \"name_hash\": -1154366602,\n    \"name\": \"l31_78\",\n    \"mapsquare\": 8014,\n    \"key\": [\n      677229402,\n      -1392165236,\n      -1290075373,\n      1127375497\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1347,\n    \"name_hash\": -1154307020,\n    \"name\": \"l33_78\",\n    \"mapsquare\": 8526,\n    \"key\": [\n      88056195,\n      1264510314,\n      -1772486211,\n      1316184148\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1349,\n    \"name_hash\": -1154277229,\n    \"name\": \"l34_78\",\n    \"mapsquare\": 8782,\n    \"key\": [\n      -1516093424,\n      -688279690,\n      -1680379897,\n      -341995244\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1351,\n    \"name_hash\": -1154247438,\n    \"name\": \"l35_78\",\n    \"mapsquare\": 9038,\n    \"key\": [\n      -1589976068,\n      -956669136,\n      615859679,\n      -1852090031\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1353,\n    \"name_hash\": -1154217647,\n    \"name\": \"l36_78\",\n    \"mapsquare\": 9294,\n    \"key\": [\n      123780264,\n      435568451,\n      -2011105306,\n      1736505938\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1355,\n    \"name_hash\": -1154187856,\n    \"name\": \"l37_78\",\n    \"mapsquare\": 9550,\n    \"key\": [\n      1890635758,\n      373622658,\n      194491547,\n      -1040541455\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1357,\n    \"name_hash\": -1153294102,\n    \"name\": \"l46_81\",\n    \"mapsquare\": 11857,\n    \"key\": [\n      -1225499683,\n      -2000595018,\n      -612194825,\n      -2014162349\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1359,\n    \"name_hash\": -1368373800,\n    \"name\": \"l51_153\",\n    \"mapsquare\": 13209,\n    \"key\": [\n      -795447804,\n      1305344381,\n      98581556,\n      2008745861\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1361,\n    \"name_hash\": -1153264311,\n    \"name\": \"l47_81\",\n    \"mapsquare\": 12113,\n    \"key\": [\n      1668148064,\n      1058673313,\n      -1746348692,\n      847341704\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1363,\n    \"name_hash\": -1153234520,\n    \"name\": \"l48_81\",\n    \"mapsquare\": 12369,\n    \"key\": [\n      -832628753,\n      1107609195,\n      262567666,\n      -1274474550\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1369,\n    \"name_hash\": -1153443182,\n    \"name\": \"l41_40\",\n    \"mapsquare\": 10536,\n    \"key\": [\n      -1644243947,\n      2011830533,\n      1900755455,\n      -920681409\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1371,\n    \"name_hash\": -1153443181,\n    \"name\": \"l41_41\",\n    \"mapsquare\": 10537,\n    \"key\": [\n      1496743910,\n      592404003,\n      619983952,\n      616035197\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1373,\n    \"name_hash\": -1154247501,\n    \"name\": \"l35_57\",\n    \"mapsquare\": 9017,\n    \"key\": [\n      -518439975,\n      -1388520826,\n      203536985,\n      1155539914\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1387,\n    \"name_hash\": -1154217710,\n    \"name\": \"l36_57\",\n    \"mapsquare\": 9273,\n    \"key\": [\n      889946754,\n      -775396017,\n      49756200,\n      1934859028\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1391,\n    \"name_hash\": -1152430251,\n    \"name\": \"l54_56\",\n    \"mapsquare\": 13880,\n    \"key\": [\n      -672032867,\n      -104766980,\n      -780173326,\n      -1476119873\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1393,\n    \"name_hash\": -1152400489,\n    \"name\": \"l55_48\",\n    \"mapsquare\": 14128,\n    \"key\": [\n      1907787432,\n      -241593923,\n      1152767085,\n      -541422403\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1395,\n    \"name_hash\": -1152400460,\n    \"name\": \"l55_56\",\n    \"mapsquare\": 14136,\n    \"key\": [\n      -1104533465,\n      1792124817,\n      -1564247177,\n      79706641\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1397,\n    \"name_hash\": -1152370669,\n    \"name\": \"l56_56\",\n    \"mapsquare\": 14392,\n    \"key\": [\n      993603906,\n      -1477196579,\n      593064486,\n      166369140\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1399,\n    \"name_hash\": -1152340878,\n    \"name\": \"l57_56\",\n    \"mapsquare\": 14648,\n    \"key\": [\n      -1757352130,\n      -1996516705,\n      263017190,\n      1647356258\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1401,\n    \"name_hash\": -1152311088,\n    \"name\": \"l58_55\",\n    \"mapsquare\": 14903,\n    \"key\": [\n      -1111240831,\n      1790345264,\n      -366658712,\n      -331374702\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1403,\n    \"name_hash\": -1152311087,\n    \"name\": \"l58_56\",\n    \"mapsquare\": 14904,\n    \"key\": [\n      -1967472485,\n      -192437954,\n      1186759929,\n      -2078112945\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1405,\n    \"name_hash\": -1152281298,\n    \"name\": \"l59_54\",\n    \"mapsquare\": 15158,\n    \"key\": [\n      -974248920,\n      -1426940984,\n      -551894435,\n      1783499939\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1407,\n    \"name_hash\": -1152281296,\n    \"name\": \"l59_56\",\n    \"mapsquare\": 15160,\n    \"key\": [\n      -553466395,\n      -1372948696,\n      -1385390322,\n      710626387\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1409,\n    \"name_hash\": -1153264337,\n    \"name\": \"l47_76\",\n    \"mapsquare\": 12108,\n    \"key\": [\n      399049703,\n      -1446144051,\n      1676451131,\n      -860006347\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1411,\n    \"name_hash\": -1153264309,\n    \"name\": \"l47_83\",\n    \"mapsquare\": 12115,\n    \"key\": [\n      2146362785,\n      1996258091,\n      -613445101,\n      539756591\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1413,\n    \"name_hash\": -1154336814,\n    \"name\": \"l32_75\",\n    \"mapsquare\": 8267,\n    \"key\": [\n      -474856962,\n      -1295973639,\n      789657650,\n      -1769058462\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1415,\n    \"name_hash\": -1154217677,\n    \"name\": \"l36_69\",\n    \"mapsquare\": 9285,\n    \"key\": [\n      -1376572370,\n      -121986361,\n      -1862348009,\n      -378224358\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1417,\n    \"name_hash\": -1153264343,\n    \"name\": \"l47_70\",\n    \"mapsquare\": 12102,\n    \"key\": [\n      -941472423,\n      -2139246656,\n      -735816304,\n      -814413277\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1419,\n    \"name_hash\": -1154396423,\n    \"name\": \"l30_69\",\n    \"mapsquare\": 7749,\n    \"key\": [\n      1999726229,\n      -1872460093,\n      328701683,\n      200119624\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1421,\n    \"name_hash\": -1151625926,\n    \"name\": \"l60_45\",\n    \"mapsquare\": 15405,\n    \"key\": [\n      1745315296,\n      704015968,\n      -1726703759,\n      250744336\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1423,\n    \"name_hash\": -1151625925,\n    \"name\": \"l60_46\",\n    \"mapsquare\": 15406,\n    \"key\": [\n      900232781,\n      1037303290,\n      2099698517,\n      -1839409134\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1425,\n    \"name_hash\": -1151625924,\n    \"name\": \"l60_47\",\n    \"mapsquare\": 15407,\n    \"key\": [\n      -1139819812,\n      165771634,\n      462281145,\n      -1626973480\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1427,\n    \"name_hash\": -1155111376,\n    \"name\": \"l27_79\",\n    \"mapsquare\": 6991,\n    \"key\": [\n      -1448188175,\n      626198836,\n      1985989165,\n      -1062391664\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1429,\n    \"name_hash\": -1155081585,\n    \"name\": \"l28_79\",\n    \"mapsquare\": 7247,\n    \"key\": [\n      -122734679,\n      -1029771956,\n      1633683499,\n      846700218\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1431,\n    \"name_hash\": -1154336850,\n    \"name\": \"l32_60\",\n    \"mapsquare\": 8252,\n    \"key\": [\n      983685450,\n      -269686274,\n      -481846473,\n      958878700\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1433,\n    \"name_hash\": -1154336849,\n    \"name\": \"l32_61\",\n    \"mapsquare\": 8253,\n    \"key\": [\n      369853627,\n      -1436579511,\n      1227029352,\n      -308778567\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1435,\n    \"name_hash\": -1154307059,\n    \"name\": \"l33_60\",\n    \"mapsquare\": 8508,\n    \"key\": [\n      832419339,\n      -645747292,\n      -1840672604,\n      -1762366129\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1437,\n    \"name_hash\": -1154307058,\n    \"name\": \"l33_61\",\n    \"mapsquare\": 8509,\n    \"key\": [\n      1945632596,\n      8962682,\n      613374775,\n      -649841921\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1439,\n    \"name_hash\": -1154277290,\n    \"name\": \"l34_59\",\n    \"mapsquare\": 8763,\n    \"key\": [\n      -161594592,\n      -1041269457,\n      871932542,\n      -908444460\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1441,\n    \"name_hash\": -1421014468,\n    \"name\": \"l36_161\",\n    \"mapsquare\": 9377,\n    \"key\": [\n      -919539010,\n      1637177409,\n      -1552993082,\n      -1871277584\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1443,\n    \"name_hash\": -1154336874,\n    \"name\": \"l32_57\",\n    \"mapsquare\": 8249,\n    \"key\": [\n      -18753106,\n      -1810101920,\n      -1918824309,\n      1059212918\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1445,\n    \"name_hash\": -1154336873,\n    \"name\": \"l32_58\",\n    \"mapsquare\": 8250,\n    \"key\": [\n      -282754239,\n      1300996433,\n      1312184796,\n      1030502655\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1447,\n    \"name_hash\": -1154336872,\n    \"name\": \"l32_59\",\n    \"mapsquare\": 8251,\n    \"key\": [\n      1142567601,\n      1650263019,\n      49659607,\n      -899246535\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1449,\n    \"name_hash\": -1154307083,\n    \"name\": \"l33_57\",\n    \"mapsquare\": 8505,\n    \"key\": [\n      -962353533,\n      -1179227127,\n      370654716,\n      -532704544\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1451,\n    \"name_hash\": -1154307082,\n    \"name\": \"l33_58\",\n    \"mapsquare\": 8506,\n    \"key\": [\n      -1227747958,\n      1491928866,\n      1293676120,\n      2065870654\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1453,\n    \"name_hash\": -1154307081,\n    \"name\": \"l33_59\",\n    \"mapsquare\": 8507,\n    \"key\": [\n      1765022148,\n      -80255262,\n      -725852452,\n      -1286377093\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1455,\n    \"name_hash\": -1154277292,\n    \"name\": \"l34_57\",\n    \"mapsquare\": 8761,\n    \"key\": [\n      1760753095,\n      -1427813077,\n      266675077,\n      -255782653\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1457,\n    \"name_hash\": -1154277291,\n    \"name\": \"l34_58\",\n    \"mapsquare\": 8762,\n    \"key\": [\n      -1868090282,\n      -1544166661,\n      -731821081,\n      1736861342\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1459,\n    \"name_hash\": -1154277268,\n    \"name\": \"l34_60\",\n    \"mapsquare\": 8764,\n    \"key\": [\n      1880374454,\n      1213514336,\n      -2078057600,\n      -2021473694\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1461,\n    \"name_hash\": -1154277267,\n    \"name\": \"l34_61\",\n    \"mapsquare\": 8765,\n    \"key\": [\n      1948979312,\n      134066799,\n      -78879371,\n      -323055687\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1463,\n    \"name_hash\": -1154247499,\n    \"name\": \"l35_59\",\n    \"mapsquare\": 9019,\n    \"key\": [\n      -1293726217,\n      -1696419000,\n      -859948399,\n      1059527576\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1465,\n    \"name_hash\": -1154247477,\n    \"name\": \"l35_60\",\n    \"mapsquare\": 9020,\n    \"key\": [\n      -1906040194,\n      849367381,\n      2098246444,\n      1605043698\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1467,\n    \"name_hash\": -1154247476,\n    \"name\": \"l35_61\",\n    \"mapsquare\": 9021,\n    \"key\": [\n      -665917952,\n      -432697139,\n      -602494723,\n      -2097936281\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1469,\n    \"name_hash\": -1153472973,\n    \"name\": \"l40_40\",\n    \"mapsquare\": 10280,\n    \"key\": [\n      -1245893544,\n      -118079793,\n      -1624599660,\n      -626968532\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1471,\n    \"name_hash\": -1153472972,\n    \"name\": \"l40_41\",\n    \"mapsquare\": 10281,\n    \"key\": [\n      2129832996,\n      -1542613289,\n      -1612399066,\n      -323592936\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1473,\n    \"name_hash\": -1153472971,\n    \"name\": \"l40_42\",\n    \"mapsquare\": 10282,\n    \"key\": [\n      667792020,\n      292003472,\n      -998896147,\n      7080342\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1475,\n    \"name_hash\": -1153413391,\n    \"name\": \"l42_40\",\n    \"mapsquare\": 10792,\n    \"key\": [\n      -125671574,\n      254943715,\n      1343095705,\n      -1965171670\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1477,\n    \"name_hash\": -1153413328,\n    \"name\": \"l42_61\",\n    \"mapsquare\": 10813,\n    \"key\": [\n      78604466,\n      -1582607088,\n      691011324,\n      -1155733133\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1479,\n    \"name_hash\": -1153383537,\n    \"name\": \"l43_61\",\n    \"mapsquare\": 11069,\n    \"key\": [\n      281072607,\n      -1791037221,\n      -1221626554,\n      801634958\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1481,\n    \"name_hash\": -1154158071,\n    \"name\": \"l38_72\",\n    \"mapsquare\": 9800,\n    \"key\": [\n      1648231074,\n      -690523856,\n      538076864,\n      -489360965\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1483,\n    \"name_hash\": -1154307021,\n    \"name\": \"l33_77\",\n    \"mapsquare\": 8525,\n    \"key\": [\n      1448703859,\n      604202172,\n      1058795271,\n      -80742538\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1485,\n    \"name_hash\": -1154217742,\n    \"name\": \"l36_46\",\n    \"mapsquare\": 9262,\n    \"key\": [\n      -1327237404,\n      -257519940,\n      -1398355053,\n      -970581711\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1487,\n    \"name_hash\": -1154187951,\n    \"name\": \"l37_46\",\n    \"mapsquare\": 9518,\n    \"key\": [\n      -557639005,\n      398566091,\n      -133936467,\n      -850662847\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1489,\n    \"name_hash\": -1153472970,\n    \"name\": \"l40_43\",\n    \"mapsquare\": 10283,\n    \"key\": [\n      -2133080221,\n      1327669620,\n      173304076,\n      -151662318\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1491,\n    \"name_hash\": -1153383600,\n    \"name\": \"l43_40\",\n    \"mapsquare\": 11048,\n    \"key\": [\n      1543404580,\n      -1789044122,\n      -804862998,\n      -191419192\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1493,\n    \"name_hash\": -1153353809,\n    \"name\": \"l44_40\",\n    \"mapsquare\": 11304,\n    \"key\": [\n      -1986328953,\n      48921946,\n      1888453159,\n      1692551214\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1495,\n    \"name_hash\": -1153324018,\n    \"name\": \"l45_40\",\n    \"mapsquare\": 11560,\n    \"key\": [\n      1374555285,\n      1132619461,\n      1914865049,\n      300933991\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1497,\n    \"name_hash\": -1153294227,\n    \"name\": \"l46_40\",\n    \"mapsquare\": 11816,\n    \"key\": [\n      1299071975,\n      1506387508,\n      -918208606,\n      -872063692\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1499,\n    \"name_hash\": -1153294226,\n    \"name\": \"l46_41\",\n    \"mapsquare\": 11817,\n    \"key\": [\n      740863947,\n      -1459447536,\n      -1272357052,\n      -1730407126\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1501,\n    \"name_hash\": -1153294224,\n    \"name\": \"l46_43\",\n    \"mapsquare\": 11819,\n    \"key\": [\n      -1632272271,\n      -1134214079,\n      -1204220834,\n      -1633494895\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1503,\n    \"name_hash\": -1153264436,\n    \"name\": \"l47_40\",\n    \"mapsquare\": 12072,\n    \"key\": [\n      -124946691,\n      1378984061,\n      -50235700,\n      -1502078778\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1505,\n    \"name_hash\": -1153264435,\n    \"name\": \"l47_41\",\n    \"mapsquare\": 12073,\n    \"key\": [\n      213992849,\n      1682100343,\n      -1120497450,\n      1116115229\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1507,\n    \"name_hash\": -1153264434,\n    \"name\": \"l47_42\",\n    \"mapsquare\": 12074,\n    \"key\": [\n      542527392,\n      769075547,\n      321487883,\n      1548595060\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1509,\n    \"name_hash\": -1153264433,\n    \"name\": \"l47_43\",\n    \"mapsquare\": 12075,\n    \"key\": [\n      186822346,\n      1877444331,\n      -1687073745,\n      -531331776\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1511,\n    \"name_hash\": -1152460042,\n    \"name\": \"l53_56\",\n    \"mapsquare\": 13624,\n    \"key\": [\n      2075882635,\n      144612382,\n      -11140999,\n      1068051873\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1513,\n    \"name_hash\": -1152460041,\n    \"name\": \"l53_57\",\n    \"mapsquare\": 13625,\n    \"key\": [\n      -1688194808,\n      -751145145,\n      -64310690,\n      -9400824\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1515,\n    \"name_hash\": -1152460040,\n    \"name\": \"l53_58\",\n    \"mapsquare\": 13626,\n    \"key\": [\n      2091552269,\n      -19993197,\n      919014338,\n      -2088295508\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1517,\n    \"name_hash\": -1152460039,\n    \"name\": \"l53_59\",\n    \"mapsquare\": 13627,\n    \"key\": [\n      341129543,\n      405594157,\n      -935930552,\n      1362472171\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1519,\n    \"name_hash\": -1152460017,\n    \"name\": \"l53_60\",\n    \"mapsquare\": 13628,\n    \"key\": [\n      83847925,\n      -1748584368,\n      -1828230097,\n      915424426\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1521,\n    \"name_hash\": -1152460016,\n    \"name\": \"l53_61\",\n    \"mapsquare\": 13629,\n    \"key\": [\n      -1526659316,\n      194744161,\n      -1617203167,\n      1822755797\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1523,\n    \"name_hash\": -1152460015,\n    \"name\": \"l53_62\",\n    \"mapsquare\": 13630,\n    \"key\": [\n      -1632016999,\n      -2015192780,\n      -2137722473,\n      477794767\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1525,\n    \"name_hash\": -1154187862,\n    \"name\": \"l37_72\",\n    \"mapsquare\": 9544,\n    \"key\": [\n      1190108828,\n      -1694672329,\n      922227461,\n      627710655\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1527,\n    \"name_hash\": -1152400464,\n    \"name\": \"l55_52\",\n    \"mapsquare\": 14132,\n    \"key\": [\n      1385901079,\n      -1626256839,\n      2140816760,\n      -1871930940\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1529,\n    \"name_hash\": -1363756198,\n    \"name\": \"l56_150\",\n    \"mapsquare\": 14486,\n    \"key\": [\n      801004021,\n      788873243,\n      -1673638404,\n      152435782\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1531,\n    \"name_hash\": -1363756197,\n    \"name\": \"l56_151\",\n    \"mapsquare\": 14487,\n    \"key\": [\n      1281568133,\n      1646126427,\n      1139545719,\n      -1051778369\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1533,\n    \"name_hash\": -1152370697,\n    \"name\": \"l56_49\",\n    \"mapsquare\": 14385,\n    \"key\": [\n      -338068420,\n      -1043269144,\n      -515762209,\n      327851952\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1535,\n    \"name_hash\": -1152370675,\n    \"name\": \"l56_50\",\n    \"mapsquare\": 14386,\n    \"key\": [\n      -1730596947,\n      85732186,\n      -1776102738,\n      -2096470688\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1537,\n    \"name_hash\": -1152370674,\n    \"name\": \"l56_51\",\n    \"mapsquare\": 14387,\n    \"key\": [\n      -327431537,\n      -279720677,\n      1848501675,\n      306634513\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1539,\n    \"name_hash\": -1152370673,\n    \"name\": \"l56_52\",\n    \"mapsquare\": 14388,\n    \"key\": [\n      1061380274,\n      -429796323,\n      -221565610,\n      -967547362\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1541,\n    \"name_hash\": -1154217646,\n    \"name\": \"l36_79\",\n    \"mapsquare\": 9295,\n    \"key\": [\n      -128031646,\n      -1131908397,\n      -1360861665,\n      1112176197\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1543,\n    \"name_hash\": -1154396370,\n    \"name\": \"l30_80\",\n    \"mapsquare\": 7760,\n    \"key\": [\n      2042192998,\n      743878512,\n      -1804236758,\n      -1160501924\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1545,\n    \"name_hash\": -1396079428,\n    \"name\": \"l42_155\",\n    \"mapsquare\": 10907,\n    \"key\": [\n      1703225431,\n      -1175749730,\n      1842579622,\n      -1861290070\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1547,\n    \"name_hash\": -1155051797,\n    \"name\": \"l29_76\",\n    \"mapsquare\": 7500,\n    \"key\": [\n      -1016974560,\n      452375550,\n      1138391999,\n      85568702\n    ]\n  },\n  {\n    \"archive\": 5,\n    \"group\": 1549,\n    \"name_hash\": -1154396367,\n    \"name\": \"l30_83\",\n    \"mapsquare\": 7763,\n    \"key\": [\n      -170495210,\n      -1183252846,\n      1190019697,\n      650832746\n    ]\n  }\n]"
  },
  {
    "path": "data/saves/.gitkeep",
    "content": ""
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  runejs_game_server:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    volumes:\n      - ./data:/usr/src/app/data\n      - ./cache:/usr/src/app/cache\n      - ./config:/usr/src/app/config\n    ports:\n      - \"43594:43594\"\n\nnetworks:\n  default:\n    name: runejs_network\n"
  },
  {
    "path": "jest.config.ts",
    "content": "/*\n * For a detailed explanation regarding each configuration property and type check, visit:\n * https://jestjs.io/docs/configuration\n */\n\nexport default {\n    // All imported modules in your tests should be mocked automatically\n    // automock: false,\n\n    // Stop running tests after `n` failures\n    // bail: 0,\n\n    // The directory where Jest should store its cached dependency information\n    // cacheDirectory: \"C:\\\\Users\\\\james\\\\AppData\\\\Local\\\\Temp\\\\jest\",\n\n    // Automatically clear mock calls, instances, contexts and results before every test\n    clearMocks: true,\n\n    // Indicates whether the coverage information should be collected while executing the test\n    collectCoverage: true,\n\n    // An array of glob patterns indicating a set of files for which coverage information should be collected\n    // collectCoverageFrom: undefined,\n\n    // The directory where Jest should output its coverage files\n    coverageDirectory: 'coverage',\n\n    // An array of regexp pattern strings used to skip coverage collection\n    // coveragePathIgnorePatterns: [\n    //   \"\\\\\\\\node_modules\\\\\\\\\"\n    // ],\n    roots: ['<rootDir>/src'],\n\n    moduleNameMapper: {\n        '@engine/(.*)': '<rootDir>/src/engine/$1',\n        '@server/(.*)': '<rootDir>/src/server/$1',\n        '@plugins/(.*)': '<rootDir>/src/plugins/$1',\n    },\n\n    // Indicates which provider should be used to instrument code for coverage\n    // coverageProvider: \"babel\",\n\n    // A list of reporter names that Jest uses when writing coverage reports\n    // coverageReporters: [\n    //   \"json\",\n    //   \"text\",\n    //   \"lcov\",\n    //   \"clover\"\n    // ],\n\n    // An object that configures minimum threshold enforcement for coverage results\n    // coverageThreshold: undefined,\n\n    // A path to a custom dependency extractor\n    // dependencyExtractor: undefined,\n\n    // Make calling deprecated APIs throw helpful error messages\n    // errorOnDeprecated: false,\n\n    // The default configuration for fake timers\n    // fakeTimers: {\n    //   \"enableGlobally\": false\n    // },\n\n    // Force coverage collection from ignored files using an array of glob patterns\n    // forceCoverageMatch: [],\n\n    // A path to a module which exports an async function that is triggered once before all test suites\n    // globalSetup: undefined,\n\n    // A path to a module which exports an async function that is triggered once after all test suites\n    // globalTeardown: undefined,\n\n    // A set of global variables that need to be available in all test environments\n    // globals: {},\n\n    // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.\n    // maxWorkers: \"50%\",\n\n    // An array of directory names to be searched recursively up from the requiring module's location\n    // moduleDirectories: [\n    //   \"node_modules\"\n    // ],\n\n    // An array of file extensions your modules use\n    // moduleFileExtensions: [\n    //   \"js\",\n    //   \"mjs\",\n    //   \"cjs\",\n    //   \"jsx\",\n    //   \"ts\",\n    //   \"tsx\",\n    //   \"json\",\n    //   \"node\"\n    // ],\n\n    // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module\n    // moduleNameMapper: {},\n\n    // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader\n    // modulePathIgnorePatterns: [],\n\n    // Activates notifications for test results\n    // notify: false,\n\n    // An enum that specifies notification mode. Requires { notify: true }\n    // notifyMode: \"failure-change\",\n\n    // A preset that is used as a base for Jest's configuration\n    preset: 'ts-jest',\n\n    // Run tests from one or more projects\n    // projects: undefined,\n\n    // Use this configuration option to add custom reporters to Jest\n    // reporters: undefined,\n\n    // Automatically reset mock state before every test\n    // resetMocks: false,\n\n    // Reset the module registry before running each individual test\n    // resetModules: false,\n\n    // A path to a custom resolver\n    // resolver: undefined,\n\n    // Automatically restore mock state and implementation before every test\n    // restoreMocks: false,\n\n    // The root directory that Jest should scan for tests and modules within\n    // rootDir: undefined,\n\n    // A list of paths to directories that Jest should use to search for files in\n    // roots: [\n    //   \"<rootDir>\"\n    // ],\n\n    // Allows you to use a custom runner instead of Jest's default test runner\n    // runner: \"jest-runner\",\n\n    // The paths to modules that run some code to configure or set up the testing environment before each test\n    // setupFiles: [],\n\n    // A list of paths to modules that run some code to configure or set up the testing framework before each test\n    // setupFilesAfterEnv: [],\n\n    // The number of seconds after which a test is considered as slow and reported as such in the results.\n    // slowTestThreshold: 5,\n\n    // A list of paths to snapshot serializer modules Jest should use for snapshot testing\n    // snapshotSerializers: [],\n\n    // The test environment that will be used for testing\n    testEnvironment: 'node',\n\n    // Options that will be passed to the testEnvironment\n    // testEnvironmentOptions: {},\n\n    // Adds a location field to test results\n    // testLocationInResults: false,\n\n    // The glob patterns Jest uses to detect test files\n    testMatch: ['**/*.test.ts'],\n\n    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped\n    // testPathIgnorePatterns: [\n    //   \"\\\\\\\\node_modules\\\\\\\\\"\n    // ],\n\n    // The regexp pattern or array of patterns that Jest uses to detect test files\n    // testRegex: [],\n\n    // This option allows the use of a custom results processor\n    // testResultsProcessor: undefined,\n\n    // This option allows use of a custom test runner\n    // testRunner: \"jest-circus/runner\",\n\n    // A map from regular expressions to paths to transformers\n    // transform: undefined,\n\n    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation\n    // transformIgnorePatterns: [\n    //   \"\\\\\\\\node_modules\\\\\\\\\",\n    //   \"\\\\.pnp\\\\.[^\\\\\\\\]+$\"\n    // ],\n\n    // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them\n    // unmockedModulePathPatterns: undefined,\n\n    // Indicates whether each individual test should be reported during the run\n    // verbose: undefined,\n\n    // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode\n    // watchPathIgnorePatterns: [],\n\n    // Whether to use watchman for file crawling\n    // watchman: true,\n};\n"
  },
  {
    "path": "nodemon.json",
    "content": "{\n    \"verbose\": true,\n    \"ignore\": [\"src/plugins/\"],\n    \"watch\": [\"src/\"],\n    \"ext\": \"ts, js\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"@runejs/server\",\n    \"version\": \"1.0.0-alpha.3\",\n    \"description\": \"A RuneScape game server emulator written in TypeScript.\",\n    \"main\": \"dist/index.js\",\n    \"scripts\": {\n        \"start\": \"npm run build && concurrently \\\"npm run build:watch\\\" \\\"npm run start:infra\\\" \\\"npm run start:game\\\"\",\n        \"start:game\": \"nodemon --delay 5000ms --max-old-space-size=2048 dist/server/runner.js -- -game\",\n        \"start:game:dev\": \"npm run build && concurrently \\\"npm run build:watch\\\" \\\"npm run start:game\\\"\",\n        \"start:login\": \"node --max-old-space-size=1024 dist/server/runner.js -- -login\",\n        \"start:update\": \"node --max-old-space-size=1024 dist/server/runner.js -- -update\",\n        \"start:infra\": \"concurrently \\\"npm run start:update\\\" \\\"npm run start:login\\\"\",\n        \"start:standalone\": \"concurrently \\\"npm run start:infra\\\" \\\"npm run start:game\\\"\",\n        \"game\": \"npm run start:game\",\n        \"game:dev\": \"npm run start:game:dev\",\n        \"login\": \"npm run start:login\",\n        \"update\": \"npm run start:update\",\n        \"infra\": \"npm run start:infra\",\n        \"standalone\": \"npm run start:standalone\",\n        \"build\": \"rimraf dist && swc ./src -d dist --strip-leading-paths\",\n        \"build:watch\": \"swc ./src -d dist -w --strip-leading-paths\",\n        \"lint\": \"biome lint\",\n        \"lint:fin\": \"biome lint --write --diagnostic-level=error --reporter=summary\",\n        \"lint:fix\": \"biome lint --write\",\n        \"format\": \"biome format\",\n        \"format:fin\": \"biome format --write --reporter=summary\",\n        \"format:fix\": \"biome format --write\",\n        \"fin\": \"npm run typecheck && npm run lint:fin && npm run format:fin && npm run test:fin\",\n        \"test\": \"jest\",\n        \"test:fin\": \"jest --silent --reporters=\\\"summary\\\"\",\n        \"typecheck\": \"tsc -p ./ --noEmit\"\n    },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+ssh://git@github.com/runejs/server.git\"\n    },\n    \"keywords\": [\n        \"runejs\",\n        \"runescape\",\n        \"typescript\",\n        \"game server\",\n        \"game engine\"\n    ],\n    \"author\": \"Tynarus\",\n    \"license\": \"GPL-3.0\",\n    \"bugs\": {\n        \"url\": \"https://github.com/runejs/server/issues\"\n    },\n    \"homepage\": \"https://github.com/runejs/server#readme\",\n    \"dependencies\": {\n        \"@runejs/common\": \"2.0.2-beta.3\",\n        \"@runejs/filestore\": \"0.17.0\",\n        \"@runejs/login-server\": \"2.1.0\",\n        \"@runejs/update-server\": \"1.4.0\",\n        \"js-yaml\": \"^4.1.0\",\n        \"json5\": \"^2.2.3\",\n        \"lodash\": \"^4.17.21\",\n        \"quadtree-lib\": \"^1.0.9\",\n        \"rxjs\": \"^7.8.1\",\n        \"source-map-support\": \"^0.5.21\",\n        \"tslib\": \"^2.8.1\",\n        \"uuid\": \"^11.0.5\"\n    },\n    \"devDependencies\": {\n        \"@biomejs/biome\": \"1.9.4\",\n        \"@swc/cli\": \"^0.8.1\",\n        \"@swc/core\": \"^1.10.9\",\n        \"@types/jest\": \"^29.5.14\",\n        \"@types/js-yaml\": \"^4.0.9\",\n        \"@types/lodash\": \"^4.17.14\",\n        \"@types/node\": \"^22.10.8\",\n        \"@types/uuid\": \"^10.0.0\",\n        \"chokidar\": \"^5.0.0\",\n        \"concurrently\": \"^9.1.2\",\n        \"jest\": \"^29.7.0\",\n        \"nodemon\": \"^3.1.9\",\n        \"rimraf\": \"^6.0.1\",\n        \"ts-jest\": \"^29.4.9\",\n        \"ts-node\": \"^10.9.2\",\n        \"tsconfig-paths\": \"^4.2.0\",\n        \"typescript\": \"^5.7.3\"\n    }\n}\n"
  },
  {
    "path": "src/engine/action/action-pipeline.ts",
    "content": "import type { ActionHook } from '@engine/action/hook/action-hook';\nimport { TaskExecutor } from '@engine/action/hook/task';\nimport type { Actor } from '@engine/world/actor/actor';\nimport { isPlayer } from '@engine/world/actor/util';\nimport { logger } from '@runejs/common';\nimport type { Subscription } from 'rxjs';\n\n/**\n * The priority of an queueable action within the pipeline.\n */\nexport type ActionStrength = 'weak' | 'normal' | 'strong';\n\n/**\n * Content action type definitions.\n */\nexport type ActionType =\n    | 'button'\n    | 'widget_interaction'\n    | 'npc_init'\n    | 'npc_interaction'\n    | 'object_interaction'\n    | 'item_interaction'\n    | 'item_on_object'\n    | 'item_on_npc'\n    | 'item_on_player'\n    | 'item_on_item'\n    | 'item_on_world_item'\n    | 'item_swap'\n    | 'move_item'\n    | 'spawned_item_interaction'\n    | 'magic_on_item'\n    | 'magic_on_player'\n    | 'magic_on_npc'\n    | 'player_init'\n    | 'player_command'\n    | 'player_interaction'\n    | 'region_change'\n    | 'equipment_change'\n    | 'prayer';\n\nexport const gentleActions: ActionType[] = [\n    'button',\n    'widget_interaction',\n    'player_init',\n    'npc_init',\n    'move_item',\n    'item_swap',\n    'player_command',\n    'region_change',\n];\n\n/**\n * Methods in which action hooks in progress may be cancelled.\n */\nexport type ActionCancelType = 'manual-movement' | 'pathing-movement' | 'generic' | 'keep-widgets-open' | 'button' | 'widget';\n\n/**\n * The definition for the actual action pipe handler function.\n */\nexport type ActionPipeHandler = (...args: any[]) => RunnableHooks | void;\n\n/**\n * Basic definition of a game engine action file (.action.ts exports).\n */\nexport type ActionPipe = [ActionType, ActionPipeHandler];\n\n/**\n * A list of filtered hooks for an actor to run.\n */\nexport interface RunnableHooks<T = any> {\n    // The action in progress\n    action: T;\n    // Matching action hooks\n    hooks?: ActionHook[];\n}\n\n/**\n * A specific actor's action pipeline handler.\n * Records action pipes and distributes content actions from the game engine down to execute plugin hooks.\n */\nexport class ActionPipeline {\n    private static pipes = new Map<string, ActionPipeHandler>();\n\n    private runningTasks: TaskExecutor<any>[] = [];\n    private canceling: boolean = false;\n    private movementSubscription: Subscription;\n\n    public constructor(public readonly actor: Actor) {\n        this.movementSubscription = this.actor.walkingQueue.movementQueued$.subscribe(async () => this.cancelRunningTasks());\n    }\n\n    public static getPipe(action: ActionType): ActionPipeHandler | null {\n        return ActionPipeline.pipes.get(action) || null;\n    }\n\n    public static register(action: ActionType, actionPipeHandlerFn: ActionPipeHandler): void {\n        ActionPipeline.pipes.set(action.toString(), actionPipeHandlerFn);\n    }\n\n    public shutdown(): void {\n        this.movementSubscription.unsubscribe();\n    }\n\n    public async call(action: ActionType, ...args: any[]): Promise<void> {\n        const actionHandler = ActionPipeline.pipes.get(action.toString());\n        if (actionHandler) {\n            try {\n                await this.runActionHandler(actionHandler, args);\n            } catch (error) {\n                if (error) {\n                    logger.error(`Error handling action ${action.toString()}`);\n                    logger.error(error);\n                }\n            }\n        }\n    }\n\n    public async cancelRunningTasks(): Promise<void> {\n        if (this.canceling || !this.runningTasks || this.runningTasks.length === 0) {\n            return;\n        }\n\n        this.canceling = true;\n\n        for (const runningTask of this.runningTasks) {\n            if (runningTask.running) {\n                await runningTask.stop();\n            }\n        }\n\n        // Remove all tasks\n        this.runningTasks = [];\n        this.canceling = false;\n    }\n\n    private async runActionHandler(actionHandler: any, args: any[]): Promise<void> {\n        const runnableHooks: RunnableHooks | null | undefined = await actionHandler(...args);\n\n        if (!runnableHooks?.hooks || runnableHooks.hooks.length === 0) {\n            return;\n        }\n\n        for (let i = 0; i < runnableHooks.hooks.length; i++) {\n            const hook = runnableHooks.hooks[i];\n            if (!hook) {\n                continue;\n            }\n\n            // Some actions are non-cancelling\n            if (gentleActions.indexOf(hook.type) === -1) {\n                await this.cancelRunningTasks();\n            }\n\n            await this.runHook(hook, runnableHooks.action);\n            if (!hook.multi) {\n                // If the highest priority hook does not allow other hooks\n                // to run during this same action, then return here to break\n                // out of the loop and complete execution.\n                return;\n            }\n        }\n    }\n\n    private async runHook(actionHook: ActionHook, action: any): Promise<void> {\n        const { handler, task } = actionHook;\n\n        if (task) {\n            // Schedule task-based hook\n            const taskExecutor = new TaskExecutor(this.actor, task, actionHook, action);\n            this.runningTasks.push(taskExecutor);\n\n            // Run the task until complete\n            await taskExecutor.run();\n\n            // Cleanup and remove the task once completed\n            const taskIdx = this.runningTasks.findIndex(task => task.taskId === taskExecutor.taskId);\n            if (taskIdx !== -1) {\n                this.runningTasks.splice(taskIdx, 1);\n            }\n        } else if (handler) {\n            // Run basic hook\n            await handler(action);\n        }\n    }\n\n    public get paused(): boolean {\n        if (isPlayer(this.actor)) {\n            if (this.actor.interfaceState.widgetOpen()) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/engine/action/hook/action-hook.ts",
    "content": "import type { ActionStrength, ActionType } from '@engine/action/action-pipeline';\nimport type { HookTask } from '@engine/action/hook/task';\nimport type { QuestKey } from '@engine/config/quest-config';\nimport { actionHookMap } from '@engine/plugins/loader';\n\n/**\n * Defines a quest requirement for an action hook.\n */\nexport interface QuestRequirement {\n    questId: string;\n    stage?: QuestKey;\n    stages?: number[];\n}\n\n/**\n * Defines a generic extensible game content action hook.\n */\nexport interface ActionHook<A = any, H = any> {\n    // The type of action to perform\n    type: ActionType;\n    // Whether or not this hook will allow other hooks from the same action to queue after it\n    multi?: boolean;\n    // The action's priority over other actions\n    priority?: number;\n    // The strength of the action hook\n    strength?: ActionStrength;\n    // [optional] Quest requirements that must be completed in order to run this hook\n    questRequirement?: QuestRequirement;\n    // [optional] The action function to be performed\n    handler?: H;\n    // [optional] The task to be performed\n    task?: HookTask<A>;\n}\n\n/**\n * Fetches the list of all discovered action hooks of the specified type.\n * @param actionType The Action Type to find the hook for.\n * @param filter [optional] Filter criteria to apply to the returned list.\n */\nexport const getActionHooks = <T extends ActionHook>(actionType: ActionType, filter?: (actionHook: T) => boolean): T[] => {\n    const hooks = actionHookMap[actionType] as T[];\n    if (!hooks || hooks.length === 0) {\n        return [];\n    }\n\n    return filter ? hooks.filter(filter) : hooks;\n};\n\n/**\n * A sorter function that action hooks can be run through.\n * Action hooks will be sorted by those with quest requirements firstly, and the rest thereafter.\n * @param actionHooks The list of hooks to sort.\n */\nexport function sortActionHooks<T = any>(actionHooks: ActionHook<T>[]): ActionHook<T>[] {\n    return actionHooks.sort(actionHook => (actionHook.questRequirement !== undefined ? -1 : 1));\n}\n"
  },
  {
    "path": "src/engine/action/hook/hook-filters.test.ts",
    "content": "import { advancedNumberHookFilter } from './hook-filters';\n\ndescribe('action/hook hook filters', () => {\n    describe('advancedNumberHookFilter', () => {\n        describe('when expected is a number array', () => {\n            const expected = [1, 2, 3];\n\n            describe('when input is in the array', () => {\n                const input = 2;\n\n                test('should return true', () => {\n                    const result = advancedNumberHookFilter(expected, input);\n\n                    expect(result).toEqual(true);\n                });\n            });\n\n            describe('when input is not in the array', () => {\n                const input = 999;\n\n                test('should return false', () => {\n                    const result = advancedNumberHookFilter(expected, input);\n\n                    expect(result).toEqual(false);\n                });\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "src/engine/action/hook/hook-filters.ts",
    "content": "import type { ActionHook } from '@engine/action/hook/action-hook';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport const stringHookFilter = (expected: string | string[], input: string): boolean => {\n    if (Array.isArray(expected)) {\n        if (expected.indexOf(input) === -1) {\n            return false;\n        }\n    } else {\n        if (expected !== input) {\n            return false;\n        }\n    }\n\n    return true;\n};\n\nexport const numberHookFilter = (expected: number | number[], input: number): boolean => {\n    if (Array.isArray(expected)) {\n        if (expected.indexOf(input) === -1) {\n            return false;\n        }\n    } else {\n        if (expected !== input) {\n            return false;\n        }\n    }\n\n    return true;\n};\n\nexport const advancedNumberHookFilter = (\n    expected: number | number[],\n    input: number,\n    options?: string | string[],\n    searchOption?: string,\n): boolean => {\n    if (expected !== undefined) {\n        if (Array.isArray(expected)) {\n            if (expected.indexOf(input) === -1) {\n                return false;\n            }\n        } else {\n            if (expected !== input) {\n                return false;\n            }\n        }\n    }\n\n    if (options !== undefined && searchOption !== undefined) {\n        if (Array.isArray(options)) {\n            return options.indexOf(searchOption) !== -1;\n        } else {\n            return options === searchOption;\n        }\n    } else {\n        return true;\n    }\n};\n\n/**\n * A quest requirement filter for hooks that uses the hook's `questRequirements` object.\n * @param player The player involved with the hook.\n * @param actionHook The action hook definition to filter.\n */\nexport function questHookFilter(player: Player, actionHook: ActionHook): boolean {\n    if (!actionHook.questRequirement) {\n        return true;\n    }\n\n    const questId = actionHook.questRequirement.questId;\n    const playerQuest = player.quests.find(quest => quest.questId === questId);\n    if (!playerQuest) {\n        // @TODO quest requirements\n        return actionHook.questRequirement.stage === 0;\n    }\n\n    if (actionHook.questRequirement.stage === 'complete') {\n        return playerQuest.progress === 'complete';\n    }\n\n    if (typeof playerQuest.progress === 'number') {\n        if (actionHook.questRequirement.stage !== undefined) {\n            if (!numberHookFilter(actionHook.questRequirement.stage, playerQuest.progress)) {\n                return false;\n            }\n        } else if (actionHook.questRequirement.stages !== undefined) {\n            if (!numberHookFilter(actionHook.questRequirement.stages, playerQuest.progress)) {\n                return false;\n            }\n        }\n    }\n\n    return playerQuest.progress === actionHook.questRequirement.stage;\n}\n"
  },
  {
    "path": "src/engine/action/hook/task.ts",
    "content": "import type { Subscription } from 'rxjs';\nimport { lastValueFrom, timer } from 'rxjs';\nimport { v4 } from 'uuid';\n\nimport { logger } from '@runejs/common';\n\nimport type { ActionStrength } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport type { Actor } from '@engine/world/actor/actor';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { isNpc, isPlayer } from '@engine/world/actor/util';\nimport { World } from '@engine/world/world';\n\nexport type TaskSessionData = { [key: string]: any };\n\nexport interface TaskDetails<T> {\n    actor: Actor;\n    player: Player | undefined;\n    npc: Npc | undefined;\n    actionData: T;\n    session: TaskSessionData;\n}\n\nexport interface HookTask<T = any> {\n    canActivate?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => boolean | Promise<boolean>;\n    activate: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | undefined | boolean | Promise<void | undefined | boolean>;\n    onComplete?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | Promise<void>;\n    delay?: number; // # of ticks before execution\n    delayMs?: number; // # of milliseconds before execution\n    interval?: number; // # of ticks between loop intervals (defaults to single run task)\n    intervalMs?: number; // # of milliseconds between loop intervals (defaults to single run task)\n}\n\n// T = current action info (ButtonAction, MoveItemAction, etc)\nexport class TaskExecutor<T> {\n    public readonly taskId = v4();\n    public readonly strength: ActionStrength;\n    public running: boolean = false;\n    public session: TaskSessionData = {}; // a session store to use for the lifetime of the task\n\n    private iteration: number = 0;\n    private intervalSubscription: Subscription;\n\n    public constructor(\n        public readonly actor: Actor,\n        public readonly task: HookTask<T>,\n        public readonly hook: ActionHook,\n        public readonly actionData: T,\n    ) {\n        this.strength = this.hook.strength || 'normal';\n    }\n\n    public async run(): Promise<void> {\n        this.running = true;\n\n        if (!!this.task.delay || !!this.task.delayMs) {\n            await lastValueFrom(timer(this.task.delayMs !== undefined ? this.task.delayMs : this.task.delay! * World.TICK_LENGTH));\n        }\n\n        if (!!this.task.interval || !!this.task.intervalMs) {\n            // Looping execution task\n            const intervalMs = this.task.intervalMs !== undefined ? this.task.intervalMs : this.task.interval! * World.TICK_LENGTH;\n\n            await new Promise<void>(resolve => {\n                this.intervalSubscription = timer(0, intervalMs).subscribe(\n                    async () => {\n                        if (!(await this.execute())) {\n                            this.intervalSubscription?.unsubscribe();\n                            resolve();\n                        }\n                    },\n                    error => {\n                        logger.error(error);\n                        resolve();\n                    },\n                    () => resolve(),\n                );\n            });\n        } else {\n            // Single execution task\n            await this.execute();\n        }\n\n        if (this.running) {\n            await this.stop();\n        }\n    }\n\n    public async execute(): Promise<boolean> {\n        if (!this.actor) {\n            // Actor destroyed, cancel the task\n            return false;\n        }\n\n        if (!(await this.canActivate())) {\n            // Unable to activate the task, cancel\n            return false;\n        }\n\n        if (this.actor.actionPipeline.paused) {\n            // Action paused, continue loop if applicable\n            return true;\n        }\n\n        if (!this.running) {\n            // Task no longer running, cancel execution\n            return false;\n        }\n\n        try {\n            const response = await this.task.activate(this, this.iteration++);\n            return typeof response === 'boolean' ? response : true;\n        } catch (error) {\n            logger.error(`Error executing action task`);\n            logger.error(error);\n            return false;\n        }\n    }\n\n    public async canActivate(): Promise<boolean> {\n        if (!this.valid) {\n            return false;\n        }\n\n        if (!this.task.canActivate) {\n            return true;\n        }\n\n        try {\n            return this.task.canActivate(this, this.iteration);\n        } catch (error) {\n            logger.error(`Error calling action canActivate`, this.task);\n            logger.error(error);\n            return false;\n        }\n    }\n\n    public async stop(): Promise<void> {\n        this.running = false;\n        this.intervalSubscription?.unsubscribe();\n\n        if (this.task?.onComplete) {\n            await this.task.onComplete(this, this.iteration);\n        }\n    }\n\n    public getDetails(): TaskDetails<T> {\n        return {\n            actor: this.actor,\n            player: isPlayer(this.actor) ? this.actor : undefined,\n            npc: isNpc(this.actor) ? this.actor : undefined,\n            actionData: this.actionData,\n            session: this.session,\n        };\n    }\n\n    public get valid(): boolean {\n        return !!this.task?.activate && !!this.actionData;\n    }\n}\n"
  },
  {
    "path": "src/engine/action/loader.ts",
    "content": "import { join } from 'path';\nimport type { ActionPipe } from '@engine/action/action-pipeline';\nimport { ActionPipeline } from '@engine/action/action-pipeline';\nimport { BUILD_DIR } from '@engine/config/directories';\nimport { getFiles } from '@engine/util/files';\nimport { logger } from '@runejs/common';\n\n/**\n * Finds and loads all available action pipe files (`*.action.ts`).\n */\nexport async function loadActionFiles(): Promise<void> {\n    const ACTION_DIRECTORY = join(BUILD_DIR, 'action');\n    const PIPE_DIRECTORY = join(ACTION_DIRECTORY, 'pipe');\n    const blacklist = [];\n    const loadedActions: string[] = [];\n\n    for await (const path of getFiles(PIPE_DIRECTORY, blacklist)) {\n        if (!path.endsWith('.action.ts') && !path.endsWith('.action.js')) {\n            continue;\n        }\n\n        const location = '.' + path.substring(ACTION_DIRECTORY.length).replace('.js', '');\n\n        try {\n            const importedAction = (require(location)?.default || null) as ActionPipe | null;\n            if (importedAction && Array.isArray(importedAction) && importedAction[0] && importedAction[1]) {\n                ActionPipeline.register(importedAction[0], importedAction[1]);\n                loadedActions.push(importedAction[0]);\n            }\n        } catch (error) {\n            logger.error(`Error loading action file at ${location}:`);\n            logger.error(error);\n        }\n    }\n\n    logger.info(`Loaded action pipes: ${loadedActions.join(', ')}.`);\n\n    return Promise.resolve();\n}\n"
  },
  {
    "path": "src/engine/action/pipe/button.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport { type ActionHook, getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a button action hook.\n */\nexport interface ButtonActionHook extends ActionHook<ButtonAction, buttonActionHandler> {\n    // The ID of the UI widget that the button is on.\n    widgetId?: number;\n    // The IDs of the UI widgets that the buttons are on.\n    widgetIds?: number[];\n    // The child ID or list of child IDs of the button(s) within the UI widget.\n    buttonIds?: number | number[];\n    // Whether or not this item action should cancel other running or queued actions.\n    cancelActions?: boolean;\n}\n\n/**\n * The button action hook handler function to be called when the hook's conditions are met.\n */\nexport type buttonActionHandler = (buttonAction: ButtonAction) => void | Promise<void>;\n\n/**\n * Details about a button action being performed.\n */\nexport interface ButtonAction {\n    // The player performing the action.\n    player: Player;\n    // The ID of the UI widget that the button is on.\n    widgetId: number;\n    // The child ID of the button within the UI widget.\n    buttonId: number;\n}\n\n/**\n * The pipe that the game engine hands button actions off to.\n * @param player\n * @param widgetId\n * @param buttonId\n */\nconst buttonActionPipe = (player: Player, widgetId: number, buttonId: number): RunnableHooks<ButtonAction> | null => {\n    let matchingHooks = getActionHooks<ButtonActionHook>('button').filter(\n        plugin =>\n            questHookFilter(player, plugin) &&\n            ((plugin.widgetId && plugin.widgetId === widgetId) ||\n                (plugin.widgetIds && advancedNumberHookFilter(plugin.widgetIds, widgetId))) &&\n            (plugin.buttonIds === undefined || advancedNumberHookFilter(plugin.buttonIds, buttonId)),\n    );\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(`Unhandled button interaction: ${widgetId}:${buttonId}`);\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            widgetId,\n            buttonId,\n        },\n    };\n};\n\n/**\n * Button action pipe definition.\n */\nexport default ['button', buttonActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/equipment-change.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { findItem } from '@engine/config/config-handler';\nimport type { EquipmentSlot, ItemDetails } from '@engine/config/item-config';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\n/**\n * Defines an equipment change action hook.\n */\nexport interface EquipmentChangeActionHook extends ActionHook<EquipmentChangeAction, equipmentChangeActionHandler> {\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds?: number | number[];\n    // A single option name or a list of option names that this action applies to.\n    eventType?: EquipmentChangeType | EquipmentChangeType[];\n}\n\n/**\n * The definition for an equip action function.\n */\nexport type equipmentChangeActionHandler = (equipmentChangeAction: EquipmentChangeAction) => void;\n\n/**\n * Equipment action types.\n */\nexport type EquipmentChangeType = 'equip' | 'unequip';\n\n/**\n * Details about an item being equipped/unequipped.\n */\nexport interface EquipmentChangeAction {\n    // The player performing the action.\n    player: Player;\n    // The ID of the item being equipped/unequipped.\n    itemId: number;\n    // Additional details about the item.\n    itemDetails: ItemDetails;\n    // If the item was equipped or unequipped.\n    eventType: EquipmentChangeType;\n    // The equipment slot.\n    equipmentSlot: EquipmentSlot;\n}\n\n/**\n * The pipe that the game engine hands equipment actions off to.\n * @param player\n * @param itemId\n * @param eventType\n * @param slot\n */\nconst equipmentChangeActionPipe = (\n    player: Player,\n    itemId: number,\n    eventType: EquipmentChangeType,\n    slot: EquipmentSlot,\n): RunnableHooks<EquipmentChangeAction> | null => {\n    let matchingHooks = getActionHooks<EquipmentChangeActionHook>('equipment_change', equipActionHook => {\n        if (!questHookFilter(player, equipActionHook)) {\n            return false;\n        }\n\n        if (equipActionHook.itemIds !== undefined) {\n            if (!numberHookFilter(equipActionHook.itemIds, itemId)) {\n                return false;\n            }\n        }\n\n        if (equipActionHook.eventType !== undefined) {\n            if (!stringHookFilter(equipActionHook.eventType, eventType)) {\n                return false;\n            }\n        }\n        return true;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (!matchingHooks || matchingHooks.length === 0) {\n        return null;\n    }\n\n    const itemDetails = findItem(itemId);\n\n    if (!itemDetails) {\n        logger.error(`Item ${itemId} not registered on the server [equipment-change action pipe]`);\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            itemId,\n            itemDetails,\n            eventType,\n            equipmentSlot: slot,\n        },\n    };\n};\n\n/**\n * Equip action pipe definition.\n */\nexport default ['equipment_change', equipmentChangeActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-interaction.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { findItem } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\n/**\n * Defines an item action hook.\n */\nexport interface ItemInteractionActionHook extends ActionHook<ItemInteractionAction, itemInteractionActionHandler> {\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds?: number | number[];\n    // A single UI widget ID or a list of widget IDs that this action applies to.\n    widgets?: { widgetId: number; containerId: number } | { widgetId: number; containerId: number }[];\n    // A single option name or a list of option names that this action applies to.\n    options?: string | string[];\n    // Whether or not this item action should cancel other running or queued actions.\n    cancelOtherActions?: boolean;\n}\n\n/**\n * The item action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemInteractionActionHandler = (itemInteractionAction: ItemInteractionAction) => void;\n\n/**\n * Details about an item action being performed.\n */\nexport interface ItemInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The ID of the item being interacted with.\n    itemId: number;\n    // The container slot that the item being interacted with is in.\n    itemSlot: number;\n    // The ID of the UI widget that the item is in.\n    widgetId: number;\n    // The ID of the UI container that the item is in.\n    containerId: number;\n    // Additional details about the item.\n    itemDetails: ItemDetails;\n    // The option that the player used (ie \"equip\"  or \"drop\").\n    option: string;\n}\n\n/**\n * The pipe that the game engine hands item actions off to.\n * @param player\n * @param itemId\n * @param slot\n * @param widgetId\n * @param containerId\n * @param option\n */\nconst itemInteractionActionPipe = (\n    player: Player,\n    itemId: number,\n    slot: number,\n    widgetId: number,\n    containerId: number,\n    option: string,\n): RunnableHooks<ItemInteractionAction> | null => {\n    const playerWidget = Object.values(player.interfaceState.widgetSlots).find(widget => widget && widget.widgetId === widgetId);\n\n    if (playerWidget && playerWidget.fakeWidget != undefined) {\n        widgetId = playerWidget.fakeWidget;\n    }\n\n    // Find all object action plugins that reference this location object\n    let matchingHooks = getActionHooks<ItemInteractionActionHook>('item_interaction', plugin => {\n        if (!questHookFilter(player, plugin)) {\n            return false;\n        }\n\n        if (plugin.itemIds !== undefined) {\n            if (!numberHookFilter(plugin.itemIds, itemId)) {\n                return false;\n            }\n        }\n\n        if (plugin.widgets !== undefined) {\n            if (Array.isArray(plugin.widgets)) {\n                let found = false;\n                for (const widget of plugin.widgets) {\n                    if (widget.widgetId === widgetId && widget.containerId === containerId) {\n                        found = true;\n                        break;\n                    }\n                }\n\n                if (!found) {\n                    return false;\n                }\n            } else {\n                if (plugin.widgets.widgetId !== widgetId || plugin.widgets.containerId !== containerId) {\n                    return false;\n                }\n            }\n        }\n\n        if (plugin.options !== undefined) {\n            if (!stringHookFilter(plugin.options, option)) {\n                return false;\n            }\n        }\n        return true;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled item option: ${option} ${itemId} in slot ${slot} within widget ${widgetId}:${containerId}`,\n        );\n        return null;\n    }\n\n    const itemDetails = findItem(itemId);\n\n    if (!itemDetails) {\n        logger.error(`Item ${itemId} not registered on the server [item-interaction action pipe]`);\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            itemId,\n            itemSlot: slot,\n            widgetId,\n            containerId,\n            itemDetails,\n            option,\n        },\n    };\n};\n\n/**\n * Item action pipe definition.\n */\nexport default ['item_interaction', itemInteractionActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-on-item.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { questHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\n\n/**\n * Defines an item-on-item action hook.\n */\nexport interface ItemOnItemActionHook extends ActionHook<ItemOnItemAction, itemOnItemActionHandler> {\n    // The item pairs being used. Each item can be used on the other, so item order does not matter.\n    items: { item1: number; item2?: number }[];\n}\n\n/**\n * The item-on-item action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemOnItemActionHandler = (itemOnItemAction: ItemOnItemAction) => void;\n\n/**\n * Details about an item-on-item action being performed.\n */\nexport interface ItemOnItemAction {\n    // The player performing the action.\n    player: Player;\n    // The item being used.\n    usedItem: Item;\n    // The item that the first item is being used on.\n    usedWithItem: Item;\n    // The container slot that the item being used is in.\n    usedSlot: number;\n    // The container slot that the second item is in.\n    usedWithSlot: number;\n    // The ID of the UI widget that the item being used is in.\n    usedWidgetId: number;\n    // The ID of the UI widget that the second item is in.\n    usedWithWidgetId: number;\n}\n\n/**\n * The pipe that the game engine hands item-on-item actions off to.\n * @param player\n * @param usedItem\n * @param usedSlot\n * @param usedWidgetId\n * @param usedWithItem\n * @param usedWithSlot\n * @param usedWithWidgetId\n */\nconst itemOnItemActionPipe = (\n    player: Player,\n    usedItem: Item,\n    usedSlot: number,\n    usedWidgetId: number,\n    usedWithItem: Item,\n    usedWithSlot: number,\n    usedWithWidgetId: number,\n): RunnableHooks<ItemOnItemAction> | null => {\n    if (player.busy) {\n        return null;\n    }\n\n    // Find all item on item action plugins that match this action\n    let matchingHooks = getActionHooks<ItemOnItemActionHook>('item_on_item', plugin => {\n        if (questHookFilter(player, plugin)) {\n            const used = usedItem.itemId;\n            const usedWith = usedWithItem.itemId;\n\n            return plugin.items.some(({ item1, item2 }) => {\n                if (item2) {\n                    return (item1 === used && item2 === usedWith) || (item1 === usedWith && item2 === used);\n                }\n\n                return item1 === used || item1 === usedWith;\n            });\n        }\n\n        return false;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(`Unhandled item on item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`);\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            usedItem,\n            usedWithItem,\n            usedSlot,\n            usedWithSlot,\n            usedWidgetId: usedWidgetId,\n            usedWithWidgetId: usedWithWidgetId,\n        },\n    };\n};\n\n/**\n * Item-on-item action pipe definition.\n */\nexport default ['item_on_item', itemOnItemActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-on-npc.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport type { Position } from '@engine/world/position';\n\n/**\n * Defines an item-on-npc action hook.\n */\nexport interface ItemOnNpcActionHook extends ActionHook<ItemOnNpcAction, itemOnNpcActionHandler> {\n    // A single npc key or a list of npc keys that this action applies to.\n    npcs: string | string[];\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds: number | number[];\n    // Whether or not the player needs to walk to this NPC before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The item-on-npc action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemOnNpcActionHandler = (itemOnNpcAction: ItemOnNpcAction) => void;\n\n/**\n * Details about an item-on-npc action being performed.\n */\nexport interface ItemOnNpcAction {\n    // The player performing the action.\n    player: Player;\n    // The NPC the action is being performed on.\n    npc: Npc;\n    // The position that the NPC was at when the action was initiated.\n    position: Position;\n    // The item being used.\n    item: Item;\n    // The ID of the UI widget that the item being used is in.\n    itemWidgetId: number;\n    // The ID of the UI container that the item being used is in.\n    itemContainerId: number;\n}\n\n/**\n * The pipe that the game engine hands item-on-npc actions off to.\n * @param player\n * @param npc\n * @param position\n * @param item\n * @param itemWidgetId\n * @param itemContainerId\n */\nconst itemOnNpcActionPipe = (\n    player: Player,\n    npc: Npc,\n    position: Position,\n    item: Item,\n    itemWidgetId: number,\n    itemContainerId: number,\n): RunnableHooks<ItemOnNpcAction> | null => {\n    const morphedNpc = player.getMorphedNpcDetails(npc);\n\n    // Find all item on npc action plugins that reference this npc and item\n    let matchingHooks = getActionHooks<ItemOnNpcActionHook>('item_on_npc').filter(\n        plugin =>\n            questHookFilter(player, plugin) &&\n            stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key) &&\n            advancedNumberHookFilter(plugin.itemIds, item.itemId),\n    );\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled item on npc interaction: ${item.itemId} on ${morphedNpc?.name || npc.name} ` +\n                `(id-${morphedNpc?.gameId || npc.id}) @ ${position.x},${position.y},${position.level}`,\n        );\n        if (morphedNpc) {\n            player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`);\n        }\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(\n            new WalkToActorPluginTask(walkToPlugins, player, 'npc', npc, {\n                item,\n                itemWidgetId,\n                itemContainerId,\n            }),\n        );\n\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            npc,\n            position,\n            item,\n            itemWidgetId,\n            itemContainerId,\n        },\n    };\n};\n\n/**\n * Item-on-npc action pipe definition.\n */\nexport default ['item_on_npc', itemOnNpcActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-on-object.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToObjectPluginTask } from '@engine/action/pipe/task/walk-to-object-plugin-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport type { Position } from '@engine/world/position';\nimport type { LandscapeObject, ObjectConfig } from '@runejs/filestore';\n\n/**\n * Defines an item-on-object action hook.\n */\nexport interface ItemOnObjectActionHook extends ActionHook<ItemOnObjectAction, itemOnObjectActionHandler> {\n    // A single game object ID or a list of object IDs that this action applies to.\n    objectIds: number | number[];\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds: number | number[];\n    // Whether or not the player needs to walk to this object before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The item-on-object action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemOnObjectActionHandler = (itemOnObjectAction: ItemOnObjectAction) => void;\n\n/**\n * Details about an item-on-object action being performed.\n */\nexport interface ItemOnObjectAction {\n    // The player performing the action.\n    player: Player;\n    // The object the action is being performed on.\n    object: LandscapeObject;\n    // Additional details about the object that the action is being performed on.\n    objectConfig: ObjectConfig;\n    // The position that the game object was at when the action was initiated.\n    position: Position;\n    // The item being used.\n    item: Item;\n    // The ID of the UI widget that the item being used is in.\n    itemWidgetId: number;\n    // The ID of the UI container that the item being used is in.\n    itemContainerId: number;\n    // Whether or not this game object is an original map object or if it has been added/replaced.\n    cacheOriginal: boolean;\n}\n\n/**\n * The pipe that the game engine hands item-on-object actions off to.\n * @param player\n * @param landscapeObject\n * @param objectConfig\n * @param position\n * @param item\n * @param itemWidgetId\n * @param itemContainerId\n * @param cacheOriginal\n */\nconst itemOnObjectActionPipe = (\n    player: Player,\n    landscapeObject: LandscapeObject,\n    objectConfig: ObjectConfig,\n    position: Position,\n    item: Item,\n    itemWidgetId: number,\n    itemContainerId: number,\n    cacheOriginal: boolean,\n): RunnableHooks<ItemOnObjectAction> | null => {\n    // Find all item on object action plugins that reference this location object\n    let matchingHooks = getActionHooks<ItemOnObjectActionHook>('item_on_object').filter(\n        plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId),\n    );\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    // Find all item on object action plugins that reference this item\n    if (matchingHooks.length !== 0) {\n        matchingHooks = matchingHooks.filter(plugin => advancedNumberHookFilter(plugin.itemIds, item.itemId));\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled item on object interaction: ${item.itemId} on ${objectConfig.name} ` +\n                `(id-${landscapeObject.objectId}) @ ${position.x},${position.y},${position.level}`,\n        );\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(\n            new WalkToObjectPluginTask<ItemOnObjectAction>(walkToPlugins, player, landscapeObject, {\n                objectConfig,\n                item,\n                itemWidgetId,\n                itemContainerId,\n                cacheOriginal,\n            }),\n        );\n\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            object: landscapeObject,\n            objectConfig,\n            position,\n            item,\n            itemWidgetId,\n            itemContainerId,\n            cacheOriginal,\n        },\n    };\n};\n\n/**\n * Item-on-object action pipe definition.\n */\nexport default ['item_on_object', itemOnObjectActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-on-player.action.ts",
    "content": "import type { ActionPipe } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport type { Position } from '@engine/world/position';\n\n/**\n * Defines an item-on-player action hook.\n */\nexport interface ItemOnPlayerActionHook extends ActionHook<itemOnPlayerActionHandler> {\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds: number | number[];\n    // Whether or not the player needs to walk to this Player before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The item-on-player action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemOnPlayerActionHandler = (itemOnPlayerAction: ItemOnPlayerAction) => void;\n\n/**\n * Details about an item-on-player action being performed.\n */\nexport interface ItemOnPlayerAction {\n    // The player performing the action.\n    player: Player;\n    // The player the action is being performed on.\n    otherPlayer: Player;\n    // The position that the Player was at when the action was initiated.\n    position: Position;\n    // The item being used.\n    item: Item;\n    // The ID of the UI widget that the item being used is in.\n    itemWidgetId: number;\n    // The ID of the UI container that the item being used is in.\n    itemContainerId: number;\n}\n\n// @TODO update\n/**\n * The pipe that the game engine hands item-on-player actions off to.\n * @param player\n * @param otherPlayer\n * @param position\n * @param item\n * @param itemWidgetId\n * @param itemContainerId\n */\nconst itemOnPlayerActionPipe = (\n    player: Player,\n    otherPlayer: Player,\n    position: Position,\n    item: Item,\n    itemWidgetId: number,\n    itemContainerId: number,\n): void => {\n    if (player.busy) {\n        return;\n    }\n\n    // Find all item on player action plugins that reference this item\n    let interactionActions = getActionHooks<ItemOnPlayerActionHook>('item_on_player').filter(\n        plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.itemIds, item.itemId),\n    );\n    const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        interactionActions = questActions;\n    }\n\n    if (interactionActions.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled item on player interaction: ${item.itemId} ` + `@ ${position.x},${position.y},${position.level}`,\n        );\n        return;\n    }\n\n    // Separate out walk-to actions from immediate actions\n    const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo);\n    const immediateHooks = interactionActions.filter(plugin => !plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(\n            new WalkToActorPluginTask(walkToPlugins, player, 'otherPlayer', otherPlayer, {\n                item,\n                itemWidgetId,\n                itemContainerId,\n            }),\n        );\n\n        return;\n    }\n\n    // Immediately run any non-walk-to plugins\n    for (const actionHook of immediateHooks) {\n        actionHook.handler({\n            player,\n            otherPlayer,\n            position,\n            item,\n            itemWidgetId,\n            itemContainerId,\n        });\n    }\n};\n\n/**\n * Item-on-player action pipe definition.\n */\nexport default ['item_on_player', itemOnPlayerActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-on-world-item.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { questHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport type { WorldItem } from '@engine/world/items/world-item';\n\n/**\n * Defines an item-on-world-item action hook.\n *\n * @author jameskmonger\n */\nexport interface ItemOnWorldItemActionHook extends ActionHook<ItemOnWorldItemAction, itemOnWorldItemActionHandler> {\n    /**\n     * The item pairs being used. Both items are optional so that you can specify a single item, a pair of items, or neither.\n     */\n    items: { item?: number; worldItem?: number }[];\n}\n\n/**\n * The item-on-world-item action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemOnWorldItemActionHandler = (itemOnWorldItemAction: ItemOnWorldItemAction) => void;\n\n/**\n * Details about an item-on-world-item action being performed.\n *\n * @author jameskmonger\n */\nexport interface ItemOnWorldItemAction {\n    /**\n     * The player performing the action.\n     */\n    player: Player;\n\n    /**\n     * The item being used.\n     */\n    usedItem: Item;\n\n    /**\n     * The WorldItem that the first item is being used on.\n     */\n    usedWithItem: WorldItem;\n\n    /**\n     * The ID of the UI widget that the item being used is in.\n     */\n    usedWidgetId: number;\n\n    /**\n     * The ID of the container that the item being used is in.\n     */\n    usedContainerId: number;\n\n    /**\n     * The slot within the container that the item being used is in.\n     */\n    usedSlot: number;\n}\n\n/**\n * The pipe that the game engine hands item-on-world-item actions off to.\n *\n * This will call the `item_on_world_item` action hooks, if any are registered and match the action being performed.\n *\n * Both `item` and `worldItem` are optional, but if they are provided then they must match the items in use.\n *\n * @author jameskmonger\n */\nconst itemOnWorldItemActionPipe = (\n    player: Player,\n    usedItem: Item,\n    usedWithItem: WorldItem,\n    usedWidgetId: number,\n    usedContainerId: number,\n    usedSlot: number,\n): RunnableHooks<ItemOnWorldItemAction> | null => {\n    if (player.busy) {\n        return null;\n    }\n\n    // Find all item on item action plugins that match this action\n    let matchingHooks = getActionHooks<ItemOnWorldItemActionHook>('item_on_world_item', plugin => {\n        if (questHookFilter(player, plugin)) {\n            const used = usedItem.itemId;\n            const usedWith = usedWithItem.itemId;\n\n            return plugin.items.some(({ item, worldItem }) => {\n                const itemMatch = item === undefined || item === used;\n                const worldItemMatch = worldItem === undefined || worldItem === usedWith;\n\n                return itemMatch && worldItemMatch;\n            });\n        }\n\n        return false;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(`Unhandled item on world item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`);\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            usedItem,\n            usedWithItem,\n            usedWidgetId,\n            usedContainerId,\n            usedSlot,\n        },\n    };\n};\n\n/**\n * Item-on-world-item action pipe definition.\n */\nexport default ['item_on_world_item', itemOnWorldItemActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/item-swap.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { numberHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a swap items action hook.\n */\nexport interface ItemSwapActionHook extends ActionHook<ItemSwapAction, itemSwapActionHandler> {\n    widgetId?: number;\n    widgetIds?: number[];\n}\n\n/**\n * The swap items action hook handler function to be called when the hook's conditions are met.\n */\nexport type itemSwapActionHandler = (itemSwapAction: ItemSwapAction) => void;\n\n/**\n * Details about a swap items action being performed.\n */\nexport interface ItemSwapAction {\n    // The player performing the action.\n    player: Player;\n    // The widget id for the container.\n    widgetId: number;\n    // The container id within the widget.\n    containerId: number;\n    // The slot of the item being swapped item.\n    fromSlot: number;\n    // The slot of the item being swapped with.\n    toSlot: number;\n}\n\n/**\n * The pipe that the game engine hands swap items actions off to.\n * @param player\n * @param fromSlot\n * @param toSlot\n * @param widget\n */\nconst itemSwapActionPipe = (\n    player: Player,\n    fromSlot: number,\n    toSlot: number,\n    widget: { widgetId: number; containerId: number },\n): RunnableHooks<ItemSwapAction> | null => {\n    const matchingHooks = getActionHooks<ItemSwapActionHook>('item_swap').filter(\n        plugin => (plugin.widgetId || plugin.widgetIds) && numberHookFilter((plugin.widgetId || plugin.widgetIds)!, widget.widgetId),\n    );\n\n    if (!matchingHooks || matchingHooks.length === 0) {\n        player.sendMessage(\n            `Unhandled Swap Items action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`,\n        );\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            widgetId: widget.widgetId,\n            containerId: widget.containerId,\n            fromSlot,\n            toSlot,\n        },\n    };\n};\n\n/**\n * Swap items action pipe definition.\n */\nexport default ['item_swap', itemSwapActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/magic-on-npc.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a button action hook.\n */\nexport interface MagicOnNPCActionHook extends ActionHook<MagicOnNPCAction, magiconnpcActionHandler> {\n    // The npc world id that was clicked on after choosing the spell\n    npcworldId?: number;\n    // The IDs of the UI widgets that the buttons are on.\n    widgetIds?: number[];\n    // The child ID or list of child IDs of the button(s) within the UI widget.\n    buttonIds?: number | number[];\n    // Whether or not this item action should cancel other running or queued actions.\n    cancelActions?: boolean;\n}\n\n/**\n * The button action hook handler function to be called when the hook's conditions are met.\n */\nexport type magiconnpcActionHandler = (buttonAction: MagicOnNPCAction) => void | Promise<void>;\n\n/**\n * Details about a button action being performed.\n */\nexport interface MagicOnNPCAction {\n    // The npc world id that was clicked on after choosing the spell\n    npc: Npc;\n    // The player performing the action.\n    player: Player;\n    // The ID of the UI widget that the button is on.\n    widgetId: number;\n    // The child ID of the button within the UI widget.\n    buttonId: number;\n}\n\n/**\n * The pipe that the game engine hands button actions off to.\n * @param npc\n * @param player\n * @param widgetId\n * @param buttonId\n */\nconst magicOnNpcActionPipe = (npc: Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks<MagicOnNPCAction> => {\n    //console.info(`pew pew you use magic on ${npc.name}!`);\n\n    // Find all object action plugins that reference this location object\n    const matchingHooks = getActionHooks<MagicOnNPCActionHook>('magic_on_npc');\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            npc,\n            player,\n            widgetId,\n            buttonId,\n        },\n    };\n};\n\n/**\n * Button action pipe definition.\n */\nexport default ['magic_on_npc', magicOnNpcActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/move-item.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { numberHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a move item action hook.\n */\nexport interface MoveItemActionHook extends ActionHook<MoveItemAction, moveItemActionHandler> {\n    widgetId?: number;\n    widgetIds?: number[];\n}\n\n/**\n * The move item action hook handler function to be called when the hook's conditions are met.\n */\nexport type moveItemActionHandler = (moveItemAction: MoveItemAction) => void;\n\n/**\n * Details about a move item action being performed.\n */\nexport interface MoveItemAction {\n    // The player performing the action.\n    player: Player;\n    // The widget id for the container.\n    widgetId: number;\n    // The container id within the widget.\n    containerId: number;\n    // The original slot of the item.\n    fromSlot: number;\n    // The new slot for the item.\n    toSlot: number;\n}\n\n/**\n * The pipe that the game engine hands move item actions off to.\n * @param player\n * @param fromSlot\n * @param toSlot\n * @param widget\n */\nconst moveItemActionPipe = (\n    player: Player,\n    fromSlot: number,\n    toSlot: number,\n    widget: { widgetId: number; containerId: number },\n): RunnableHooks<MoveItemAction> | null => {\n    const matchingHooks = getActionHooks<MoveItemActionHook>('move_item').filter(\n        plugin => (plugin.widgetId || plugin.widgetIds) && numberHookFilter((plugin.widgetId || plugin.widgetIds)!, widget.widgetId),\n    );\n\n    if (!matchingHooks || matchingHooks.length === 0) {\n        player.sendMessage(\n            `Unhandled Move Item action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`,\n        );\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            widgetId: widget.widgetId,\n            containerId: widget.containerId,\n            fromSlot,\n            toSlot,\n        },\n    };\n};\n\n/**\n * Move item action pipe definition.\n */\nexport default ['move_item', moveItemActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/npc-init.action.ts",
    "content": "import type { ActionPipe } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { stringHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Npc } from '@engine/world/actor/npc';\n\n/**\n * Defines an npc init action hook.\n */\nexport interface NpcInitActionHook extends ActionHook<NpcInitAction, npcInitActionHandler> {\n    // A single NPC key or a list of NPC keys that this action applies to.\n    npcs?: string | string[];\n}\n\n/**\n * The npc init action hook handler function to be called when the hook's conditions are met.\n */\nexport type npcInitActionHandler = (npcAction: NpcInitAction) => void;\n\n/**\n * Details about an npc init action being performed.\n */\nexport interface NpcInitAction {\n    // The npc that is being initialized.\n    npc: Npc;\n}\n\n/**\n * The pipe that the game engine hands npc init actions off to.\n * @param npc\n */\nconst npcInitActionPipe = ({ npc }: NpcInitAction): void => {\n    const actionHooks = getActionHooks<NpcInitActionHook>('npc_init').filter(\n        plugin => !plugin.npcs || stringHookFilter(plugin.npcs, npc.key),\n    );\n    actionHooks.forEach(actionHook => {\n        if (!actionHook.handler) {\n            return;\n        }\n\n        actionHook.handler({ npc });\n    });\n};\n\n/**\n * Npc init action pipe definition.\n */\nexport default ['npc_init', npcInitActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/npc-interaction.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Position } from '@engine/world/position';\n\n/**\n * Defines an npc action hook.\n */\nexport interface NpcInteractionActionHook extends ActionHook<NpcInteractionAction, npcInteractionActionHandler> {\n    // A single NPC key or a list of NPC keys that this action applies to.\n    npcs?: string | string[];\n    // A single option name or a list of option names that this action applies to.\n    options?: string | string[];\n    // Whether or not the player needs to walk to this NPC before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The npc action hook handler function to be called when the hook's conditions are met.\n */\nexport type npcInteractionActionHandler = (npcInteractionAction: NpcInteractionAction) => void;\n\n/**\n * Details about an npc action being performed.\n */\nexport interface NpcInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The NPC the action is being performed on.\n    npc: Npc;\n    // The position that the NPC was at when the action was initiated.\n    position: Position;\n    // The option used when interacting with the NPC\n    option: string;\n}\n\n/**\n * The pipe that the game engine hands npc actions off to.\n * @param player\n * @param npc\n * @param position\n * @param option\n */\nconst npcInteractionActionPipe = (\n    player: Player,\n    npc: Npc,\n    position: Position,\n    option: string,\n): RunnableHooks<NpcInteractionAction> | null => {\n    if (player.busy) {\n        return null;\n    }\n\n    const morphedNpc = player.getMorphedNpcDetails(npc);\n\n    // Find all NPC action plugins that reference this NPC\n    let matchingHooks = getActionHooks<NpcInteractionActionHook>('npc_interaction').filter(\n        plugin =>\n            questHookFilter(player, plugin) &&\n            (!plugin.npcs || stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key)) &&\n            (!plugin.options || stringHookFilter(plugin.options, option)),\n    );\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled NPC interaction: ${option} ${morphedNpc?.key || npc.key} (id-${morphedNpc?.gameId || npc.id}) @ ${position.x},${position.y},${position.level}`,\n        );\n        if (morphedNpc) {\n            player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`);\n        }\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(new WalkToActorPluginTask(walkToPlugins, player, 'npc', npc, { option }));\n\n        return null;\n    }\n\n    console.log(`WE ARE INTERACTING WITH NPC quests: ${questActions.length} ${matchingHooks.length}`);\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            npc,\n            position,\n            option,\n        },\n    };\n};\n\n/**\n * Npc action pipe definition.\n */\nexport default ['npc_interaction', npcInteractionActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/object-interaction.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToObjectPluginTask } from '@engine/action/pipe/task/walk-to-object-plugin-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Position } from '@engine/world/position';\nimport type { LandscapeObject, ObjectConfig } from '@runejs/filestore';\n\n/**\n * Defines an object action hook.\n */\nexport interface ObjectInteractionActionHook extends ActionHook<ObjectInteractionAction, objectInteractionActionHandler> {\n    // A single game object ID or a list of object IDs that this action applies to.\n    objectIds: number | number[];\n    // A single option name or a list of option names that this action applies to.\n    options: string | string[];\n    // Whether or not the player needs to walk to this object before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The object action hook handler function to be called when the hook's conditions are met.\n */\nexport type objectInteractionActionHandler = (objectInteractionAction: ObjectInteractionAction) => void;\n\n/**\n * Details about an object action being performed.\n */\nexport interface ObjectInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The object the action is being performed on.\n    object: LandscapeObject;\n    // Additional details about the object that the action is being performed on.\n    objectConfig: ObjectConfig;\n    // The position that the game object was at when the action was initiated.\n    position: Position;\n    // Whether or not this game object is an original map object or if it has been added/replaced.\n    cacheOriginal: boolean;\n    // The option that the player used (ie \"cut\" tree, or \"smelt\" furnace).\n    option: string;\n}\n\n/**\n * The pipe that the game engine hands object actions off to.\n * @param player\n * @param landscapeObject\n * @param objectConfig\n * @param position\n * @param option\n * @param cacheOriginal\n */\nconst objectInteractionActionPipe = (\n    player: Player,\n    landscapeObject: LandscapeObject,\n    objectConfig: ObjectConfig,\n    position: Position,\n    option: string,\n    cacheOriginal: boolean,\n): RunnableHooks<ObjectInteractionAction> | null => {\n    if (player.metadata.blockObjectInteractions) {\n        return null;\n    }\n\n    // Find all object action plugins that reference this location object\n    let matchingHooks = getActionHooks<ObjectInteractionActionHook>('object_interaction').filter(\n        plugin =>\n            questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId, plugin.options, option),\n    );\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(\n            `Unhandled object interaction: ${option} ${objectConfig.name} ` +\n                `(id-${landscapeObject.objectId}) @ ${position.x},${position.y},${position.level}`,\n        );\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(new WalkToObjectPluginTask(walkToPlugins, player, landscapeObject, { objectConfig, cacheOriginal, option }));\n\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            object: landscapeObject,\n            objectConfig,\n            option,\n            position,\n            cacheOriginal,\n        },\n    };\n};\n\n/**\n * Object action pipe definition.\n */\nexport default ['object_interaction', objectInteractionActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/player-command.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { reloadContent, reloadContentCommands } from '@engine/plugins/reload-content';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\n/**\n * Defines a player command action hook.\n */\nexport interface PlayerCommandActionHook extends ActionHook<PlayerCommandAction, commandActionHandler> {\n    // The single command or list of commands that this action applies to.\n    commands: string | string[];\n    // The potential arguments for this command action.\n    args?: {\n        name: string;\n        type: 'number' | 'string' | 'either';\n        defaultValue?: number | string;\n    }[];\n}\n\n/**\n * The player command action hook handler function to be called when the hook's conditions are met.\n */\nexport type commandActionHandler = (playerCommandAction: PlayerCommandAction) => void;\n\n/**\n * Details about a player command action being performed.\n */\nexport interface PlayerCommandAction {\n    // The player performing the action.\n    player: Player;\n    // The command that the player entered.\n    command: string;\n    // If the player used the console\n    isConsole: boolean;\n    // The arguments that the player entered for their command.\n    args: { [key: string]: number | string };\n}\n\n/**\n * The pipe that the game engine hands player command actions off to.\n * @param player\n * @param command\n * @param isConsole\n * @param inputArgs\n */\nconst playerCommandActionPipe = (\n    player: Player,\n    command: string,\n    isConsole: boolean,\n    inputArgs: string[],\n): RunnableHooks<PlayerCommandAction> | null => {\n    command = command.toLowerCase();\n\n    // Reload game content\n    if (reloadContentCommands.indexOf(command) !== -1) {\n        reloadContent(player, isConsole).catch(logger.error);\n        return null;\n    }\n\n    const actionArgs = {};\n\n    const plugins = getActionHooks<PlayerCommandActionHook>('player_command').filter(actionHook => {\n        let valid: boolean;\n        if (Array.isArray(actionHook.commands)) {\n            valid = actionHook.commands.indexOf(command) !== -1;\n        } else {\n            valid = actionHook.commands === command;\n        }\n\n        if (!valid) {\n            return false;\n        }\n\n        if (actionHook.args) {\n            const args = actionHook.args;\n            let syntaxError = `Syntax error. Try ::${command}`;\n\n            args.forEach(commandArg => {\n                syntaxError += ` ${commandArg.name}:${commandArg.type}${commandArg.defaultValue === undefined ? '' : '?'}`;\n            });\n\n            const requiredArgLength = actionHook.args.filter(arg => arg.defaultValue === undefined).length;\n            if (requiredArgLength > inputArgs.length) {\n                player.sendLogMessage(syntaxError, isConsole);\n                return;\n            }\n\n            for (let i = 0; i < actionHook.args.length; i++) {\n                let argValue: string | number | null = inputArgs[i] || null;\n                const pluginArg = actionHook.args[i];\n\n                if (argValue === null || argValue === undefined) {\n                    if (pluginArg.defaultValue === undefined) {\n                        player.sendLogMessage(syntaxError, isConsole);\n                        return;\n                    } else {\n                        argValue = pluginArg.defaultValue;\n                    }\n                } else {\n                    if (pluginArg.type === 'number') {\n                        argValue = parseInt(argValue);\n                        if (isNaN(argValue)) {\n                            player.sendLogMessage(syntaxError, isConsole);\n                            return;\n                        }\n                    } else if (pluginArg.type === 'string') {\n                        if (!argValue || argValue.trim() === '') {\n                            player.sendLogMessage(syntaxError, isConsole);\n                            return;\n                        }\n                    }\n                }\n\n                actionArgs[pluginArg.name] = argValue;\n            }\n        }\n\n        return true;\n    });\n\n    if (plugins.length === 0) {\n        player.sendLogMessage(`Unhandled command: ${command}`, isConsole);\n        return null;\n    }\n\n    return {\n        hooks: plugins,\n        action: {\n            player,\n            command,\n            isConsole,\n            args: actionArgs,\n        },\n    };\n};\n\n/**\n * Player command action pipe definition.\n */\nexport default ['player_command', playerCommandActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/player-init.action.ts",
    "content": "import type { ActionPipe } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a player init action hook.\n */\nexport type PlayerInitActionHook = ActionHook<PlayerInitAction, playerInitActionHandler>;\n\n/**\n * The player init action hook handler function to be called when the hook's conditions are met.\n */\nexport type playerInitActionHandler = (playerInitAction: PlayerInitAction) => void;\n\n/**\n * Details about a player init action being performed.\n */\nexport interface PlayerInitAction {\n    // The player that is being initialized.\n    player: Player;\n}\n\n/**\n * The pipe that the game engine hands player init actions off to.\n * @param player\n */\nconst playerInitActionPipe = ({ player }: PlayerInitAction): void => {\n    const actionHooks = getActionHooks<PlayerInitActionHook>('player_init');\n    actionHooks.forEach(actionHook => {\n        if (!actionHook.handler) {\n            return;\n        }\n\n        actionHook.handler({ player });\n    });\n};\n\n/**\n * Player init action pipe definition.\n */\nexport default ['player_init', playerInitActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/player-interaction.action.ts",
    "content": "import type { RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Position } from '@engine/world/position';\n\n/**\n * Defines a player action hook.\n */\nexport interface PlayerInteractionActionHook extends ActionHook<PlayerInteractionAction, playerInteractionActionHandler> {\n    // A single option name or a list of option names that this action applies to.\n    options: string | string[];\n    // Whether or not the player needs to walk to the other player before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The player action hook handler function to be called when the hook's conditions are met.\n */\nexport type playerInteractionActionHandler = (playerInteractionAction: PlayerInteractionAction) => void;\n\n/**\n * Details about a player action being performed.\n */\nexport interface PlayerInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The player that the action is being performed on.\n    otherPlayer: Player;\n    // The position that the other player was at when the action was initiated.\n    position: Position;\n}\n\n/**\n * The pipe that the game engine hands player actions off to.\n * @param player\n * @param otherPlayer\n * @param position\n * @param option\n */\nconst playerInteractionActionPipe = (\n    player: Player,\n    otherPlayer: Player,\n    position: Position,\n    option: string,\n): RunnableHooks<PlayerInteractionAction> | null => {\n    // Find all player action plugins that reference this option\n    let matchingHooks = getActionHooks<PlayerInteractionActionHook>('player_interaction').filter(\n        plugin => questHookFilter(player, plugin) && stringHookFilter(plugin.options, option),\n    );\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.sendMessage(`Unhandled Player interaction: ${option} @ ${position.x},${position.y},${position.level}`);\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(new WalkToActorPluginTask(walkToPlugins, player, 'otherPlayer', otherPlayer, {}));\n\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            otherPlayer,\n            position,\n        },\n    };\n};\n\n/**\n * Player action pipe definition.\n */\nexport default ['player_interaction', playerInteractionActionPipe];\n"
  },
  {
    "path": "src/engine/action/pipe/prayer.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a button action hook.\n */\nexport interface PrayerActionHook extends ActionHook<PrayerAction, PrayerActionHandler> {\n    // The npc world id that was clicked on after choosing the spell\n    npcworldId?: number;\n    // The IDs of the UI widgets that the buttons are on.\n    widgetIds?: number[];\n    // The child ID or list of child IDs of the button(s) within the UI widget.\n    buttonIds?: number | number[];\n    // Whether or not this item action should cancel other running or queued actions.\n    cancelActions?: boolean;\n}\n\n/**\n * The button action hook handler function to be called when the hook's conditions are met.\n */\nexport type PrayerActionHandler = (buttonAction: PrayerAction) => void | Promise<void>;\n\n/**\n * Details about a button action being performed.\n */\nexport interface PrayerAction {\n    // The npc world id that was clicked on after choosing the spell\n    npc: Npc;\n    // The player performing the action.\n    player: Player;\n    // The ID of the UI widget that the button is on.\n    widgetId: number;\n    // The child ID of the button within the UI widget.\n    buttonId: number;\n}\n\n/**\n * The pipe that the game engine hands button actions off to.\n * @param npc\n * @param player\n * @param widgetId\n * @param buttonId\n */\nconst prayerActionPipe = (npc: Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks<PrayerAction> => {\n    console.info(`You used prayer`);\n\n    // Find all object action plugins that reference this location object\n    const matchingHooks = getActionHooks<PrayerActionHook>('button');\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            npc,\n            player,\n            widgetId,\n            buttonId,\n        },\n    };\n};\n\n/**\n * Button action pipe definition.\n */\nexport default ['prayer', prayerActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/region-change.action.ts",
    "content": "import type { ActionPipe } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { RegionType } from '@engine/world/map/region';\nimport type { Position } from '@engine/world/position';\nimport { Coords } from '@engine/world/position';\n\n/**\n * Defines a player region change action hook.\n */\nexport interface RegionChangeActionHook extends ActionHook<RegionChangeAction, regionChangeActionHandler> {\n    // Optional single region type for the action hook to apply to.\n    regionType?: RegionType;\n    // Optional multiple region types for the action hook to apply to.\n    regionTypes?: RegionType[];\n    // Optional teleporting requirement\n    teleporting?: boolean;\n}\n\n/**\n * The player region change action hook handler function to be called when the hook's conditions are met.\n */\nexport type regionChangeActionHandler = (regionChangeAction: RegionChangeAction) => void;\n\n/**\n * Details about a player region change action being performed.\n */\nexport interface RegionChangeAction {\n    // The player performing the action.\n    player: Player;\n    // Whether or not the player is teleporting to their new location\n    teleporting: boolean;\n    // The original position that the player was at before moving to the new region.\n    originalPosition: Position;\n    // The position that the player ended up at in the new region.\n    currentPosition: Position;\n    // The player's original chunk coordinates\n    originalChunkCoords: Coords;\n    // The player's current chunk coordinates\n    currentChunkCoords: Coords;\n    // The player's original map region coordinates\n    originalMapRegionCoords: Coords;\n    // The player's current map region coordinates\n    currentMapRegionCoords: Coords;\n    // The player's original map region id\n    originalMapRegionId: number;\n    // The player's current map region id\n    currentMapRegionId: number;\n    // The region types that changed for the player.\n    regionTypes: RegionType[];\n}\n\n/**\n * Creates a RegionChangeAction object from the given inputs.\n *\n * TODO (Jameskmonger) I changed this function's return type to `| null` to satisfy TypeScript,\n *          not sure if this is correct or if the code was just wrong before.\n *\n * @param player The player.\n * @param originalPosition The player's original position.\n * @param currentPosition The player's current position.\n * @param teleporting Whether or not the player is teleporting; defaults to false.\n */\nexport const regionChangeActionFactory = (\n    player: Player,\n    originalPosition: Position,\n    currentPosition: Position,\n    teleporting: boolean = false,\n): RegionChangeAction | null => {\n    const regionTypes: RegionType[] = [];\n    const originalMapRegionId: number = ((originalPosition.x >> 6) << 8) + (originalPosition.y >> 6);\n    const currentMapRegionId: number = ((currentPosition.x >> 6) << 8) + (currentPosition.y >> 6);\n    const originalChunkCoords: Coords = {\n        x: originalPosition.chunkX,\n        y: originalPosition.chunkY,\n        level: originalPosition.level,\n    };\n    const currentChunkCoords: Coords = {\n        x: currentPosition.chunkX,\n        y: currentPosition.chunkY,\n        level: currentPosition.level,\n    };\n\n    if (originalMapRegionId !== currentMapRegionId) {\n        regionTypes.push('region');\n    }\n\n    if (!Coords.equals(originalChunkCoords, currentChunkCoords)) {\n        regionTypes.push('chunk');\n    }\n\n    if (regionTypes.length === 0) {\n        return null;\n    }\n\n    return {\n        player,\n        regionTypes,\n        teleporting,\n\n        originalPosition,\n        originalChunkCoords,\n        originalMapRegionCoords: {\n            x: originalPosition.x >> 6,\n            y: originalPosition.y >> 6,\n            level: originalPosition.level,\n        },\n        originalMapRegionId,\n\n        currentPosition: player.position,\n        currentChunkCoords,\n        currentMapRegionCoords: {\n            x: currentPosition.x >> 6,\n            y: currentPosition.y >> 6,\n            level: currentPosition.level,\n        },\n        currentMapRegionId,\n    };\n};\n\n/**\n * The pipe that the game engine hands player region change actions off to.\n * @param actionData\n */\nconst regionChangeActionPipe = (actionData: RegionChangeAction): void => {\n    if (!actionData) {\n        return;\n    }\n\n    const { regionTypes } = actionData;\n\n    if (!regionTypes || regionTypes.length === 0) {\n        return;\n    }\n\n    // Find all action hooks that match the provided input\n    const actionList =\n        getActionHooks<RegionChangeActionHook>('region_change')?.filter(actionHook => {\n            if (actionHook.teleporting && !actionData.teleporting) {\n                return false;\n            }\n\n            if (actionHook.regionType) {\n                return regionTypes.indexOf(actionHook.regionType) !== -1;\n            } else if (actionHook.regionTypes && actionHook.regionTypes.length !== 0) {\n                let valid = false;\n                for (const type of actionHook.regionTypes) {\n                    if (regionTypes.indexOf(type) !== -1) {\n                        valid = true;\n                        break;\n                    }\n                }\n\n                return valid;\n            }\n\n            return false;\n        }) || null;\n\n    if (!actionList || actionList.length === 0) {\n        // No matching actions found\n        return;\n    }\n\n    actionList.forEach(\n        async actionHook =>\n            new Promise<void>(resolve => {\n                if (actionHook && actionHook.handler) {\n                    actionHook.handler(actionData);\n                }\n\n                resolve();\n            }),\n    );\n};\n\n/**\n * Player region change action pipe definition.\n */\nexport default ['region_change', regionChangeActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/spawned-item-interaction.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters';\nimport { WalkToItemPluginTask } from '@engine/action/pipe/task/walk-to-item-plugin-task';\nimport { findItem } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport { logger } from '@runejs/common';\n\n/**\n * Defines a world item action hook.\n */\nexport interface SpawnedItemInteractionHook extends ActionHook<SpawnedItemInteractionAction, spawnedItemInteractionHandler> {\n    // A single game item ID or a list of item IDs that this action applies to.\n    itemIds?: number | number[];\n    // A single option name or a list of option names that this action applies to.\n    options: string | string[];\n    // Whether or not the player needs to walk to this world item before performing the action.\n    walkTo: boolean;\n}\n\n/**\n * The world item action hook handler function to be called when the hook's conditions are met.\n */\nexport type spawnedItemInteractionHandler = (spawnedItemInteractionAction: SpawnedItemInteractionAction) => void;\n\n/**\n * Details about a world item action being performed.\n */\nexport interface SpawnedItemInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The world item that the player is interacting with.\n    worldItem: WorldItem;\n    // Details about the item\n    itemDetails: ItemDetails;\n\n    // TODO (jkm) add \"option\" to the action\n}\n\n/**\n * The pipe that the game engine hands world item actions off to.\n * @param player\n * @param worldItem\n * @param option\n */\nconst spawnedItemInteractionPipe = (\n    player: Player,\n    worldItem: WorldItem,\n    option: string,\n): RunnableHooks<SpawnedItemInteractionAction> | null => {\n    // Find all world item action plugins that reference this world item\n    let matchingHooks = getActionHooks<SpawnedItemInteractionHook>('spawned_item_interaction').filter(plugin => {\n        if (!questHookFilter(player, plugin)) {\n            return false;\n        }\n\n        if (plugin.itemIds !== undefined) {\n            if (!numberHookFilter(plugin.itemIds, worldItem.itemId)) {\n                return false;\n            }\n        }\n\n        if (!stringHookFilter(plugin.options, option)) {\n            return false;\n        }\n\n        return true;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(`Unhandled world item interaction: ${option} ${worldItem.itemId}`);\n        return null;\n    }\n\n    const itemDetails = findItem(worldItem.itemId);\n\n    if (!itemDetails) {\n        logger.error(`Item ${worldItem.itemId} not registered on the server [spawned-item-interaction action pipe]`);\n        return null;\n    }\n\n    const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo);\n\n    if (walkToPlugins.length > 0) {\n        player.enqueueBaseTask(new WalkToItemPluginTask(walkToPlugins, player, worldItem, itemDetails));\n\n        return null;\n    }\n\n    return {\n        hooks: matchingHooks,\n        action: {\n            player,\n            worldItem,\n            itemDetails,\n        },\n    };\n};\n\n/**\n * World item action pipe definition.\n */\nexport default ['spawned_item_interaction', spawnedItemInteractionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/action/pipe/task/queueable-task.ts",
    "content": "import type { ActionHook } from '@engine/action/hook/action-hook';\nimport { ActorTask } from '@engine/task/impl/actor-task';\nimport type { Task } from '@engine/task/task';\nimport type { Actor } from '@engine/world/actor/actor';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ItemOnObjectAction } from '../item-on-object.action';\nimport type { ObjectInteractionAction } from '../object-interaction.action';\n\n/**\n * The result of running a callback function will be recorded here so that the\n * `QueueableTask` can make a choice to continue looping and/or to enqueue\n * the desired task.\n */\nexport interface QueueableTaskEval {\n    callbackResult: boolean;\n    shouldContinueLooping: boolean;\n}\n\n/**\n * All actions supported by this plugin task.\n */\ntype ObjectAction = ObjectInteractionAction | ItemOnObjectAction;\n\n/**\n * An ActionHook for a supported ObjectAction.\n */\ntype ObjectActionHook<TAction extends ObjectAction> = ActionHook<TAction, (data: TAction) => void>;\n\n/**\n * The data unique to the action being executed (i.e. excluding shared data)\n */\ntype ObjectActionData<TAction extends ObjectAction> = Omit<TAction, 'player' | 'actor' | 'object' | 'position'>;\n\n/**\n * Processes the provided callback function on every single tick, and also allows any\n * arbitrary base task to be queued at the next tick. Useful for queuing random\n * movements, as well as other things.\n *\n * Can loop infinitely based on the result of the passed in `callback` function.\n */\nexport class QueueableTask<TAction extends ObjectAction> extends ActorTask<Player | Actor> {\n    /**\n     * The plugins to execute on each tick. These will not get called if the\n     * `callback` indicates a halting condition.\n     */\n    private plugins: ObjectActionHook<TAction>[];\n\n    private data: ObjectActionData<TAction> | null;\n\n    /**\n     * Optionally provided base task to enqueue on each tick. Can be `null`.\n     */\n    private task: Task | null;\n\n    /**\n     * This callback function will be executed on every tick. It must return a\n     * `QueueableTaskEval` so that this loop can determine if it should\n     * terminate or continue looping.\n     */\n    private callback: () => QueueableTaskEval;\n\n    constructor(\n        plugins: ObjectActionHook<TAction>[],\n        actor: Player | Actor,\n        callback: () => QueueableTaskEval,\n        task: Task | null,\n        data: ObjectActionData<TAction> | null,\n    ) {\n        super(actor);\n\n        this.plugins = plugins;\n        this.data = data;\n        this.task = task;\n        this.callback = callback;\n    }\n\n    /**\n     * Executed every tick. Depending on the callback value, this task can stop\n     * future executions.\n     */\n    public execute(): void {\n        const ev = this.callback();\n        if (!ev.callbackResult) {\n            if (!ev.shouldContinueLooping) {\n                this.stop();\n            }\n            return;\n        }\n\n        if (this.task) {\n            // only gets executed if the callback returns true for its result\n            this.actor.enqueueBaseTask(this.task);\n        }\n\n        // call the relevant plugins on each tick, if provided\n        this.plugins.forEach(plugin => {\n            if (!plugin || !plugin.handler) {\n                return;\n            }\n\n            const action = {\n                player: this.actor,\n                ...this.data,\n            } as TAction;\n\n            plugin.handler(action);\n        });\n\n        if (!ev.shouldContinueLooping) {\n            this.stop();\n            return;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/action/pipe/task/walk-to-actor-plugin-task.ts",
    "content": "import type { ActionHook } from '@engine/action/hook/action-hook';\nimport type { ItemOnNpcAction } from '@engine/action/pipe/item-on-npc.action';\nimport type { ItemOnPlayerAction } from '@engine/action/pipe/item-on-player.action';\nimport type { NpcInteractionAction } from '@engine/action/pipe/npc-interaction.action';\nimport type { PlayerInteractionAction } from '@engine/action/pipe/player-interaction.action';\nimport { ActorActorInteractionTask } from '@engine/task/impl/actor-actor-interaction-task';\nimport type { Actor } from '@engine/world/actor/actor';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * All actions supported by this plugin task.\n */\ntype ActorAction = PlayerInteractionAction | ItemOnPlayerAction | NpcInteractionAction | ItemOnNpcAction;\n\n/**\n * An ActionHook for a supported ObjectAction.\n */\ntype ActorActionHook<TAction extends ActorAction> = ActionHook<TAction, (data: TAction) => void>;\n\ntype ActorKey = 'otherPlayer' | 'npc';\n\n/**\n * The data unique to the action being executed (i.e. excluding shared data)\n */\ntype ActorActionData<TAction extends ActorAction> = Omit<TAction, 'player' | ActorKey | 'position'>;\n\n/**\n * This is a task to migrate old `walkTo` item interaction actions to the new task system.\n *\n * This is a first-pass implementation to allow for removal of the old action system.\n * It will be refactored in future to be more well suited to our plugin system.\n */\nexport class WalkToActorPluginTask<\n    TAction extends ActorAction,\n    TActorKey extends ActorKey,\n    TOtherActor extends Actor,\n> extends ActorActorInteractionTask<Player, TOtherActor> {\n    /**\n     * The plugins to execute when the player arrives at the object.\n     */\n    private plugins: ActorActionHook<TAction>[];\n\n    private data: ActorActionData<TAction>;\n\n    private actorKey: TActorKey;\n\n    constructor(\n        plugins: ActorActionHook<TAction>[],\n        player: Player,\n        actorKey: TActorKey,\n        other: TOtherActor,\n        data: ActorActionData<TAction>,\n    ) {\n        super(player, other);\n\n        this.plugins = plugins;\n        this.data = data;\n        this.actorKey = actorKey;\n    }\n\n    /**\n     * Executed every tick to check if the player has arrived yet and calls the plugins if so.\n     */\n    public execute(): void {\n        // call super to manage waiting for the movement to complete\n        super.execute();\n\n        // check if the player has arrived yet\n        const other = this.other;\n        const otherPosition = this.other?.position;\n        if (!other || !otherPosition) {\n            return;\n        }\n\n        // call the relevant plugins\n        this.plugins.forEach(plugin => {\n            if (!plugin || !plugin.handler) {\n                return;\n            }\n\n            const action = {\n                player: this.actor,\n                position: otherPosition,\n                [this.actorKey]: other,\n                ...this.data,\n            };\n\n            // I wish I didn't have to cast here, but TypeScript is making it difficult\n            plugin.handler(action as unknown as TAction);\n        });\n\n        // this task only executes once, on arrival\n        this.stop();\n    }\n}\n"
  },
  {
    "path": "src/engine/action/pipe/task/walk-to-item-plugin-task.ts",
    "content": "import type { SpawnedItemInteractionHook } from '@engine/action/pipe/spawned-item-interaction.action';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport { ActorWorldItemInteractionTask } from '@engine/task/impl/actor-world-item-interaction-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { WorldItem } from '@engine/world/items/world-item';\n\n/**\n * This is a task to migrate old `walkTo` item interaction actions to the new task system.\n *\n * This is a first-pass implementation to allow for removal of the old action system.\n * It will be refactored in future to be more well suited to our plugin system.\n */\nexport class WalkToItemPluginTask extends ActorWorldItemInteractionTask<Player> {\n    /**\n     * The plugins to execute when the player arrives at the item.\n     */\n    private plugins: SpawnedItemInteractionHook[];\n\n    /**\n     * Details about the item\n     */\n    private itemDetails: ItemDetails;\n\n    constructor(plugins: SpawnedItemInteractionHook[], player: Player, worldItem: WorldItem, itemDetails: ItemDetails) {\n        super(player, worldItem);\n\n        this.plugins = plugins;\n        this.itemDetails = itemDetails;\n    }\n\n    /**\n     * Executed every tick to check if the player has arrived yet and calls the plugins if so.\n     */\n    public execute(): void {\n        // call super to manage waiting for the movement to complete\n        super.execute();\n\n        // check if the player has arrived yet\n        const worldItem = this.worldItem;\n        if (!worldItem) {\n            return;\n        }\n\n        // call the relevant plugins\n        this.plugins.forEach(plugin => {\n            if (!plugin || !plugin.handler) {\n                return;\n            }\n\n            plugin.handler({\n                player: this.actor,\n                worldItem: worldItem,\n                itemDetails: this.itemDetails,\n            });\n        });\n\n        // this task only executes once, on arrival\n        this.stop();\n    }\n}\n"
  },
  {
    "path": "src/engine/action/pipe/task/walk-to-object-plugin-task.ts",
    "content": "import type { ActionHook } from '@engine/action/hook/action-hook';\nimport type { ItemOnObjectAction } from '@engine/action/pipe/item-on-object.action';\nimport type { ObjectInteractionAction } from '@engine/action/pipe/object-interaction.action';\nimport { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { LandscapeObject } from '@runejs/filestore';\n\n/**\n * All actions supported by this plugin task.\n */\ntype ObjectAction = ObjectInteractionAction | ItemOnObjectAction;\n\n/**\n * An ActionHook for a supported ObjectAction.\n */\ntype ObjectActionHook<TAction extends ObjectAction> = ActionHook<TAction, (data: TAction) => void>;\n\n/**\n * The data unique to the action being executed (i.e. excluding shared data)\n */\ntype ObjectActionData<TAction extends ObjectAction> = Omit<TAction, 'player' | 'object' | 'position'>;\n\n/**\n * This is a task to migrate old `walkTo` item interaction actions to the new task system.\n *\n * This is a first-pass implementation to allow for removal of the old action system.\n * It will be refactored in future to be more well suited to our plugin system.\n */\nexport class WalkToObjectPluginTask<TAction extends ObjectAction> extends ActorLandscapeObjectInteractionTask<Player> {\n    /**\n     * The plugins to execute when the player arrives at the object.\n     */\n    private plugins: ObjectActionHook<TAction>[];\n\n    private data: ObjectActionData<TAction>;\n\n    constructor(plugins: ObjectActionHook<TAction>[], player: Player, landscapeObject: LandscapeObject, data: ObjectActionData<TAction>) {\n        super(\n            player,\n            landscapeObject,\n            // TODO (jkm) handle object size\n            // TODO (jkm) pass orientation instead of size\n            1,\n            1,\n        );\n\n        this.plugins = plugins;\n        this.data = data;\n    }\n\n    /**\n     * Executed every tick to check if the player has arrived yet and calls the plugins if so.\n     */\n    public execute(): void {\n        // call super to manage waiting for the movement to complete\n        super.execute();\n\n        // check if the player has arrived yet\n        const landscapeObject = this.landscapeObject;\n        const landscapeObjectPosition = this.landscapeObjectPosition;\n        if (!landscapeObject || !landscapeObjectPosition) {\n            return;\n        }\n\n        // call the relevant plugins\n        this.plugins.forEach(plugin => {\n            if (!plugin || !plugin.handler) {\n                return;\n            }\n\n            const action = {\n                player: this.actor,\n                object: landscapeObject,\n                position: landscapeObjectPosition,\n                ...this.data,\n            } as TAction;\n\n            plugin.handler(action);\n        });\n\n        // this task only executes once, on arrival\n        this.stop();\n    }\n}\n"
  },
  {
    "path": "src/engine/action/pipe/widget-interaction.action.ts",
    "content": "import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters';\nimport type { Player } from '@engine/world/actor/player/player';\n\n/**\n * Defines a widget action hook.\n */\nexport interface WidgetInteractionActionHook extends ActionHook<WidgetInteractionAction, widgetInteractionActionHandler> {\n    // A single UI widget ID or a list of widget IDs that this action applies to.\n    widgetIds: number | number[];\n    // A single UI widget child ID or a list of child IDs that this action applies to.\n    childIds?: number | number[];\n    // The context menu option index for this action.\n    optionId?: number;\n    // Whether or not this item action should cancel other running or queued actions.\n    cancelActions?: boolean;\n}\n\n/**\n * The widget action hook handler function to be called when the hook's conditions are met.\n */\nexport type widgetInteractionActionHandler = (widgetInteractionAction: WidgetInteractionAction) => void;\n\n/**\n * Details about a widget action being performed.\n */\nexport interface WidgetInteractionAction {\n    // The player performing the action.\n    player: Player;\n    // The ID of the UI widget that the button is on.\n    widgetId: number;\n    // The ID of the interacted child within the UI widget.\n    childId: number;\n    // The selected context menu option index.\n    optionId: number;\n}\n\n/**\n * The pipe that the game engine hands widget actions off to.\n * @param player The player performing the action.\n * @param widgetId The ID of the widget.\n * @param childId The ID of the widget child being interacted with.\n * @param optionId The widget context option chosen by the player.\n */\nconst widgetActionPipe = (\n    player: Player,\n    widgetId: number,\n    childId: number,\n    optionId: number,\n): RunnableHooks<WidgetInteractionAction> | null => {\n    const playerWidget = Object.values(player.interfaceState.widgetSlots).find(widget => widget && widget.widgetId === widgetId);\n\n    if (playerWidget?.fakeWidget) {\n        widgetId = playerWidget.fakeWidget;\n    }\n\n    // Find all item on item action plugins that match this action\n    let matchingHooks = getActionHooks<WidgetInteractionActionHook>('widget_interaction').filter(plugin => {\n        if (!plugin.widgetIds) {\n            return false;\n        }\n\n        if (!questHookFilter(player, plugin)) {\n            return false;\n        }\n\n        if (!advancedNumberHookFilter(plugin.widgetIds, widgetId)) {\n            return false;\n        }\n\n        if (plugin.optionId !== undefined && plugin.optionId !== optionId) {\n            return false;\n        }\n\n        if (plugin.childIds !== undefined) {\n            return advancedNumberHookFilter(plugin.childIds, childId);\n        }\n        return true;\n    });\n\n    const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);\n\n    if (questActions.length !== 0) {\n        matchingHooks = questActions;\n    }\n\n    if (matchingHooks.length === 0) {\n        player.outgoingPackets.chatboxMessage(`Unhandled widget option: ${widgetId}, ${childId}:${optionId}`);\n        return null;\n    }\n\n    const action: WidgetInteractionAction = { player, widgetId, childId, optionId };\n\n    return {\n        hooks: matchingHooks,\n        action,\n    };\n};\n\n/**\n * Widget action pipe definition.\n */\nexport default ['widget_interaction', widgetActionPipe] as ActionPipe;\n"
  },
  {
    "path": "src/engine/config/config-handler.ts",
    "content": "import 'json5/lib/register';\nimport type { ItemPresetConfiguration } from '@engine/config/item-config';\nimport { ItemDetails, loadItemConfigurations } from '@engine/config/item-config';\nimport type { ItemSpawn } from '@engine/config/item-spawn-config';\nimport { loadItemSpawnConfigurations } from '@engine/config/item-spawn-config';\nimport type { MusicTrack } from '@engine/config/music-regions-config';\nimport { loadMusicRegionConfigurations } from '@engine/config/music-regions-config';\nimport type { NpcDetails, NpcPresetConfiguration } from '@engine/config/npc-config';\nimport { loadNpcConfigurations, translateNpcServerConfig } from '@engine/config/npc-config';\nimport type { NpcSpawn } from '@engine/config/npc-spawn-config';\nimport { loadNpcSpawnConfigurations } from '@engine/config/npc-spawn-config';\nimport type { Shop } from '@engine/config/shop-config';\nimport { loadShopConfigurations } from '@engine/config/shop-config';\nimport { questMap } from '@engine/plugins/loader';\nimport type { Quest } from '@engine/world/actor/player/quest';\nimport { logger } from '@runejs/common';\nimport type { ObjectConfig, XteaRegion } from '@runejs/filestore';\nimport { loadXteaRegionFiles } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\nimport _ from 'lodash';\n\nexport let itemMap: { [key: string]: ItemDetails };\nexport let itemGroupMap: Record<string, Record<string, boolean>>;\nexport let itemIdMap: { [key: number]: string };\nexport let objectMap: { [key: number]: ObjectConfig };\nexport let itemPresetMap: ItemPresetConfiguration;\nexport let npcMap: { [key: string]: NpcDetails };\nexport let npcIdMap: { [key: number]: string };\nexport let npcPresetMap: NpcPresetConfiguration;\nexport let npcSpawns: NpcSpawn[] = [];\nexport let musicRegions: MusicTrack[] = [];\nexport let itemSpawns: ItemSpawn[] = [];\nexport let shopMap: { [key: string]: Shop };\nexport let xteaRegions: { [key: number]: XteaRegion };\n\nexport const musicRegionMap = new Map<number, number>();\nexport const widgets: { [key: string]: any } = require('../../../data/config/widgets.json');\n\nexport async function loadCoreConfigurations(): Promise<void> {\n    xteaRegions = await loadXteaRegionFiles('data/config/xteas');\n}\n\nexport async function loadGameConfigurations(): Promise<void> {\n    logger.info(`Loading server configurations...`);\n\n    const { items, itemIds, itemPresets, itemGroups } = await loadItemConfigurations('data/config/items/');\n    itemMap = items;\n    itemGroupMap = itemGroups;\n    itemIdMap = itemIds;\n    itemPresetMap = itemPresets;\n\n    const { npcs, npcIds, npcPresets } = await loadNpcConfigurations('data/config/npcs/');\n    npcMap = npcs;\n    npcIdMap = npcIds;\n    npcPresetMap = npcPresets;\n\n    npcSpawns = await loadNpcSpawnConfigurations('data/config/npc-spawns/');\n    musicRegions = await loadMusicRegionConfigurations();\n    musicRegions.forEach(song => song.regionIds.forEach(region => musicRegionMap.set(region, song.songId)));\n    itemSpawns = await loadItemSpawnConfigurations('data/config/item-spawns/');\n\n    shopMap = await loadShopConfigurations('data/config/shops/');\n\n    objectMap = {};\n\n    logger.info(\n        `Loaded ${musicRegions.length} music regions, ${Object.keys(itemMap).length} items, ${itemSpawns.length} item spawns, ` +\n            `${Object.keys(npcMap).length} npcs, ${npcSpawns.length} npc spawns, and ${Object.keys(shopMap).length} shops.`,\n    );\n}\n\n/**\n * find all items in all select groups\n * @param groupKeys array of string of which to find items connected with\n * @return itemsKeys array of itemkeys in all select groups\n */\nexport const findItemTagsInGroups = (groupKeys: string[]): string[] => {\n    return Object.keys(\n        groupKeys.reduce<Record<string, boolean>>((all, groupKey) => {\n            const items = itemGroupMap[groupKey] || {};\n            return { ...all, ...items };\n        }, {}),\n    );\n};\n\n/**\n * find all items which are shared by all the groups, and discard items not in all groups\n * @param groupKeys groups keys which to find items shared by\n * @return itemKeys of items shared by all groups\n */\nexport const findItemTagsInGroupFilter = (groupKeys: string[]): string[] => {\n    if (!groupKeys || groupKeys.length === 0) {\n        return [];\n    }\n    let collection: Record<string, boolean> | undefined = undefined;\n    groupKeys.forEach(groupKey => {\n        if (!collection) {\n            collection = { ...(itemGroupMap[groupKey] || {}) };\n            return;\n        }\n        const current = itemGroupMap[groupKey] || {};\n\n        Object.keys(collection).forEach(existingItemKey => {\n            if (!(existingItemKey in current) && collection) {\n                delete collection[existingItemKey];\n            }\n        });\n    });\n\n    return Object.keys(collection || {});\n};\n\nexport const findItem = (itemKey: number | string): ItemDetails | null => {\n    if (!itemKey) {\n        return null;\n    }\n\n    let gameId: number | null = null;\n    if (typeof itemKey === 'number') {\n        gameId = itemKey;\n        itemKey = itemIdMap[gameId];\n\n        if (!itemKey) {\n            logger.warn(`Item ${gameId} is not yet registered on the server.`);\n        }\n    }\n\n    let item;\n\n    if (itemKey) {\n        item = itemMap[itemKey];\n        if (!item) {\n            // Try fetching variation with suffix 0\n            item = itemMap[`${itemKey}:0`];\n        }\n        if (item?.gameId) {\n            gameId = item.gameId;\n        }\n    }\n\n    if (gameId) {\n        const cacheItem = filestore.configStore.itemStore.getItem(gameId);\n        item = _.merge(item, cacheItem);\n    }\n\n    return item ? new ItemDetails(item) : null;\n};\n\nexport const findNpc = (inputKey: number | string): NpcDetails => {\n    if (!inputKey) {\n        throw new Error('No NPC was provided to findNpc.');\n    }\n\n    // Pathway for finding an NPC by its game id\n    if (typeof inputKey === 'number') {\n        const gameId = inputKey;\n        const npcKey = npcIdMap[gameId];\n\n        // If we can't find a config in the project for this NPC - we fallback\n        // to the cache which is the basic info loaded by `fileserver`.\n        if (!npcKey) {\n            const cacheNpc = filestore.configStore.npcStore.getNpc(gameId);\n            if (cacheNpc) {\n                return cacheNpc;\n            } else {\n                logger.warn(`NPC ${gameId} is not yet configured on the server and a matching cache NPC was not found.`);\n                throw new Error(`NPC ${gameId} is not yet configured on the server and a matching cache NPC was not found.`);\n            }\n        }\n    }\n\n    // Otherwise we got a string identifier for the npcs\n    let npc = npcMap[inputKey];\n    if (!npc) {\n        // Try fetching variation with suffix 0\n        npc = npcMap[`${npc}:0`];\n    }\n\n    if (!npc) {\n        logger.warn(`NPC ${inputKey} is not yet configured on the server and a matching cache NPC was not provided.`);\n        throw new Error(`NPC ${inputKey} is not yet configured on the server and a matching cache NPC was not provided.`);\n    }\n\n    if (npc.extends) {\n        let extensions = npc.extends;\n        if (typeof extensions === 'string') {\n            extensions = [extensions];\n        }\n\n        extensions.forEach(extKey => {\n            const extensionNpc = npcPresetMap[extKey];\n            if (extensionNpc) {\n                npc = _.merge(npc, translateNpcServerConfig(undefined, extensionNpc));\n            }\n        });\n    }\n\n    return npc;\n};\n\nexport const findObject = (objectId: number): ObjectConfig | null => {\n    if (!objectMap[objectId]) {\n        const object = filestore.objectStore.getObject(objectId);\n        if (!object) {\n            return null;\n        }\n\n        objectMap[objectId] = object;\n        return object;\n    } else {\n        return objectMap[objectId];\n    }\n};\n\nexport const findShop = (shopKey: string): Shop | null => {\n    if (!shopKey) {\n        return null;\n    }\n\n    return shopMap[shopKey] || null;\n};\n\nexport const findQuest = (questId: string): Quest | null => {\n    const questKey = Object.keys(questMap).find(quest => quest.toLocaleLowerCase() === questId.toLocaleLowerCase());\n\n    return questKey ? questMap[questKey] : null;\n};\n\nexport const findMusicTrack = (trackId: number): MusicTrack | null => {\n    return musicRegions.find(track => track.songId === trackId) || null;\n};\n\nexport const findMusicTrackByButtonId = (buttonId: number): MusicTrack | null => {\n    return musicRegions.find(track => track.musicTabButtonId === buttonId) || null;\n};\n\nexport const findSongIdByRegionId = (regionId: number): number | null => {\n    return musicRegionMap.get(regionId) || null;\n};\n"
  },
  {
    "path": "src/engine/config/data-dump.ts",
    "content": "import { writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { logger } from '@runejs/common';\nimport type { ItemConfig, NpcConfig, ObjectConfig, WidgetBase } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\n\nexport interface DataDumpResult {\n    successful: boolean;\n    filePath: string;\n}\n\nfunction dump<T>(fileName: string, definitions: T[]): DataDumpResult {\n    const filePath = join('data/dump', fileName);\n\n    const arr: T[] = [];\n    for (let i = 0; i < definitions.length; i++) {\n        arr.push(definitions[i]);\n    }\n\n    try {\n        writeFileSync(filePath, JSON.stringify(arr, null, 4));\n        return {\n            successful: true,\n            filePath,\n        };\n    } catch (error) {\n        logger.error(`Error dumping ${fileName}`);\n        return {\n            successful: false,\n            filePath,\n        };\n    }\n}\n\nexport const dumpNpcs = (): DataDumpResult => {\n    return dump<NpcConfig>('npcs.json', filestore.configStore.npcStore.decodeNpcStore());\n};\n\nexport const dumpItems = (): DataDumpResult => {\n    return dump<ItemConfig>('items.json', filestore.configStore.itemStore.decodeItemStore());\n};\n\nexport const dumpObjects = (): DataDumpResult => {\n    return dump<ObjectConfig>('objects.json', filestore.configStore.objectStore.decodeObjectStore());\n};\n\nexport const dumpWidgets = (): DataDumpResult => {\n    return dump<WidgetBase>('widgets.json', filestore.widgetStore.decodeWidgetStore());\n};\n"
  },
  {
    "path": "src/engine/config/directories.ts",
    "content": "import { join } from 'path';\n\nexport const BUILD_DIR = join('.', 'dist', 'engine');\n"
  },
  {
    "path": "src/engine/config/item-config.ts",
    "content": "import { deepMerge } from '@engine/util/objects';\nimport type { SkillName } from '@engine/world/actor/skills';\nimport { logger } from '@runejs/common';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\n\nexport type WeaponStyle =\n    | 'axe'\n    | 'hammer'\n    | 'bow'\n    | 'claws'\n    | 'crossbow'\n    | 'gun'\n    | 'slash_sword'\n    | '2h_sword'\n    | 'pickaxe'\n    | 'halberd'\n    | 'polestaff'\n    | 'scythe'\n    | 'spear'\n    | 'mace'\n    | 'dagger'\n    | 'magical_staff'\n    | 'darts'\n    | 'unarmed'\n    | 'whip';\n\nexport const weaponWidgetIds: Record<WeaponStyle, number> = {\n    axe: 75,\n    hammer: 76,\n    bow: 77,\n    claws: 78,\n    crossbow: 79,\n    gun: 80,\n    slash_sword: 81,\n    '2h_sword': 82,\n    pickaxe: 83,\n    halberd: 84,\n    polestaff: 85,\n    scythe: 86,\n    spear: 87,\n    mace: 88,\n    dagger: 89,\n    magical_staff: 90,\n    darts: 91,\n    unarmed: 92,\n    whip: 93,\n};\n\nexport type EquipmentSlot =\n    | 'head'\n    | 'back'\n    | 'neck'\n    | 'main_hand'\n    | 'off_hand'\n    | 'torso'\n    | 'legs'\n    | 'hands'\n    | 'feet'\n    | 'ring'\n    | 'quiver'\n    | '2h';\n\nexport const equipmentIndices = {\n    head: 0,\n    back: 1,\n    neck: 2,\n    main_hand: 3,\n    torso: 4,\n    off_hand: 5,\n    legs: 7,\n    hands: 9,\n    feet: 10,\n    ring: 12,\n    quiver: 13,\n};\n\nexport const equipmentIndex = (equipmentSlot: EquipmentSlot): number => equipmentIndices[equipmentSlot];\nexport const getEquipmentSlot = (index: number): EquipmentSlot =>\n    Object.keys(equipmentIndices).find(key => equipmentIndices[key] === index) as EquipmentSlot;\n\nexport type EquipmentType = 'hat' | 'helmet' | 'torso' | 'full_top' | 'one_handed' | 'two_handed';\n\nexport interface ItemRequirements {\n    skills?: { [key: string]: number };\n    quests?: { [key: string]: number };\n}\n\nexport interface OffensiveBonuses {\n    speed?: number;\n    stab?: number;\n    slash?: number;\n    crush?: number;\n    magic?: number;\n    ranged?: number;\n}\n\nexport interface DefensiveBonuses {\n    stab?: number;\n    slash?: number;\n    crush?: number;\n    magic?: number;\n    ranged?: number;\n}\n\nexport interface SkillBonuses {\n    [key: string]: number;\n}\n\nexport interface WeaponInfo {\n    style: WeaponStyle;\n    playerAnimations: any;\n}\n\nexport interface ItemMetadata {\n    [key: string]: unknown;\n\n    consume_effects?: {\n        replaced_by?: string;\n        clock: string; // Name of timer to be used for cooldown\n        skills?: {\n            [key in SkillName]: number | [number, number];\n        };\n        energy?: number | [number, number];\n        special: boolean;\n    };\n\n    /**\n     * If defined, the 'bury bones' plugin will assign experience according\n     * to this value.\n     */\n    prayerBuryXp?: number;\n\n    /**\n     * If the full URL is `\"https://oldschool.runescape.wiki/w/Bat_bones\"`\n     * then the value is `\"Bat_bones\"`.\n     *\n     * @example \"Bat_bones\"\n     */\n    wikiId?: string;\n}\n\nexport interface EquipmentData {\n    equipmentSlot: EquipmentSlot;\n    equipmentType?: EquipmentType;\n    requirements?: ItemRequirements;\n    offensiveBonuses?: OffensiveBonuses;\n    defensiveBonuses?: DefensiveBonuses;\n    skillBonuses?: SkillBonuses;\n    weaponInfo?: WeaponInfo;\n}\n\nexport interface ItemPresetConfiguration {\n    [key: string]: ItemConfiguration;\n}\n\nexport interface ItemConfiguration {\n    extends?: string | string[];\n    game_id?: number;\n    examine?: string;\n    tradable?: boolean;\n    variations?: [\n        {\n            suffix: string;\n        } & ItemConfiguration,\n    ];\n    weight?: number;\n    equippable?: boolean;\n    consumable?: boolean;\n    destroy?: string | boolean;\n    groups?: string[];\n    equipment_data?: {\n        equipment_slot: EquipmentSlot;\n        equipment_type?: EquipmentType;\n        requirements?: ItemRequirements;\n        offensive_bonuses?: OffensiveBonuses;\n        defensive_bonuses?: DefensiveBonuses;\n        skill_bonuses?: SkillBonuses;\n        weapon_info?: WeaponInfo;\n    };\n    metadata?: ItemMetadata;\n}\n\n/**\n * Full server + cache details about a specific game item.\n */\nexport class ItemDetails {\n    extends?: string | string[];\n    key: string;\n    gameId: number;\n    name: string = '';\n    examine: string = '';\n    tradable: boolean = false;\n    equippable: boolean = false;\n    destroy?: string | boolean;\n    weight: number;\n    equipmentData: EquipmentData;\n    metadata: ItemMetadata = {};\n    consumable?: boolean;\n    stackable: boolean = false;\n    value: number = 0;\n    groups: string[] = [];\n    members: boolean = false;\n    groundOptions: string[] = [];\n    inventoryOptions: string[] = [];\n    teamId: number;\n    bankNoteId: number;\n    bankNoteTemplate: number;\n    stackableIds: number[];\n    stackableAmounts: number[];\n\n    public constructor(item?: ItemDetails) {\n        if (item) {\n            const keys = Object.keys(item);\n            keys.forEach(key => (this[key] = item[key]));\n        }\n    }\n\n    get lowAlchValue(): number {\n        return Math.floor(this.value * 0.4);\n    }\n\n    get highAlchValue(): number {\n        return Math.floor(this.lowAlchValue * 1.5);\n    }\n\n    get minimumValue(): number {\n        return Math.floor(this.lowAlchValue * 0.25);\n    }\n}\n\nexport function translateItemConfig(key: string | undefined, config: ItemConfiguration): any {\n    return {\n        key,\n        extends: config.extends || undefined,\n        gameId: config.game_id,\n        examine: config.examine,\n        tradable: config.tradable,\n        equippable: config.equippable,\n        weight: config.weight,\n        destroy: config.destroy || undefined,\n        groups: config.groups || [],\n        consumable: config.consumable,\n        equipmentData: config.equipment_data\n            ? {\n                  equipmentType: config.equipment_data?.equipment_type || undefined,\n                  equipmentSlot: config.equipment_data?.equipment_slot || undefined,\n                  requirements: config.equipment_data?.requirements || undefined,\n                  offensiveBonuses: config.equipment_data?.offensive_bonuses || undefined,\n                  defensiveBonuses: config.equipment_data?.defensive_bonuses || undefined,\n                  skillBonuses: config.equipment_data?.skill_bonuses || undefined,\n                  weaponInfo: config.equipment_data?.weapon_info || undefined,\n              }\n            : undefined,\n        metadata: config.metadata ? { ...config.metadata } : {},\n    };\n}\n\nexport async function loadItemConfigurations(path: string): Promise<{\n    items: { [key: string]: ItemDetails };\n    itemIds: { [key: number]: string };\n    itemPresets: ItemPresetConfiguration;\n    itemGroups: Record<string, Record<string, boolean>>;\n}> {\n    const itemIds: { [key: number]: string } = {};\n    const items: { [key: string]: ItemDetails } = {};\n    const itemGroups: Record<string, Record<string, boolean>> = {}; // Record where key is group id, and value is an array of all itemstags in group\n    let itemPresets: ItemPresetConfiguration = {};\n\n    const files = await loadConfigurationFiles<ItemConfiguration>(path);\n    const itemConfigurations: Record<string, ItemConfiguration> = {};\n\n    files.forEach(itemConfigs => {\n        const itemKeys = Object.keys(itemConfigs);\n        itemKeys.forEach(key => {\n            if (key === 'presets') {\n                itemPresets = { ...itemPresets, ...itemConfigs[key] };\n            } else {\n                itemConfigurations[key] = itemConfigs[key] as ItemConfiguration;\n            }\n        });\n    });\n    Object.entries(itemConfigurations).forEach(([key, itemConfig]) => {\n        if (itemConfig.game_id !== undefined && !isNaN(itemConfig.game_id)) {\n            itemIds[itemConfig.game_id] = key;\n            let item = { ...translateItemConfig(key, itemConfig) };\n            if (item?.extends) {\n                let extensions = item.extends;\n                if (typeof extensions === 'string') {\n                    extensions = [extensions];\n                }\n\n                extensions.forEach(extKey => {\n                    const extensionItem = itemPresets[extKey];\n                    if (extensionItem) {\n                        const preset = translateItemConfig(undefined, extensionItem);\n                        item = deepMerge(item, preset);\n                    }\n                });\n            }\n            items[key] = item;\n            item.groups.forEach(group => {\n                if (!itemGroups[group]) {\n                    itemGroups[group] = {};\n                }\n                itemGroups[group][key] = true;\n            });\n        }\n\n        if (itemConfig.variations) {\n            for (const subItem of itemConfig.variations) {\n                if (!subItem.game_id) {\n                    logger.warn(`Item ${key} has a variation without a game_id. Skipping.`);\n                    continue;\n                }\n\n                const subKey = subItem.suffix ? key + ':' + subItem.suffix : key;\n                const baseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(key, itemConfig) }));\n                const subBaseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(subKey, subItem) }));\n                itemIds[subItem.game_id] = subKey;\n\n                if (!items[subKey]) {\n                    let item = deepMerge(baseItem, subBaseItem);\n                    if (item?.extends) {\n                        let extensions = item.extends;\n                        if (typeof extensions === 'string') {\n                            extensions = [extensions];\n                        }\n\n                        extensions.forEach(extKey => {\n                            const extensionItem = itemPresets[extKey];\n                            if (extensionItem) {\n                                const preset = translateItemConfig(undefined, extensionItem);\n                                item = deepMerge(item, preset);\n                            }\n                        });\n                    }\n                    items[subKey] = item;\n                    items[subKey].groups.forEach(group => {\n                        if (!itemGroups[group]) {\n                            itemGroups[group] = {};\n                        }\n                        itemGroups[group][subKey] = true;\n                    });\n                } else {\n                    logger.warn(`Duplicate item key ${subKey} found - the item was not loaded.`);\n                }\n            }\n        }\n    });\n\n    return { items, itemIds, itemPresets, itemGroups };\n}\n"
  },
  {
    "path": "src/engine/config/item-spawn-config.ts",
    "content": "import { Position } from '@engine/world/position';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\n\nexport interface ItemSpawnConfiguration {\n    item: string;\n    amount?: number;\n    spawn_x: number;\n    spawn_y: number;\n    spawn_level?: number;\n    instance?: 'global' | 'player';\n    respawn?: number;\n    metadata?: { [key: string]: unknown };\n}\n\nexport class ItemSpawn {\n    public itemKey: string;\n    public amount: number = 1;\n    public spawnPosition: Position;\n    public instance: 'global' | 'player' = 'global';\n    public respawn: number = 30;\n    public metadata: { [key: string]: unknown } = {};\n\n    public constructor(itemKey: string, position: Position) {\n        this.itemKey = itemKey;\n        this.spawnPosition = position;\n    }\n}\n\nexport function translateItemSpawnConfig(config: ItemSpawnConfiguration): ItemSpawn {\n    const spawn = new ItemSpawn(config.item, new Position(config.spawn_x, config.spawn_y, config.spawn_level || 0));\n    if (config.amount !== undefined) {\n        spawn.amount = config.amount;\n    }\n    if (config.instance !== undefined) {\n        spawn.instance = config.instance;\n    }\n    if (config.respawn !== undefined) {\n        spawn.respawn = config.respawn;\n    }\n    if (config.metadata !== undefined) {\n        spawn.metadata = config.metadata;\n    }\n\n    return spawn;\n}\n\nexport async function loadItemSpawnConfigurations(path: string): Promise<ItemSpawn[]> {\n    const itemSpawns: ItemSpawn[] = [];\n\n    const files = await loadConfigurationFiles<ItemSpawnConfiguration[]>(path);\n    files.forEach(spawns => spawns.forEach(itemSpawn => itemSpawns.push(translateItemSpawnConfig(itemSpawn))));\n\n    return itemSpawns;\n}\n"
  },
  {
    "path": "src/engine/config/music-regions-config.ts",
    "content": "import * as musicRegionsFile from '../../../data/config/music/musicRegions.json';\n\nexport interface MusicRegionsConfiguration {\n    songId: number;\n    songName: string;\n    musicTabButtonId: number;\n    regionIds: number[];\n}\n\nexport class MusicTrack {\n    public songId: number;\n    public songName: string;\n    public musicTabButtonId: number;\n    public regionIds: number[];\n\n    public constructor(songId: number, songName: string, musicTabButtonId: number, regionIds: number[]) {\n        this.songId = songId;\n        this.songName = songName;\n        this.musicTabButtonId = musicTabButtonId;\n        this.regionIds = regionIds;\n    }\n}\n\nexport function translateMusicRegionsConfig(config: MusicRegionsConfiguration): MusicTrack {\n    return new MusicTrack(config.songId, config.songName, config.musicTabButtonId, config.regionIds);\n}\n\nexport async function loadMusicRegionConfigurations(): Promise<MusicTrack[]> {\n    const regions: MusicTrack[] = [];\n\n    await musicRegionsFile.musicRegions.forEach(musicRegion => regions.push(translateMusicRegionsConfig(musicRegion)));\n    return regions;\n}\n"
  },
  {
    "path": "src/engine/config/npc-config.ts",
    "content": "import type { QuestRequirement } from '@engine/action/hook/action-hook';\nimport type { DefensiveBonuses } from '@engine/config/item-config';\nimport { logger } from '@runejs/common';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\nimport { NpcConfig } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\nimport _ from 'lodash';\n\nexport interface NpcSkills {\n    [key: string]: number;\n}\n\nexport interface OffensiveStats {\n    speed?: number;\n    attack?: number;\n    strength?: number;\n    magic?: number;\n    magicStrength?: number;\n    ranged?: number;\n    rangedStrength?: number;\n}\n\nexport interface NpcCombatAnimations {\n    attack?: number | number[];\n    defend?: number;\n    death?: number;\n}\n\nexport interface DropTable {\n    itemKey: string;\n    frequency: string;\n    amount?: number;\n    amountMax?: number;\n    questRequirement?: QuestRequirement;\n}\n\nexport interface NpcPresetConfiguration {\n    [key: string]: NpcServerConfig;\n}\n\nexport interface NpcServerConfig {\n    extends?: string | string[];\n    game_id: number;\n    skills?: NpcSkills;\n    killable?: boolean;\n    respawn_time?: number;\n    offensive_stats?: {\n        speed?: number;\n        attack?: number;\n        strength?: number;\n        magic?: number;\n        magic_strength?: number;\n        ranged?: number;\n        ranged_strength?: number;\n    };\n    defensive_stats?: DefensiveBonuses;\n    variations?: [\n        {\n            suffix: string;\n        } & NpcServerConfig,\n    ];\n    animations?: NpcCombatAnimations;\n    drop_table?: DropTable[];\n    metadata: { [key: string]: unknown };\n}\n\n/**\n * Full server + cache details about a specific game NPC.\n */\nexport class NpcDetails extends NpcConfig {\n    extends?: string | string[];\n    key?: string;\n    skills?: NpcSkills;\n    killable?: boolean;\n    respawnTime?: number;\n    offensiveStats?: OffensiveStats;\n    defensiveStats?: DefensiveBonuses;\n    combatAnimations?: NpcCombatAnimations;\n    dropTable?: DropTable[] = [];\n    metadata?: { [key: string]: unknown } = {};\n\n    public constructor(defaultValues: { [key: string]: any }) {\n        super();\n        Object.keys(defaultValues).forEach(key => (this[key] = defaultValues[key]));\n    }\n}\n\nexport function translateNpcServerConfig(npcKey: string | undefined, config: NpcServerConfig): NpcDetails {\n    return new NpcDetails({\n        key: npcKey,\n        extends: config.extends || undefined,\n        skills: config.skills || {},\n        killable: config.killable || false,\n        respawnTime: config.respawn_time || 1,\n        offensiveStats: config.offensive_stats\n            ? {\n                  speed: config.offensive_stats.speed || undefined,\n                  attack: config.offensive_stats.attack || undefined,\n                  strength: config.offensive_stats.strength || undefined,\n                  magic: config.offensive_stats.magic || undefined,\n                  magicStrength: config.offensive_stats.magic_strength || undefined,\n                  ranged: config.offensive_stats.ranged || undefined,\n                  rangedStrength: config.offensive_stats.ranged_strength || undefined,\n              }\n            : undefined,\n        defensiveStats: config.defensive_stats || undefined,\n        combatAnimations: config.animations || {},\n        dropTable: config.drop_table || undefined,\n        metadata: config.metadata || {},\n    });\n}\n\nexport async function loadNpcConfigurations(path: string): Promise<{\n    npcs: { [key: string]: NpcDetails };\n    npcIds: { [key: number]: string };\n    npcPresets: NpcPresetConfiguration;\n}> {\n    const npcIds: { [key: number]: string } = {};\n    const npcs: { [key: string]: NpcDetails } = {};\n    let npcPresets: NpcPresetConfiguration = {};\n\n    const files = await loadConfigurationFiles<{ presets: NpcPresetConfiguration | undefined } & { [key: string]: NpcServerConfig }>(path);\n\n    files.forEach(npcConfigs => {\n        const npcKeys = Object.keys(npcConfigs);\n        npcKeys.forEach(key => {\n            if (key === 'presets') {\n                npcPresets = { ...npcPresets, ...npcConfigs[key] };\n            } else {\n                const npcConfig = npcConfigs[key] as NpcServerConfig;\n                if (!isNaN(npcConfig.game_id)) {\n                    npcIds[npcConfig.game_id] = key;\n                    npcs[key] = {\n                        ...translateNpcServerConfig(key, npcConfig),\n                        ...filestore.configStore.npcStore.getNpc(npcConfig.game_id),\n                    };\n                }\n                if (npcConfig.variations) {\n                    for (const variation of npcConfig.variations) {\n                        try {\n                            const subKey = key + ':' + variation.suffix;\n                            const baseItem = JSON.parse(\n                                JSON.stringify({\n                                    ...translateNpcServerConfig(key, npcConfig),\n                                    ...filestore.configStore.npcStore.getNpc(npcConfig.game_id),\n                                }),\n                            );\n\n                            const subBaseItem = JSON.parse(\n                                JSON.stringify({\n                                    ...translateNpcServerConfig(subKey, variation),\n                                    ...filestore.configStore.npcStore.getNpc(variation.game_id),\n                                }),\n                            );\n                            npcIds[variation.game_id] = subKey;\n                            npcs[subKey] = _.merge(baseItem, subBaseItem);\n                        } catch (error) {\n                            logger.error(`Error registering npc variant ${key}_${variation.suffix}`);\n                            logger.error(error);\n                        }\n                    }\n                }\n            }\n        });\n    });\n\n    return { npcs, npcIds, npcPresets };\n}\n"
  },
  {
    "path": "src/engine/config/npc-spawn-config.ts",
    "content": "import type { Direction } from '@engine/world/direction';\nimport { Position } from '@engine/world/position';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\n\nexport interface NpcSpawnConfiguration {\n    npc: string;\n    spawn_x: number;\n    spawn_y: number;\n    spawn_level?: number;\n    movement_radius?: number;\n    face?: Direction;\n}\n\nexport class NpcSpawn {\n    public npcKey: string;\n    public spawnPosition: Position;\n    public movementRadius: number;\n    public faceDirection: Direction;\n\n    public constructor(npcKey: string, spawnPosition: Position, movementRadius: number = 0, faceDirection: Direction = 'WEST') {\n        this.npcKey = npcKey;\n        this.spawnPosition = spawnPosition;\n        this.movementRadius = movementRadius;\n        this.faceDirection = faceDirection;\n    }\n}\n\nexport function translateNpcSpawnConfig(config: NpcSpawnConfiguration): NpcSpawn {\n    return new NpcSpawn(\n        config.npc,\n        new Position(config.spawn_x, config.spawn_y, config.spawn_level || 0),\n        config.movement_radius || 0,\n        config.face || 'WEST',\n    );\n}\n\nexport async function loadNpcSpawnConfigurations(path: string): Promise<NpcSpawn[]> {\n    const npcSpawns: NpcSpawn[] = [];\n\n    const files = await loadConfigurationFiles<NpcSpawnConfiguration[]>(path);\n    files.forEach(spawns => spawns.forEach(npcSpawn => npcSpawns.push(translateNpcSpawnConfig(npcSpawn))));\n\n    return npcSpawns;\n}\n"
  },
  {
    "path": "src/engine/config/quest-config.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\nexport type QuestKey = number | 'complete';\n\nexport type QuestStageHandler = {\n    [key in QuestKey]?: (player: Player) => void | Promise<void>;\n};\n\nexport type QuestDialogueHandler = {\n    [key in QuestKey]?: (player: Player, npc: Npc) => void | Promise<void>;\n};\n\nexport type QuestJournalHandler = {\n    [key in QuestKey]?: ((player: Player) => Promise<string>) | ((player: Player) => string) | string;\n};\n\nexport interface QuestCompletion {\n    questCompleteWidget: {\n        rewardText?: string[];\n        modelId?: number;\n        itemId?: number;\n        modelRotationX?: number;\n        modelRotationY?: number;\n        modelZoom?: number;\n    };\n    giveRewards?: ((player?: Player) => void) | ((player?: Player) => Promise<void>);\n}\n\nexport class PlayerQuest {\n    public readonly questId: string;\n    public progress: QuestKey = 0;\n    public complete: boolean = false;\n    public readonly metadata: { [key: string]: any } = {};\n\n    public constructor(questId: string) {\n        this.questId = questId;\n    }\n}\n\nexport function questDialogueActionFactory(\n    questId: string,\n    npcDialogueHandler: QuestDialogueHandler,\n    stageHandler: (player: Player) => Promise<void>,\n): npcInteractionActionHandler {\n    return async ({ player, npc }) => {\n        const quest = player.getQuest(questId);\n        if (!quest) {\n            return;\n        }\n\n        const progress = quest.progress;\n        const dialogueHandler = npcDialogueHandler[progress];\n        if (dialogueHandler) {\n            try {\n                await dialogueHandler(player, npc);\n            } catch (e) {\n                logger.error(e);\n            }\n\n            await stageHandler(player);\n        }\n    };\n}\n"
  },
  {
    "path": "src/engine/config/shop-config.testskip.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport type { Shop, ShopConfiguration } from '@engine/config/shop-config';\nimport { shopFactory } from '@engine/config/shop-config';\nimport { setupConfig } from '@server/game/game-server';\n\n// @todo fix this by mocking the world instead of spinning it up\ndescribe('shopping', () => {\n    const shopConfig: ShopConfiguration = {\n        name: 'Test Shop',\n        general_store: false,\n        shop_sell_rate: 1.0,\n        shop_buy_rate: 0.55,\n        rate_modifier: 0.02,\n        stock: [\n            {\n                itemKey: 'rs:battlestaff',\n                amount: 5,\n                restock: 100,\n            },\n            {\n                itemKey: 'rs:staff',\n                amount: 5,\n                restock: 100,\n            },\n            {\n                itemKey: 'rs:magic_staff',\n                amount: 5,\n                restock: 200,\n            },\n            {\n                itemKey: 'rs:staff_of_air',\n                amount: 2,\n                restock: 1000,\n            },\n            {\n                itemKey: 'rs:staff_of_water',\n                amount: 2,\n                restock: 1000,\n            },\n            {\n                itemKey: 'rs:staff_of_earth',\n                amount: 2,\n                restock: 1000,\n            },\n            {\n                itemKey: 'rs:staff_of_fire',\n                amount: 2,\n                restock: 1000,\n            },\n        ],\n    };\n\n    const shopname = 'rs:test_shop';\n    let shop: Shop;\n    let world;\n\n    beforeAll(async () => {\n        world = await setupConfig();\n    });\n\n    beforeEach(() => {\n        shop = shopFactory(shopname, shopConfig);\n    });\n\n    describe('shop object', () => {\n        test('shop should exist', () => {\n            expect(shop?.key).toEqual(shopname);\n        });\n        test('shop stock should be correct', () => {\n            expect(shopConfig.stock.reduce((all, curr) => all && shop.isItemSoldHere(curr.itemKey), true)).toBeTruthy();\n            expect(shop.isItemSoldHere('rs:salmon')).toBeFalsy();\n        });\n        test('player should buy item from shop at correct value', () => {\n            // Player purchases, shop sells\n\n            console.log(shop.container.items.map(a => a?.itemId));\n\n            const battleStaffItem = findItem('rs:battlestaff');\n            if (!battleStaffItem) {\n                throw new Error('battleStaffItem not found in server config');\n            }\n            const shopBStaffIndex = shop.container.findIndex(battleStaffItem.gameId);\n            const shopBstaff = shop.container.items[shopBStaffIndex];\n            if (!shopBstaff) {\n                throw new Error('Battle staff item not found in shop config');\n            }\n            expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(Math.round(battleStaffItem.value * (shopConfig.shop_sell_rate || 1.0)));\n            expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(7000); // BStaff standard price\n            expect(shopBstaff.amount).toBe(5);\n            shopBstaff.amount += 1;\n            expect(shopBstaff.amount).toBe(6);\n            expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(6860);\n            shopBstaff.amount -= 3;\n            expect(shopBstaff.amount).toBe(3);\n            expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(7280);\n        });\n        test('shop should sell at correct value', () => {\n            // Player sells, shop pays\n\n            const battleStaffItem = findItem('rs:battlestaff');\n            if (!battleStaffItem) {\n                throw new Error('battleStaffItem not found in server config');\n            }\n            const shopBStaffIndex = shop.container.findIndex(battleStaffItem.gameId);\n            const shopBstaff = shop.container.items[shopBStaffIndex];\n            if (!shopBstaff) {\n                throw new Error('Battle staff item not found in shop config');\n            }\n\n            // shopSpade.amount = 2;\n\n            expect(shopBstaff.amount).toBe(5);\n            expect(shop.getSellToShopPrice(battleStaffItem)).toBe(3850);\n            shopBstaff.amount = 4;\n            expect(shopBstaff.amount).toBe(4);\n            expect(shop.getSellToShopPrice(battleStaffItem)).toBe(3990);\n\n            shopBstaff.amount = 1;\n            expect(shopBstaff.amount).toBe(1);\n            expect(shop.getSellToShopPrice(battleStaffItem)).toBe(4410);\n\n            shopBstaff.amount = 16;\n            expect(shopBstaff.amount).toBe(16);\n            expect(shop.getSellToShopPrice(battleStaffItem)).toBe(2310);\n\n            shopBstaff.amount = 3;\n            expect(shopBstaff.amount).toBe(3);\n            expect(shop.getSellToShopPrice(battleStaffItem)).toBe(4130);\n        });\n    });\n});\n"
  },
  {
    "path": "src/engine/config/shop-config.ts",
    "content": "import { findItem, widgets } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport type { WidgetClosedEvent } from '@engine/interface/interface-state';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ContainerUpdateEvent } from '@engine/world/items/item-container';\nimport { ItemContainer } from '@engine/world/items/item-container';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\nimport type { Subscription } from 'rxjs';\n\nexport type ShopStock = { itemKey: string; amount: number; restock?: number }[];\n\nexport class Shop {\n    public readonly key: string;\n    public readonly originalStock: ShopStock;\n    public readonly container: ItemContainer;\n    public readonly originalSellRate: number;\n    public readonly originalBuyRate: number;\n    public readonly generalStore: boolean;\n    public name: string;\n    public sellRate: number;\n    public buyRate: number;\n    public rateModifier: number;\n    private customers: Player[];\n    private containerSubscription: Subscription;\n\n    public constructor(\n        key: string,\n        name: string,\n        generalStore: boolean,\n        stock: ShopStock,\n        sellRate: number,\n        buyRate: number,\n        modifier: number,\n    ) {\n        this.key = key;\n        this.name = name;\n        this.generalStore = generalStore;\n        this.buyRate = buyRate;\n        this.sellRate = sellRate;\n        this.originalBuyRate = buyRate;\n        this.originalSellRate = sellRate;\n        this.rateModifier = modifier;\n        this.originalStock = stock;\n        this.container = new ItemContainer(40);\n        this.containerSubscription = this.container.containerUpdated.subscribe((_update: ContainerUpdateEvent) => this.updateCustomers());\n        this.customers = [];\n        this.resetShopStock();\n    }\n\n    public resetShopStock(): void {\n        for (let i = 0; i < this.container.size; i++) {\n            if (this.originalStock[i]) {\n                const { itemKey, amount } = this.originalStock[i];\n                const itemDetails = findItem(itemKey);\n                this.container.set(i, !itemDetails ? null : { itemId: itemDetails.gameId, amount: amount || 0 }, false);\n            } else {\n                this.container.set(i, null, false);\n            }\n        }\n    }\n\n    /**\n     * Get the price to purchase an item from shop\n     * @param item to purchase from shop\n     * @return price which item is available for purchase at\n     */\n    public getBuyFromShopPrice(item: ItemDetails): number {\n        const itemKey = item.key;\n        const itemSoldHere: boolean = this.isItemSoldHere(itemKey);\n        let originalStockAmount: number = 0;\n        const itemStock = this.container.amount(item.gameId);\n\n        if (itemSoldHere) {\n            const foundStock = this.originalStock.find(stock => stock && stock.itemKey === itemKey);\n            if (foundStock) {\n                originalStockAmount = foundStock.amount;\n            }\n        } else {\n            return -1; // Cannot buy from this shop\n        }\n\n        let finalAmount: number;\n        if (itemStock === originalStockAmount) {\n            finalAmount = Math.round(item.value * this.sellRate);\n        } else if (itemStock > originalStockAmount) {\n            const overstockAmount = itemStock - originalStockAmount;\n            let shopSellRate = this.sellRate;\n            shopSellRate -= this.rateModifier * overstockAmount;\n            finalAmount = item.value * shopSellRate;\n\n            finalAmount = Math.round(finalAmount);\n        } else {\n            const understockAmount = originalStockAmount - itemStock;\n            let shopSellRate = this.sellRate;\n            shopSellRate += this.rateModifier * understockAmount;\n            finalAmount = item.value * shopSellRate;\n\n            finalAmount = Math.round(finalAmount);\n        }\n\n        const min = item.minimumValue || 1;\n        return finalAmount < min ? min : finalAmount;\n    }\n\n    /**\n     * Price which shop pays player for.\n     * @param item\n     */\n    public getSellToShopPrice(item: ItemDetails): number {\n        const itemKey = item.key;\n        const itemSoldHere: boolean = this.isItemSoldHere(itemKey);\n        let originalStockAmount: number = 0;\n        const itemStock = this.container.amount(item.gameId);\n\n        if (itemSoldHere) {\n            originalStockAmount = this.originalStock.find(stock => stock && stock.itemKey === itemKey)?.amount || 0;\n        } else if (!this.generalStore) {\n            return -1; // Can not sell this item to this shop (shop is not a general store!)\n        }\n\n        let finalAmount: number;\n\n        if (itemStock === originalStockAmount) {\n            finalAmount = Math.round(item.value * this.buyRate);\n        } else if (itemStock > originalStockAmount) {\n            const overstockAmount = itemStock - originalStockAmount;\n            let shopBuyRate = this.buyRate;\n            shopBuyRate -= this.rateModifier * overstockAmount;\n            finalAmount = item.value * shopBuyRate;\n\n            finalAmount = Math.round(finalAmount);\n        } else {\n            const understockAmount = originalStockAmount - itemStock;\n            let shopBuyRate = this.buyRate;\n            shopBuyRate += this.rateModifier * understockAmount;\n            finalAmount = item.value * shopBuyRate;\n\n            finalAmount = Math.round(finalAmount);\n        }\n\n        const min = item.minimumValue || 0;\n        return finalAmount < min ? min : finalAmount;\n    }\n\n    public isItemSoldHere(itemKey: string): boolean {\n        return this.originalStock.some(stockedItem => stockedItem.itemKey === itemKey);\n    }\n\n    public open(player: Player): void {\n        player.metadata.lastOpenedShopKey = this.key;\n        player.metadata.shopCloseListener = player.interfaceState.closed.subscribe((whatClosed: WidgetClosedEvent) => {\n            if (whatClosed && whatClosed.widget && whatClosed.widget.widgetId === widgets.shop.widgetId) {\n                this.removePlayerFromShop(player);\n            }\n        });\n\n        player.outgoingPackets.updateWidgetString(widgets.shop.widgetId, widgets.shop.title, this.name);\n        player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, this.container);\n        player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, player.inventory);\n\n        player.interfaceState.openWidget(widgets.shop.widgetId, {\n            slot: 'screen',\n            multi: true,\n        });\n        player.interfaceState.openWidget(widgets.shopPlayerInventory.widgetId, {\n            slot: 'tabarea',\n            multi: true,\n        });\n        this.customers.push(player);\n    }\n\n    private updateCustomers() {\n        for (const player of this.customers) {\n            if (player.metadata.lastOpenedShopKey === this.key) {\n                player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, this.container);\n            } else {\n                this.removePlayerFromShop(player);\n            }\n        }\n    }\n\n    private removePlayerFromShop(player: Player) {\n        if (player.metadata.lastOpenedShopKey === this.key) {\n            delete player.metadata.lastOpenedShopKey;\n            player.metadata.shopCloseListener?.unsubscribe();\n        }\n        this.customers = this.customers.filter(c => c !== player);\n    }\n}\n\nexport interface ShopConfiguration {\n    name: string;\n    general_store?: boolean;\n    shop_sell_rate?: number;\n    shop_buy_rate?: number;\n    rate_modifier?: number;\n    stock: ShopStock;\n}\n\nexport function shopFactory(key: string, config: ShopConfiguration): Shop {\n    return new Shop(\n        key,\n        config.name,\n        config.general_store || false,\n        config.stock,\n        config.shop_sell_rate || 1.0,\n        config.shop_buy_rate || 0.65,\n        config.rate_modifier || 0.2,\n    );\n}\n\nexport async function loadShopConfigurations(path: string): Promise<{ [key: string]: Shop }> {\n    const shops: { [key: string]: Shop } = {};\n\n    const files = await loadConfigurationFiles<{ [key: string]: ShopConfiguration }>(path);\n    files.forEach(shopConfigs => {\n        const shopKeys = Object.keys(shopConfigs);\n        shopKeys.forEach(key => {\n            shops[key] = shopFactory(key, shopConfigs[key]);\n        });\n    });\n\n    return shops;\n}\n"
  },
  {
    "path": "src/engine/interface/interface-state.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport type { ItemContainer } from '@engine/world/items/item-container';\nimport { logger } from '@runejs/common';\nimport { Subject, lastValueFrom } from 'rxjs';\nimport { filter, take } from 'rxjs/operators';\n\nexport type TabType =\n    | 'combat'\n    | 'skills'\n    | 'quests'\n    | 'inventory'\n    | 'equipment'\n    | 'prayers'\n    | 'spells'\n    | 'friends'\n    | 'ignores'\n    | 'logout'\n    | 'emotes'\n    | 'settings'\n    | 'music';\n\nexport type GameInterfaceSlot = 'full' | 'screen' | 'chatbox' | 'tabarea';\n\nexport const tabIndex: { [key: string]: number } = {\n    combat: 0,\n    skills: 1,\n    quests: 2,\n    inventory: 3,\n    equipment: 4,\n    prayers: 5,\n    spells: 6,\n    friends: 8,\n    ignores: 9,\n    logout: 10,\n    emotes: 11,\n    settings: 12,\n    music: 13,\n};\n\nexport interface WidgetOptions {\n    slot: GameInterfaceSlot;\n    multi?: boolean;\n    queued?: boolean;\n    containerId?: number;\n    container?: ItemContainer;\n    fakeWidget?: number;\n    metadata?: { [key: string]: any };\n    doNotRegister?: boolean;\n}\n\nexport class Widget {\n    public widgetId: number;\n    public slot: GameInterfaceSlot;\n    public multi: boolean = false;\n    public queued: boolean = false;\n    public containerId: number;\n    public container: ItemContainer | null = null;\n    public fakeWidget?: number;\n    public metadata: { [key: string]: any };\n\n    public constructor(interfaceId: number, options: WidgetOptions) {\n        const { slot, multi, queued, containerId, container, fakeWidget, metadata } = options;\n\n        this.widgetId = interfaceId;\n        this.fakeWidget = fakeWidget;\n        this.slot = slot;\n        this.multi = multi || false;\n        this.queued = queued || false;\n        this.containerId = containerId || -1;\n        this.container = container || null;\n        this.metadata = { ...metadata };\n    }\n}\n\nexport interface WidgetClosedEvent {\n    widget: Widget;\n    widgetId?: number;\n    data?: number;\n}\n\n/**\n * Control's a Player's Game Interface state.\n */\nexport class InterfaceState {\n    public readonly tabs: { [key: string]: Widget | null };\n    public readonly widgetSlots: { [key: string]: Widget | null };\n    public readonly closed: Subject<WidgetClosedEvent> = new Subject<WidgetClosedEvent>();\n    private readonly player: Player;\n    private _screenOverlayWidget: number | null;\n    private _chatOverlayWidget: number | null;\n\n    public constructor(player: Player) {\n        this.tabs = {\n            combat: null,\n            skills: null,\n            quests: null,\n            inventory: null,\n            equipment: null,\n            prayers: null,\n            spells: null,\n            friends: null,\n            ignores: null,\n            logout: null,\n            emotes: null,\n            settings: null,\n            music: null,\n        };\n\n        this.widgetSlots = {};\n        this._screenOverlayWidget = null;\n        this._chatOverlayWidget = null;\n        this.clearSlots();\n\n        this.player = player;\n    }\n\n    public openChatOverlayWidget(widgetId: number): void {\n        this._chatOverlayWidget = widgetId;\n        this.player.outgoingPackets.showChatDialogue(widgetId);\n    }\n\n    public closeChatOverlayWidget(): void {\n        this._chatOverlayWidget = null;\n        this.player.outgoingPackets.showChatDialogue(-1);\n    }\n\n    public openScreenOverlayWidget(widgetId: number): void {\n        this._screenOverlayWidget = widgetId;\n        this.player.outgoingPackets.showScreenOverlayWidget(widgetId);\n    }\n\n    public closeScreenOverlayWidget(): void {\n        this._screenOverlayWidget = null;\n        this.player.outgoingPackets.showScreenOverlayWidget(-1);\n    }\n\n    public async widgetClosed(slot: GameInterfaceSlot): Promise<WidgetClosedEvent> {\n        return await lastValueFrom(\n            this.closed\n                .asObservable()\n                .pipe(filter(event => event.widget.slot === slot))\n                .pipe(take(1)),\n        );\n    }\n\n    public closeWidget(slot: GameInterfaceSlot, widgetId?: number, data?: number): void {\n        let widget: Widget | null = null;\n\n        if (slot) {\n            widget = this.widgetSlots[slot];\n        } else {\n            if (!widgetId) {\n                throw new Error('Invalid widget close request: no slot or widgetId provided');\n            }\n\n            widget = this.findWidget(widgetId);\n        }\n\n        if (!widget) {\n            return;\n        }\n\n        this.closed.next({ widget, widgetId, data });\n        this.widgetSlots[widget.slot] = null;\n    }\n\n    public openWidget(widgetId: number, options: WidgetOptions): Widget {\n        // if(this.widgetOpen(options.slot, widgetId)) {\n        //     return;\n        // }\n\n        const widget = new Widget(widgetId, options);\n\n        if (widget.queued) {\n            // @TODO queued widgets\n        }\n\n        if (widget.slot === 'full' || !widget.multi) {\n            this.clearSlots();\n        }\n\n        if (!options.doNotRegister) {\n          this.widgetSlots[widget.slot] = widget;\n        }\n        this.showWidget(widget);\n        return widget;\n    }\n\n    public setTab(type: TabType, widget: Widget | number | null): void {\n        if (widget && typeof widget === 'number') {\n            // Create a new tab interface instance\n\n            let container: ItemContainer | undefined;\n            if (type === 'inventory') {\n                container = this.player.inventory;\n            } else if (type === 'equipment') {\n                container = this.player.equipment;\n            }\n\n            widget = new Widget(widget, {\n                slot: 'tabarea',\n                multi: true,\n                container,\n            });\n        }\n\n        widget = (widget as Widget) || null;\n\n        this.tabs[type] = widget;\n        this.player.outgoingPackets.sendTabWidget(tabIndex[type], widget === null ? -1 : widget.widgetId);\n    }\n\n    public getTab(type: TabType): Widget | null {\n        return this.tabs[type] || null;\n    }\n\n    public findWidget(widgetId: number): Widget | null {\n        const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[];\n        let widget: Widget | null = null;\n        slots.forEach(slot => {\n            if (this.widgetSlots[slot]?.widgetId === widgetId) {\n                widget = this.widgetSlots[slot];\n            }\n        });\n        return widget || null;\n    }\n\n    public widgetOpen(slot?: GameInterfaceSlot, widgetId?: number): boolean {\n        if (!slot) {\n            const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[];\n            return slots.some(s => this.getWidget(s) !== null);\n        }\n\n        if (widgetId === undefined) {\n            return this.getWidget(slot) !== null;\n        } else {\n            return this.getWidget(slot)?.widgetId === widgetId;\n        }\n    }\n\n    public getWidget(slot: GameInterfaceSlot): Widget | null {\n        return this.widgetSlots[slot] || null;\n    }\n\n    public closeAllSlots(): void {\n        const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[];\n        slots.forEach(slot => this.closeWidget(slot));\n        this.player.outgoingPackets.closeActiveWidgets();\n    }\n\n    private showWidget(widget: Widget): void {\n        const { outgoingPackets: packets } = this.player;\n        const { widgetId, containerId, slot, multi } = widget;\n\n        if (slot === 'full' || !multi) {\n            this.closeOthers(slot);\n        }\n\n        if (slot === 'full' && containerId !== undefined) {\n            packets.showFullscreenWidget(widgetId, containerId);\n        } else if (slot === 'screen') {\n            const tabWidget = this.getWidget('tabarea');\n            if (multi && tabWidget) {\n                packets.showScreenAndTabWidgets(widgetId, tabWidget.widgetId);\n            } else {\n                packets.showStandaloneScreenWidget(widgetId);\n            }\n        } else if (slot === 'chatbox') {\n            if (multi) {\n                // Dialogue Widget\n                packets.showChatDialogue(widgetId);\n            } else {\n                // Chatbox Widget\n                packets.showChatboxWidget(widgetId);\n            }\n        } else if (slot === 'tabarea') {\n            const screenWidget = this.getWidget('screen');\n\n            if (!screenWidget) {\n                logger.error(`Tried to open tab widget ${widgetId} without a screen widget open`);\n                return;\n            }\n\n            if (multi) {\n                packets.showScreenAndTabWidgets(screenWidget.widgetId, widgetId);\n            } else {\n                packets.showTabWidget(widgetId);\n            }\n        }\n    }\n\n    private closeOthers(openSlot: GameInterfaceSlot): void {\n        const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots).filter(slot => slot !== openSlot) as GameInterfaceSlot[];\n        slots.forEach(slot => this.closeWidget(slot));\n    }\n\n    private clearSlots(): void {\n        this.widgetSlots.full = null;\n        this.widgetSlots.screen = null;\n        this.widgetSlots.chatbox = null;\n        this.widgetSlots.tabarea = null;\n    }\n\n    public get fullScreenWidget(): Widget | null {\n        return this.widgetSlots.full || null;\n    }\n\n    public get screenWidget(): Widget | null {\n        return this.widgetSlots.screen || null;\n    }\n\n    public get chatboxWidget(): Widget | null {\n        return this.widgetSlots.chatbox || null;\n    }\n\n    public get tabareaWidget(): Widget | null {\n        return this.widgetSlots.tabarea || null;\n    }\n\n    public get screenOverlayWidget(): number | null {\n        return this._screenOverlayWidget;\n    }\n}\n"
  },
  {
    "path": "src/engine/net/inbound-packet-handler.ts",
    "content": "import { BUILD_DIR } from '@engine/config/directories';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ByteBuffer } from '@runejs/common';\nimport { logger } from '@runejs/common';\nimport { getFiles } from '../util/files';\n\ninterface InboundPacket {\n    opcode: number;\n    size: number;\n    handler: (player: Player, packet: PacketData) => void;\n}\n\nexport interface PacketData {\n    packetId: number;\n    packetSize: number;\n    buffer: ByteBuffer;\n}\n\nexport const incomingPackets = new Map<number, InboundPacket>();\n\nexport const PACKET_DIRECTORY = `${BUILD_DIR}/net/inbound-packets`;\n\nexport async function loadPackets(): Promise<Map<number, InboundPacket>> {\n    incomingPackets.clear();\n\n    for await (const path of getFiles(PACKET_DIRECTORY, ['.packet.js'], true)) {\n        const location = './inbound-packets' + path.substring(PACKET_DIRECTORY.length).replace('.js', '');\n        const packet = require(location).default;\n        if (Array.isArray(packet)) {\n            packet.forEach(p => incomingPackets.set(p.opcode, p));\n        } else {\n            incomingPackets.set(packet.opcode, packet);\n        }\n    }\n\n    return incomingPackets;\n}\n\nexport function handlePacket(player: Player, packetId: number, packetSize: number, buffer: ByteBuffer): boolean {\n    const incomingPacket = incomingPackets.get(packetId);\n\n    if (!incomingPacket) {\n        logger.info(`Unknown packet ${packetId} with size ${packetSize} received.`);\n        return false;\n    }\n\n    new Promise<void>(resolve => {\n        try {\n            incomingPacket.handler(player, { packetId, packetSize, buffer });\n        } catch (error) {\n            logger.error(`Error handling inbound packet ${packetId} with size ${packetSize}`);\n            logger.error(error);\n        }\n        resolve();\n    });\n    return true;\n}\n"
  },
  {
    "path": "src/engine/net/inbound-packets/add-friend.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { longToString } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport default {\n    opcode: 114,\n    size: 8,\n    handler: (player: Player, packet: PacketData) => player.addFriend(longToString(BigInt(packet.buffer.get('LONG')))),\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/add-ignore.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { longToString } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport default {\n    opcode: 251,\n    size: 8,\n    handler: (player: Player, packet: PacketData) => player.addIgnoredPlayer(longToString(BigInt(packet.buffer.get('LONG')))),\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/blinking-tab-click.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst blinkingTabClickPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const tabIndex = buffer.get('byte');\n\n    const tabClickEventIndex = player.metadata?.tabClickEvent?.tabIndex || -1;\n\n    if (tabClickEventIndex === tabIndex) {\n        if (player.metadata.tabClickEvent) {\n            player.metadata.tabClickEvent.event.next(true);\n        }\n    }\n};\n\nexport default {\n    opcode: 44,\n    size: 1,\n    handler: blinkingTabClickPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/button-click.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst buttonClickPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const widgetId = buffer.get('short');\n    const buttonId = buffer.get('short');\n\n    player.actionPipeline.call('button', player, widgetId, buttonId);\n};\n\nexport default {\n    opcode: 64,\n    size: 4,\n    handler: buttonClickPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/character-design.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst characterDesignPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n\n    // @TODO verify validity of selections\n\n    const gender = buffer.get('byte');\n    const models = new Array(7);\n    const colors = new Array(5);\n\n    for (let i = 0; i < models.length; i++) {\n        models[i] = buffer.get('byte');\n    }\n\n    for (let i = 0; i < colors.length; i++) {\n        colors[i] = buffer.get('byte');\n    }\n\n    player.appearance = {\n        gender,\n        head: models[0],\n        facialHair: models[1],\n        torso: models[2],\n        arms: models[3],\n        hands: models[4],\n        legs: models[5],\n        feet: models[6],\n        hairColor: colors[0],\n        torsoColor: colors[1],\n        legColor: colors[2],\n        feetColor: colors[3],\n        skinColor: colors[4],\n    };\n\n    player.updateFlags.appearanceUpdateRequired = true;\n    player.interfaceState.closeAllSlots();\n};\n\nexport default {\n    opcode: 231,\n    size: 13,\n    handler: characterDesignPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/chat.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst chatPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    buffer.get('byte');\n    const color = buffer.get('byte');\n    const effects = buffer.get('byte');\n    const data = Buffer.from(buffer.getSlice(buffer.readerIndex, buffer.length - buffer.readerIndex));\n    player.updateFlags.addChatMessage({ color, effects, data });\n};\n\nexport default {\n    opcode: 75,\n    size: -3,\n    handler: chatPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/command.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { type Player, Rights } from '@engine/world/actor/player/player';\n\nconst commandPacket = (player: Player, packet: PacketData) => {\n    const input = packet.buffer.getString();\n\n    if (!input || input.trim().length === 0) {\n        return;\n    }\n\n    const isConsole = packet.packetId === 246;\n\n    const args = input.trim().split(' ');\n    const command = args[0];\n\n    args.splice(0, 1);\n    if (player.rights !== Rights.ADMIN) {\n        player.sendLogMessage('You need to be an administrator to use commands.', isConsole);\n    } else {\n        player.actionPipeline.call('player_command', player, command, isConsole, args);\n    }\n};\n\nexport default [\n    {\n        opcode: 246,\n        size: -3,\n        handler: commandPacket,\n    },\n    {\n        opcode: 248,\n        size: -1,\n        handler: commandPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/drop-item.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst dropItemPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const widgetId = buffer.get('short', 'u', 'le');\n    const containerId = buffer.get('short', 'u', 'le');\n    const slot = buffer.get('short', 'u');\n    const itemId = buffer.get('short', 'u', 'le');\n\n    player.actionPipeline.call('item_interaction', player, itemId, slot, widgetId, containerId, 'drop');\n};\n\nexport default {\n    opcode: 29,\n    size: 8,\n    handler: dropItemPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/examine.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst examinePacket = (player: Player, packet: PacketData) => {\n    const { packetId, buffer } = packet;\n    const id = buffer.get('short', 's', 'le');\n\n    let message;\n\n    if (packetId === 151) {\n        message = activeWorld.examine.getItem(id);\n    } else if (packetId === 148) {\n        message = activeWorld.examine.getObject(id);\n    } else if (packetId === 247) {\n        message = activeWorld.examine.getNpc(id);\n    }\n\n    if (message) {\n        player.sendMessage(message);\n    }\n};\n\nexport default [\n    {\n        opcode: 148,\n        size: 2,\n        handler: examinePacket,\n    },\n    {\n        opcode: 151,\n        size: 2,\n        handler: examinePacket,\n    },\n    {\n        opcode: 247,\n        size: 2,\n        handler: examinePacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-interaction.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { getItemOption } from '@engine/world/items/item';\n\nconst option1 = buffer => {\n    const itemId = buffer.get('short', 'u');\n    const slot = buffer.get('short', 'u', 'le');\n    const widgetId = buffer.get('short', 's', 'le');\n    const containerId = buffer.get('short', 's', 'le');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst option2 = buffer => {\n    const itemId = buffer.get('short', 'u', 'le');\n    const containerId = buffer.get('short', 's', 'le');\n    const widgetId = buffer.get('short', 's', 'le');\n    const slot = buffer.get('short', 'u', 'le');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst option3 = buffer => {\n    const slot = buffer.get('short', 'u');\n    const containerId = buffer.get('short', 's', 'le');\n    const widgetId = buffer.get('short', 's', 'le');\n    const itemId = buffer.get('short', 'u');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst option4 = buffer => {\n    const itemId = buffer.get('short', 'u');\n    const slot = buffer.get('short', 'u', 'le');\n    const containerId = buffer.get('short', 's', 'le');\n    const widgetId = buffer.get('short', 's', 'le');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst option5 = buffer => {\n    const containerId = buffer.get('short', 's', 'le');\n    const widgetId = buffer.get('short', 's', 'le');\n    const slot = buffer.get('short', 'u', 'le');\n    const itemId = buffer.get('short', 'u');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst inventoryOption1 = buffer => {\n    const slot = buffer.get('short', 'u', 'le');\n    const itemId = buffer.get('short', 's', 'le');\n    const containerId = buffer.get('short', 's', 'le');\n    const widgetId = buffer.get('short', 'u');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst inventoryOption4 = buffer => {\n    const slot = buffer.get('short', 'u');\n    const widgetId = buffer.get('short', 's', 'le');\n    const containerId = buffer.get('short', 's', 'le');\n    const itemId = buffer.get('short', 'u');\n    return { widgetId, containerId, itemId, slot };\n};\n\nconst itemInteractionPacket = (player: Player, packet: PacketData) => {\n    const { packetId } = packet;\n    const packets = {\n        38: { packetDef: option1, optionNumber: 1 },\n        228: { packetDef: option2, optionNumber: 2 },\n        26: { packetDef: option3, optionNumber: 3 },\n        147: { packetDef: option4, optionNumber: 4 },\n        102: { packetDef: option5, optionNumber: 2 },\n        98: { packetDef: inventoryOption4, optionNumber: 4 },\n        240: { packetDef: inventoryOption1, optionNumber: 1 },\n    };\n\n    const packetDetails = packets[packetId];\n    const { widgetId, containerId, itemId, slot } = packetDetails.packetDef(packet.buffer);\n\n    const option = getItemOption(itemId, packetDetails.optionNumber, { widgetId, containerId });\n\n    player.actionPipeline.call('item_interaction', player, itemId, slot, widgetId, containerId, option);\n};\n\nexport default [\n    {\n        opcode: 38,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 98,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 228,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 26,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 147,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 240,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n    {\n        opcode: 102,\n        size: 8,\n        handler: itemInteractionPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-on-item.packet.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\nconst itemOnItemPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const usedWithItemId = buffer.get('short', 'u', 'le');\n    const usedWithSlot = buffer.get('short', 'u', 'le');\n    const usedWithContainerId = buffer.get('short', 's', 'le');\n    const usedWithWidgetId = buffer.get('short', 's', 'le');\n    const usedContainerId = buffer.get('short', 's', 'le');\n    const usedWidgetId = buffer.get('short', 's', 'le');\n    const usedItemId = buffer.get('short', 'u', 'le');\n    const usedSlot = buffer.get('short', 'u');\n\n    if (\n        usedWidgetId === widgets.inventory.widgetId &&\n        usedContainerId === widgets.inventory.containerId &&\n        usedWithWidgetId === widgets.inventory.widgetId &&\n        usedWithContainerId === widgets.inventory.containerId\n    ) {\n        if (usedSlot < 0 || usedSlot > 27 || usedWithSlot < 0 || usedWithSlot > 27) {\n            return;\n        }\n\n        const usedItem = player.inventory.items[usedSlot];\n        const usedWithItem = player.inventory.items[usedWithSlot];\n        if (!usedItem || !usedWithItem) {\n            return;\n        }\n\n        if (usedItem.itemId !== usedItemId || usedWithItem.itemId !== usedWithItemId) {\n            return;\n        }\n\n        player.actionPipeline.call('item_on_item', player, usedItem, usedSlot, usedWidgetId, usedWithItem, usedWithSlot, usedWithWidgetId);\n    } else {\n        logger.warn(\n            `Unhandled item on item case using widgets ${usedWidgetId}:${usedContainerId} => ${usedWithWidgetId}:${usedWithContainerId}`,\n        );\n    }\n};\n\nexport default {\n    opcode: 40,\n    size: 16,\n    handler: itemOnItemPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-on-npc.packet.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { World } from '@engine/world/world';\nimport { logger } from '@runejs/common';\n\nconst itemOnNpcPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const npcIndex = buffer.get('short', 'u');\n    const itemId = buffer.get('short', 'u');\n    const itemSlot = buffer.get('short', 'u', 'le');\n    const itemWidgetId = buffer.get('short');\n    const itemContainerId = buffer.get('short');\n\n    let usedItem;\n    if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) {\n        if (itemSlot < 0 || itemSlot > 27) {\n            return;\n        }\n\n        usedItem = player.inventory.items[itemSlot];\n        if (!usedItem) {\n            return;\n        }\n\n        if (usedItem.itemId !== itemId) {\n            return;\n        }\n    } else {\n        logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`);\n    }\n\n    if (npcIndex < 0 || npcIndex > World.MAX_NPCS - 1) {\n        return;\n    }\n\n    const npc = activeWorld.npcList[npcIndex];\n    if (!npc) {\n        return;\n    }\n\n    const position = npc.position;\n    const distance = Math.floor(position.distanceBetween(player.position));\n\n    // Too far away\n    if (distance > 16) {\n        return;\n    }\n\n    player.actionPipeline.call('item_on_npc', player, npc, position, usedItem, itemWidgetId, itemContainerId);\n};\n\nexport default {\n    opcode: 208,\n    size: 10,\n    handler: itemOnNpcPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-on-object.packet.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { getVarbitMorphIndex } from '@engine/util/varbits';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\nimport { filestore } from '@server/game/game-server';\n\nconst itemOnObjectPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const objectY = buffer.get('short', 'u', 'le');\n    const itemId = buffer.get('short', 'u');\n    const objectId = buffer.get('short', 'u', 'le');\n    const itemSlot = buffer.get('short', 'u', 'le');\n    const itemWidgetId = buffer.get('short', 's', 'le');\n    const itemContainerId = buffer.get('short', 's', 'le');\n    const objectX = buffer.get('short', 'u', 'le');\n\n    let usedItem;\n    if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) {\n        if (itemSlot < 0 || itemSlot > 27) {\n            return;\n        }\n\n        usedItem = player.inventory.items[itemSlot];\n        if (!usedItem) {\n            return;\n        }\n\n        if (usedItem.itemId !== itemId) {\n            return;\n        }\n    } else {\n        logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`);\n    }\n\n    const level = player.position.level;\n    const objectPosition = new Position(objectX, objectY, level);\n\n    const { object: locationObject, cacheOriginal } = activeWorld.findObjectAtLocation(player, objectId, objectPosition);\n    if (!locationObject) {\n        return;\n    }\n\n    let objectConfig = filestore.configStore.objectStore.getObject(objectId);\n\n    if (!objectConfig) {\n        logger.error(`Could not find object config for object id ${objectId}!`);\n    } else if (objectConfig.configChangeDest) {\n        let morphIndex = -1;\n        if (objectConfig.varbitId === -1) {\n            if (objectConfig.configId !== -1) {\n                const configValue =\n                    player.metadata.configs && player.metadata.configs[objectConfig.configId]\n                        ? player.metadata.configs[objectConfig.configId]\n                        : 0;\n                morphIndex = configValue;\n            }\n        } else {\n            morphIndex = getVarbitMorphIndex(objectConfig.varbitId, player.metadata.configs);\n        }\n        if (morphIndex !== -1) {\n            objectConfig = filestore.configStore.objectStore.getObject(objectConfig.configChangeDest[morphIndex]);\n        }\n    }\n\n    player.actionPipeline.call(\n        'item_on_object',\n        player,\n        locationObject,\n        objectConfig,\n        objectPosition,\n        usedItem,\n        itemWidgetId,\n        itemContainerId,\n        cacheOriginal,\n    );\n};\n\nexport default {\n    opcode: 24,\n    size: 14,\n    handler: itemOnObjectPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-on-player.packet.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { World } from '@engine/world/world';\nimport { logger } from '@runejs/common';\n\nconst itemOnPlayerPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const playerIndex = buffer.get('short', 'u', 'le') - 1;\n    const itemWidgetId = buffer.get('short', 's', 'le');\n    const itemContainerId = buffer.get('short');\n    const itemId = buffer.get('short', 'u');\n    const itemSlot = buffer.get('short', 'u');\n\n    let usedItem;\n    if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) {\n        if (itemSlot < 0 || itemSlot > 27) {\n            return;\n        }\n\n        usedItem = player.inventory.items[itemSlot];\n        if (!usedItem) {\n            return;\n        }\n\n        if (usedItem.itemId !== itemId) {\n            return;\n        }\n    } else {\n        logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`);\n    }\n\n    if (playerIndex < 0 || playerIndex > World.MAX_PLAYERS - 1) {\n        return;\n    }\n\n    const otherPlayer = activeWorld.playerList[playerIndex];\n    if (!otherPlayer) {\n        return;\n    }\n\n    const position = otherPlayer.position;\n    const distance = Math.floor(position.distanceBetween(player.position));\n\n    // Too far away\n    if (distance > 16) {\n        return;\n    }\n\n    player.actionPipeline.call('item_on_player', player, otherPlayer, position, usedItem, itemWidgetId, itemContainerId);\n};\n\nexport default {\n    opcode: 110,\n    size: 10,\n    handler: itemOnPlayerPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-on-world-item.packet.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\n\n/**\n * Parses the item on world item packet and calls the `item_on_world_item` action pipeline.\n *\n * This will check that the item being used is in the player's inventory, and that the world item exists in the correct location.\n * The action pipeline will not be called if either of these conditions are not met.\n *\n * @param player The player that sent the packet.\n * @param packet The packet to parse.\n *\n * @author jameskmonger\n */\nconst itemOnWorldItemPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n\n    const usedWithX = buffer.get('short', 'u');\n    const usedSlot = buffer.get('short', 'u');\n    const usedWithItemId = buffer.get('short', 'u');\n    const usedContainerId = buffer.get('short', 's', 'be');\n    const usedWidgetId = buffer.get('short', 's', 'be');\n    const usedWithY = buffer.get('short', 'u', 'le');\n    const usedItemId = buffer.get('short', 'u', 'le');\n\n    const position = new Position(usedWithX, usedWithY, player.position.level);\n\n    if (usedWidgetId === widgets.inventory.widgetId && usedContainerId === widgets.inventory.containerId) {\n        // TODO (James) we should use constants for these rather than magic numbers\n        if (usedSlot < 0 || usedSlot > 27) {\n            return;\n        }\n\n        const usedItem = player.inventory.items[usedSlot];\n        const usedWithItem = player.instance.getTileModifications(position).mods.worldItems.find(p => p.itemId === usedWithItemId);\n        if (!usedItem || !usedWithItem) {\n            logger.warn(\n                `Unhandled item on world item case (A) for ${usedSlot} (${usedItemId}) on ${usedWithItemId} (${usedWithX}, ${usedWithY}) by ${player.username}`,\n            );\n            return;\n        }\n\n        if (usedItem.itemId !== usedItemId || usedWithItem.itemId !== usedWithItemId) {\n            logger.warn(\n                `Unhandled item on world item case (B) for ${usedItem.itemId}:${usedItemId} on ${usedWithItem.itemId}:${usedWithItemId} by ${player.username}`,\n            );\n            return;\n        }\n\n        player.actionPipeline.call('item_on_world_item', player, usedItem, usedWithItem, usedWidgetId, usedContainerId, usedSlot);\n    } else {\n        logger.warn(`Unhandled item on world item case (C) using widgets ${usedWidgetId}:${usedContainerId} by ${player.username}`);\n    }\n};\n\nexport default {\n    opcode: 172,\n    size: 14,\n    handler: itemOnWorldItemPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/item-swap.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst itemSwapPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const swapType = buffer.get('byte');\n    const fromSlot = buffer.get('short', 'u');\n    const toSlot = buffer.get('short', 'u', 'le');\n    const containerId = buffer.get('short');\n    const widgetId = buffer.get('short');\n\n    if (toSlot < 0 || fromSlot < 0) {\n        return;\n    }\n\n    if (swapType === 0) {\n        player.actionPipeline.call('item_swap', player, fromSlot, toSlot, { widgetId, containerId });\n    } else if (swapType === 1) {\n        player.actionPipeline.call('move_item', player, fromSlot, toSlot, { widgetId, containerId });\n    }\n};\n\nexport default {\n    opcode: 83,\n    size: 9,\n    handler: itemSwapPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/junk.packet.ts",
    "content": "const handler = () => {};\n\nexport default [\n    {\n        opcode: 234,\n        size: 4,\n        handler,\n    },\n    {\n        opcode: 160,\n        size: 1,\n        handler,\n    },\n    {\n        opcode: 216,\n        size: 0,\n        handler,\n    },\n    {\n        opcode: 13,\n        size: 0,\n        handler,\n    },\n    {\n        opcode: 58, // camera move\n        size: 4,\n        handler,\n    },\n    {\n        opcode: 121,\n        size: 4,\n        handler,\n    },\n    {\n        opcode: 178,\n        size: 0,\n        handler,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/magic-attack.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst magicAttackPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const npcWorldIndex = buffer.get('short', 'u'); // unsigned short BE\n    const widgetId = buffer.get('short', 'u', 'le'); // unsigned short LE\n    const widgetChildId = buffer.get('byte'); // unsigned short LE\n\n    const npc = activeWorld.npcList[npcWorldIndex];\n\n    player.actionPipeline.call('magic_on_npc', npc, player, widgetId, widgetChildId);\n};\n\nexport default [\n    {\n        opcode: 253,\n        size: 6,\n        handler: magicAttackPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/npc-interaction.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { World } from '@engine/world/world';\nimport { logger } from '@runejs/common';\n\nconst npcInteractionPacket = (player: Player, packet: PacketData) => {\n    const { buffer, packetId } = packet;\n\n    const packetReaders: Record<number, () => number> = {\n        63: () => buffer.get('short', 'u', 'le'),\n        116: () => buffer.get('short', 'u', 'le'),\n        57: () => buffer.get('short', 'u'),\n        /*42: 'readUnsignedShortLE',\n        8: 'readUnsignedShortLE'*/\n    };\n    const npcIndex = packetReaders[packetId]();\n\n    if (npcIndex < 0 || npcIndex > World.MAX_NPCS - 1) {\n        return;\n    }\n\n    const npc = activeWorld.npcList[npcIndex];\n    if (!npc) {\n        return;\n    }\n\n    const position = npc.position;\n    const distance = Math.floor(position.distanceBetween(player.position));\n\n    // Too far away\n    if (distance > 16) {\n        return;\n    }\n\n    const actions = {\n        63: 0, // Usually the Talk-to option\n        57: 1, // Usually the Attack option\n        116: 2, // Usually the Pickpocket option\n        /*42: 3,\n        8: 4*/\n    };\n\n    const morphedNpc = player.getMorphedNpcDetails(npc);\n    const options = morphedNpc?.options || npc.options;\n\n    const actionIdx = actions[packetId];\n    let optionName = `action-${actionIdx + 1}`;\n    if (options && options.length >= actionIdx) {\n        if (!options[actionIdx] || options[actionIdx].toLowerCase() === 'hidden') {\n            // Invalid action\n            logger.info(npc);\n            logger.error(`1: Invalid npc ${morphedNpc?.gameId || npc.id} option ${actionIdx + 1}, options: ${JSON.stringify(options)}`);\n            if (morphedNpc) {\n                logger.warn(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`);\n            }\n            return;\n        }\n\n        optionName = options[actionIdx];\n    } else {\n        // Invalid action\n        logger.error(`2: Invalid npc ${morphedNpc?.gameId || npc.id} option ${actionIdx + 1}, options: ${JSON.stringify(options)}`);\n        if (morphedNpc) {\n            logger.warn(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`);\n        }\n        return;\n    }\n\n    player.actionPipeline.call('npc_interaction', player, npc, position, optionName.toLowerCase());\n};\n\nexport default [\n    {\n        opcode: 63,\n        size: 2,\n        handler: npcInteractionPacket,\n    },\n    {\n        opcode: 116,\n        size: 2,\n        handler: npcInteractionPacket,\n    },\n    {\n        opcode: 57,\n        size: 2,\n        handler: npcInteractionPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/number-input.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport default {\n    opcode: 238,\n    size: 4,\n    handler: (player: Player, packet: PacketData) => player.numericInputEvent.next(packet.buffer.get('int', 'u')),\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/object-interaction.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { getVarbitMorphIndex } from '@engine/util/varbits';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Rights } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\nimport { filestore } from '@server/game/game-server';\n\ninterface ObjectInteractionData {\n    objectId: number;\n    x: number;\n    y: number;\n}\n\ntype objectInteractionPacket = (packet: PacketData) => ObjectInteractionData;\n\nconst option1: objectInteractionPacket = packet => {\n    const { buffer } = packet;\n    const objectId = buffer.get('short', 'u');\n    const y = buffer.get('short', 'u');\n    const x = buffer.get('short', 'u', 'le');\n    return { objectId, x, y };\n};\n\nconst option2: objectInteractionPacket = packet => {\n    const { buffer } = packet;\n    const x = buffer.get('short', 'u', 'le');\n    const y = buffer.get('short', 'u', 'le');\n    const objectId = buffer.get('short', 'u', 'le');\n    return { objectId, x, y };\n};\n\nconst option3: objectInteractionPacket = packet => {\n    const { buffer } = packet;\n    const y = buffer.get('short', 'u');\n    const objectId = buffer.get('short', 'u');\n    const x = buffer.get('short', 'u');\n    return { objectId, x, y };\n};\n\nconst option4: objectInteractionPacket = packet => {\n    const { buffer } = packet;\n    const x = buffer.get('short', 'u', 'le');\n    const objectId = buffer.get('short', 'u', 'le');\n    const y = buffer.get('short', 'u', 'le');\n    return { objectId, x, y };\n};\n\nconst option5: objectInteractionPacket = packet => {\n    const { buffer } = packet;\n    const objectId = buffer.get('short', 'u');\n    const y = buffer.get('short', 'u', 'le');\n    const x = buffer.get('short', 'u', 'le');\n    return { objectId, x, y };\n};\n\nconst objectInteractionPackets: { [key: number]: { packetDef: objectInteractionPacket; index: number } } = {\n    30: { packetDef: option1, index: 0 },\n    164: { packetDef: option2, index: 1 },\n    183: { packetDef: option3, index: 2 },\n    229: { packetDef: option4, index: 3 },\n    62: { packetDef: option5, index: 4 },\n};\n\nconst objectInteractionPacket = (player: Player, packet: PacketData) => {\n    const { packetId } = packet;\n\n    const { objectId, x, y } = objectInteractionPackets[packetId].packetDef(packet);\n    const level = player.position.level;\n    const objectPosition = new Position(x, y, level);\n    const { object: landscapeObject, cacheOriginal } = activeWorld.findObjectAtLocation(player, objectId, objectPosition);\n    if (!landscapeObject) {\n        if (player.rights === Rights.ADMIN) {\n            player.sendMessage(`Custom object ${objectId} @[${objectPosition.key}]`);\n        }\n        return;\n    }\n\n    let objectConfig = filestore.configStore.objectStore.getObject(objectId);\n\n    if (!objectConfig) {\n        logger.error(`[object-interaction] Could not find object config for object id ${objectId}!`);\n        return;\n    }\n\n    if (objectConfig.configChangeDest) {\n        let morphIndex = -1;\n        if (objectConfig.varbitId === -1) {\n            if (objectConfig.configId !== -1) {\n                morphIndex =\n                    player.metadata.configs && player.metadata.configs[objectConfig.configId]\n                        ? player.metadata.configs[objectConfig.configId]\n                        : 0;\n            }\n        } else {\n            morphIndex = getVarbitMorphIndex(objectConfig.varbitId, player.metadata.configs);\n        }\n        if (morphIndex !== -1) {\n            objectConfig = filestore.configStore.objectStore.getObject(objectConfig.configChangeDest[morphIndex]);\n        }\n    }\n\n    if (!objectConfig) {\n        logger.error(`[object-interaction - after morph] Could not find object config for object id ${objectId}!`);\n        return;\n    }\n\n    const actionIdx = objectInteractionPackets[packetId].index;\n    let optionName = `action-${actionIdx + 1}`;\n    if (objectConfig.options && objectConfig.options.length >= actionIdx) {\n        if (!objectConfig.options[actionIdx]) {\n            // Invalid action\n            logger.error(`1: Invalid object ${objectId} option ${actionIdx + 1}, options: ${JSON.stringify(objectConfig.options)}`);\n            return;\n        }\n\n        optionName = objectConfig.options[actionIdx];\n    } else {\n        // Invalid action\n        logger.error(`2: Invalid object ${objectId} option ${actionIdx + 1}, options: ${JSON.stringify(objectConfig.options)}`);\n        return;\n    }\n\n    player.actionPipeline.call(\n        'object_interaction',\n        player,\n        landscapeObject,\n        objectConfig,\n        objectPosition,\n        optionName.toLowerCase(),\n        cacheOriginal,\n    );\n};\n\nexport default Object.keys(objectInteractionPackets).map(opcode => ({\n    opcode: parseInt(opcode, 10),\n    size: 6,\n    handler: objectInteractionPacket,\n}));\n"
  },
  {
    "path": "src/engine/net/inbound-packets/pickup-item.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\n\nconst pickupItemPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const y = buffer.get('short', 'u');\n    const itemId = buffer.get('short', 'u');\n    const x = buffer.get('short', 'u', 'le');\n\n    const level = player.position.level;\n    const worldItemPosition = new Position(x, y, level);\n\n    const worldMods = player.instance.getInstancedChunk(worldItemPosition);\n    const worldItems = worldMods?.mods?.get(worldItemPosition.key)?.worldItems || [];\n\n    let worldItem = worldItems.find(i => i.itemId === itemId) || null;\n\n    if (!worldItem) {\n        const personalMods = player.personalInstance.getInstancedChunk(worldItemPosition);\n        const personalItems = personalMods?.mods?.get(worldItemPosition.key)?.worldItems || [];\n        worldItem = personalItems.find(i => i.itemId === itemId) || null;\n    }\n\n    if (worldItem && !worldItem.removed) {\n        if (worldItem.owner && !worldItem.owner.equals(player)) {\n            return;\n        }\n\n        player.actionPipeline.call('spawned_item_interaction', player, worldItem, 'pick-up');\n    }\n};\n\nexport default {\n    opcode: 85,\n    size: 6,\n    handler: pickupItemPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/player-interaction.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { activeWorld } from '@engine/world';\nimport { type Player, playerOptions } from '@engine/world/actor/player/player';\nimport { World } from '@engine/world/world';\nimport { logger } from '@runejs/common';\n\nconst playerInteractionPacket = (player: Player, packet: PacketData) => {\n    const { buffer, packetId } = packet;\n\n    const packetReaders: Record<number, () => number> = {\n        68: () => buffer.get('short', 'u', 'le'),\n        211: () => buffer.get('short', 'u', 'le'),\n    };\n\n    const playerIndex = packetReaders[packetId]() - 1;\n\n    if (playerIndex < 0 || playerIndex > World.MAX_PLAYERS - 1) {\n        return;\n    }\n\n    const otherPlayer = activeWorld.playerList[playerIndex];\n    if (!otherPlayer) {\n        return;\n    }\n\n    const position = otherPlayer.position;\n    const distance = Math.floor(position.distanceBetween(player.position));\n\n    // Too far away\n    if (distance > 16) {\n        return;\n    }\n\n    const actions = {\n        68: 0,\n        211: 1,\n    };\n\n    const playerOption = playerOptions.find(playerOption => playerOption.index === actions[packetId]);\n\n    if (!playerOption) {\n        logger.error(`Invalid player option ${actions[packetId]}!`);\n        return;\n    }\n\n    player.actionPipeline.call('player_interaction', player, otherPlayer, position, playerOption.option.toLowerCase());\n};\n\nexport default [\n    {\n        opcode: 68,\n        size: 2,\n        handler: playerInteractionPacket,\n    },\n    {\n        opcode: 211,\n        size: 2,\n        handler: playerInteractionPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/private-message.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { longToString } from '@engine/util/strings';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport default {\n    opcode: 207,\n    size: -3,\n    handler: (player: Player, packet: PacketData) => {\n        const { buffer } = packet;\n\n        buffer.get('byte'); // junk\n        const nameLong = BigInt(buffer.get('long'));\n        const username = longToString(nameLong).toLowerCase();\n        const messageLength = buffer.length - 9;\n        const messageBytes = new Array(messageLength);\n        for (let i = 0; i < messageLength; i++) {\n            messageBytes[i] = buffer[buffer.readerIndex + i];\n        }\n\n        const otherPlayer = activeWorld.findActivePlayerByUsername(username);\n        if (otherPlayer) {\n            otherPlayer.privateMessageReceived(player, messageBytes);\n        }\n    },\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/remove-friend.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { longToString } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { PrivateMessaging } from '@engine/world/actor/player/private-messaging';\n\nexport default {\n    opcode: 255,\n    size: 8,\n    handler: (player: Player, packet: PacketData) => {\n        const friendName = longToString(BigInt(packet.buffer.get('long')));\n        if (!friendName) {\n            return;\n        }\n\n        player.removeFriend(friendName);\n        PrivateMessaging.friendRemoved(player, friendName);\n    },\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/remove-ignore.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport { longToString } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport default {\n    opcode: 28,\n    size: 8,\n    handler: (player: Player, packet: PacketData) => player.removeIgnoredPlayer(longToString(BigInt(packet.buffer.get('long')))),\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/social-button.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { PrivateMessaging } from '@engine/world/actor/player/private-messaging';\n\nexport default {\n    opcode: 32,\n    size: 3,\n    handler: (player: Player, packet: PacketData) => {\n        const { buffer } = packet;\n\n        const currentPrivateChatMode = player.settings.privateChatMode;\n\n        player.settings.publicChatMode = buffer.get('byte');\n        player.settings.privateChatMode = buffer.get('byte');\n        player.settings.tradeMode = buffer.get('byte');\n\n        if (currentPrivateChatMode !== player.settings.privateChatMode) {\n            PrivateMessaging.playerPrivateChatModeChanged(player);\n        }\n    },\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/walk.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst walkPacket = (player: Player, packet: PacketData) => {\n    const { buffer, packetSize, packetId } = packet;\n\n    let size = packetSize;\n    if (packetId === 236) {\n        size -= 14;\n    }\n\n    if (!player.canMove()) {\n        return;\n    }\n\n    const totalSteps = Math.floor((size - 5) / 2);\n\n    const firstY = buffer.get('short', 'u', 'le');\n    const runSteps = buffer.get('byte') === 1; // @TODO forced running\n    const firstX = buffer.get('short', 'u', 'le');\n\n    const walkingQueue = player.walkingQueue;\n\n    player.actionsCancelled.next('manual-movement');\n\n    walkingQueue.clear();\n    walkingQueue.valid = true;\n    walkingQueue.add(firstX, firstY);\n\n    for (let i = 0; i < totalSteps; i++) {\n        const x = buffer.get('byte');\n        const y = buffer.get('byte');\n        walkingQueue.add(x + firstX, y + firstY);\n    }\n};\n\nexport default [\n    {\n        opcode: 73,\n        size: -1,\n        handler: walkPacket,\n    },\n    {\n        opcode: 236,\n        size: -1,\n        handler: walkPacket,\n    },\n    {\n        opcode: 89,\n        size: -1,\n        handler: walkPacket,\n    },\n];\n"
  },
  {
    "path": "src/engine/net/inbound-packets/widget-interaction.packet.ts",
    "content": "import type { PacketData } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst widgetInteractionPacket = (player: Player, packet: PacketData) => {\n    const { buffer } = packet;\n    const childId = buffer.get('short');\n    const widgetId = buffer.get('short');\n    const optionId = buffer.get('short', 's', 'le');\n\n    player.actionPipeline.call('widget_interaction', player, widgetId, childId, optionId);\n};\n\nexport default {\n    opcode: 132,\n    size: 6,\n    handler: widgetInteractionPacket,\n};\n"
  },
  {
    "path": "src/engine/net/inbound-packets/widgets-closed.packet.ts",
    "content": "export default {\n    opcode: 176,\n    size: 0,\n    handler: player => player.interfaceState.closeAllSlots(),\n};\n"
  },
  {
    "path": "src/engine/net/isaac.ts",
    "content": "export class Isaac {\n    private m: number[] = Array(256); // internal memory\n    private acc = 0; // accumulator\n    private brs = 0; // last result\n    private cnt = 0; // counter\n    private r: number[] = Array(256); // result array\n    private gnt = 0; // generation counter\n\n    public constructor(seed: number[]) {\n        this.seed(seed);\n    }\n\n    public getR(): number[] {\n        return this.r;\n    }\n\n    public add(x: number, y: number): number {\n        const lsb = (x & 0xffff) + (y & 0xffff);\n        const msb = (x >>> 16) + (y >>> 16) + (lsb >>> 16);\n        return (msb << 16) | (lsb & 0xffff);\n    }\n\n    public reset(): void {\n        this.acc = this.brs = this.cnt = 0;\n        for (let i = 0; i < 256; ++i) this.m[i] = this.r[i] = 0;\n        this.gnt = 0;\n    }\n\n    public seed(s: number[]): void {\n        let a, b, c, d, e, f, g, h, i;\n\n        /* seeding the seeds of love */\n        a = b = c = d = e = f = g = h = 0x9e3779b9; /* the golden ratio */\n\n        this.reset();\n        for (i = 0; i < s.length; i++) this.r[i & 0xff] += typeof s[i] === 'number' ? s[i] : 0;\n\n        /* private: seed mixer */\n        const seed_mix = () => {\n            a ^= b << 11;\n            d = this.add(d, a);\n            b = this.add(b, c);\n            b ^= c >>> 2;\n            e = this.add(e, b);\n            c = this.add(c, d);\n            c ^= d << 8;\n            f = this.add(f, c);\n            d = this.add(d, e);\n            d ^= e >>> 16;\n            g = this.add(g, d);\n            e = this.add(e, f);\n            e ^= f << 10;\n            h = this.add(h, e);\n            f = this.add(f, g);\n            f ^= g >>> 4;\n            a = this.add(a, f);\n            g = this.add(g, h);\n            g ^= h << 8;\n            b = this.add(b, g);\n            h = this.add(h, a);\n            h ^= a >>> 9;\n            c = this.add(c, h);\n            a = this.add(a, b);\n        };\n\n        for (i = 0; i < 4; i++ /* scramble it */) seed_mix();\n\n        for (i = 0; i < 256; i += 8) {\n            if (s) {\n                /* use all the information in the seed */\n                a = this.add(a, this.r[i + 0]);\n                b = this.add(b, this.r[i + 1]);\n                c = this.add(c, this.r[i + 2]);\n                d = this.add(d, this.r[i + 3]);\n                e = this.add(e, this.r[i + 4]);\n                f = this.add(f, this.r[i + 5]);\n                g = this.add(g, this.r[i + 6]);\n                h = this.add(h, this.r[i + 7]);\n            }\n            seed_mix();\n            /* fill in m[] with messy stuff */\n            this.m[i + 0] = a;\n            this.m[i + 1] = b;\n            this.m[i + 2] = c;\n            this.m[i + 3] = d;\n            this.m[i + 4] = e;\n            this.m[i + 5] = f;\n            this.m[i + 6] = g;\n            this.m[i + 7] = h;\n        }\n        if (s) {\n            /* do a second pass to make all of the seed affect all of m[] */\n            for (i = 0; i < 256; i += 8) {\n                a = this.add(a, this.m[i + 0]);\n                b = this.add(b, this.m[i + 1]);\n                c = this.add(c, this.m[i + 2]);\n                d = this.add(d, this.m[i + 3]);\n                e = this.add(e, this.m[i + 4]);\n                f = this.add(f, this.m[i + 5]);\n                g = this.add(g, this.m[i + 6]);\n                h = this.add(h, this.m[i + 7]);\n                seed_mix();\n                /* fill in m[] with messy stuff (again) */\n                this.m[i + 0] = a;\n                this.m[i + 1] = b;\n                this.m[i + 2] = c;\n                this.m[i + 3] = d;\n                this.m[i + 4] = e;\n                this.m[i + 5] = f;\n                this.m[i + 6] = g;\n                this.m[i + 7] = h;\n            }\n        }\n\n        this.prng(); /* fill in the first set of results */\n        this.gnt = 256; /* prepare to use the first set of results */\n    }\n\n    public prng(n?: number): void {\n        let i, x, y;\n\n        n = n && typeof n === 'number' ? Math.abs(Math.floor(n)) : 1;\n\n        while (n--) {\n            this.cnt = this.add(this.cnt, 1);\n            this.brs = this.add(this.brs, this.cnt);\n\n            for (i = 0; i < 256; i++) {\n                switch (i & 3) {\n                    case 0:\n                        this.acc ^= this.acc << 13;\n                        break;\n                    case 1:\n                        this.acc ^= this.acc >>> 6;\n                        break;\n                    case 2:\n                        this.acc ^= this.acc << 2;\n                        break;\n                    case 3:\n                        this.acc ^= this.acc >>> 16;\n                        break;\n                }\n                this.acc = this.add(this.m[(i + 128) & 0xff], this.acc);\n                x = this.m[i];\n                this.m[i] = y = this.add(this.m[(x >>> 2) & 0xff], this.add(this.acc, this.brs));\n                this.r[i] = this.brs = this.add(this.m[(y >>> 10) & 0xff], x);\n            }\n        }\n    }\n\n    public rand(): number {\n        if (!this.gnt--) {\n            this.prng();\n            this.gnt = 255;\n        }\n        return this.r[this.gnt];\n    }\n\n    public internals(): { a: number; b: number; c: number; m: number[]; r: number[] } {\n        return { a: this.acc, b: this.brs, c: this.cnt, m: this.m, r: this.r };\n    }\n}\n"
  },
  {
    "path": "src/engine/net/outbound-packet-handler.ts",
    "content": "import type { Socket } from 'net';\nimport { xteaRegions } from '@engine/config/config-handler';\nimport { Packet, PacketType } from '@engine/net/packet';\nimport { stringToLong } from '@engine/util/strings';\nimport { activeWorld } from '@engine/world';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Player, SidebarTab } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport { ItemContainer } from '@engine/world/items/item-container';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport type { Chunk, ChunkUpdateItem } from '@engine/world/map/chunk';\nimport type { ConstructedChunk, ConstructedRegion } from '@engine/world/map/region';\nimport { Position } from '@engine/world/position';\nimport { ByteBuffer, logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { serverConfig } from '@server/game/game-server';\n\n/**\n * A helper class for sending various network packets back to the game client.\n */\nexport class OutboundPacketHandler {\n    private static privateMessageCounter: number = Math.floor(Math.random() * 100000000);\n\n    private readonly player: Player;\n    private readonly socket: Socket;\n    private updatingQueue: Buffer[];\n    private packetQueue: Buffer[];\n\n    public constructor(player: Player) {\n        this.updatingQueue = [];\n        this.packetQueue = [];\n        this.player = player;\n        this.socket = player.socket;\n    }\n\n    public resetCamera(): void {\n        this.queue(new Packet(7));\n    }\n\n    public snapCameraTo(position: Position, height: number, speed: number, acceleration: number): void {\n        const packet = new Packet(253);\n        this.putCameraPosition(packet, position, height, speed, acceleration);\n        this.queue(packet);\n    }\n\n    public turnCameraTowards(position: Position, height: number, speed: number, acceleration: number): void {\n        const packet = new Packet(234);\n        this.putCameraPosition(packet, position, height, speed, acceleration);\n        this.queue(packet);\n    }\n\n    public updateSocialSettings(): void {\n        const packet = new Packet(196);\n        packet.put(this.player.settings.publicChatMode || 0);\n        packet.put(this.player.settings.privateChatMode || 0);\n        packet.put(this.player.settings.tradeMode || 0);\n        this.queue(packet);\n    }\n\n    public sendPrivateMessage(chatId: number, sender: Player, message: number[]): void {\n        const packet = new Packet(51, PacketType.DYNAMIC_SMALL);\n        packet.put(stringToLong(sender.username.toLowerCase()), 'LONG');\n        packet.put(32767, 'SHORT');\n        packet.put(OutboundPacketHandler.privateMessageCounter++, 'INT24');\n        packet.put(sender.rights);\n        packet.putBytes(Buffer.from(message));\n        this.queue(packet);\n    }\n    //packet - 129 - freezes client?\n    //packet - 202 - directly to login screen\n\n    public sendProjectile(\n        position: Position,\n        offsetX: number,\n        offsetY: number,\n        id: number,\n        startHeight: number,\n        endHeight: number,\n        speed: number,\n        lockon: number,\n        delay: number,\n    ) {\n        this.updateReferencePosition(position);\n\n        const packet = new Packet(1);\n        packet.put(0);\n        packet.put(offsetY, 'byte');\n        packet.put(offsetX, 'byte');\n        packet.put(lockon, 'SHORT', 'BIG_ENDIAN');\n        packet.put(id, 'SHORT', 'BIG_ENDIAN');\n        packet.put(startHeight);\n        packet.put(endHeight);\n        packet.put(delay, 'SHORT', 'BIG_ENDIAN');\n        packet.put(speed, 'SHORT', 'BIG_ENDIAN');\n        packet.put(16);\n        packet.put(64);\n        this.queue(packet);\n    }\n\n    public updateFriendStatus(friendName: string, worldId: number): void {\n        const packet = new Packet(156);\n        packet.put(stringToLong(friendName.toLowerCase()), 'LONG');\n        packet.put(worldId, 'SHORT');\n        this.queue(packet);\n    }\n\n    public sendFriendServerStatus(status: 0 | 1 | 2): void {\n        // 0 = loading, 1 = connecting to friend server, 2 = friend list\n        const packet = new Packet(70);\n        packet.put(status);\n        this.queue(packet);\n    }\n\n    public playSong(songId: number): void {\n        const packet = new Packet(217);\n        packet.put(songId, 'SHORT', 'LITTLE_ENDIAN');\n        this.queue(packet);\n    }\n\n    public playQuickSong(songId: number, previousSongId: number): void {\n        const packet = new Packet(40);\n        packet.put(previousSongId, 'INT24');\n        packet.put(songId, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public playSound(soundId: number, volume: number, delay: number = 0): void {\n        const packet = new Packet(131);\n        packet.put(soundId, 'SHORT');\n        packet.put(volume);\n        packet.put(delay, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public playSoundAtPosition(\n        soundId: number,\n        soundX: number,\n        soundY: number,\n        volume: number,\n        radius: number = 5,\n        delay: number = 0,\n    ): void {\n        const packet = new Packet(9);\n        const offset = 0;\n        packet.put(offset, 'BYTE');\n        packet.put(soundId, 'SHORT');\n        packet.put((volume & 7) + (radius << 4), 'BYTE');\n        packet.put(delay, 'BYTE');\n\n        this.queue(packet);\n    }\n\n    public updateChunk(chunk: Chunk, chunkUpdates: ChunkUpdateItem[]): void {\n        const { offsetX, offsetY } = this.getChunkOffset(chunk);\n\n        const packet = new Packet(63, PacketType.DYNAMIC_LARGE);\n        packet.put(offsetX);\n        packet.put(offsetY);\n\n        chunkUpdates.forEach(update => {\n            if (update.type === 'ADD') {\n                if (update.object && !update.object.reference) {\n                    const offset = this.getChunkPositionOffset(update.object.x, update.object.y, chunk);\n                    packet.put(241, 'BYTE');\n                    packet.put((update.object.type << 2) + (update.object.orientation & 3));\n                    packet.put(update.object.objectId, 'SHORT');\n                    packet.put(offset);\n                } else if (update.worldItem) {\n                    const offset = this.getChunkPositionOffset(update.worldItem.position.x, update.worldItem.position.y, chunk);\n                    packet.put(175, 'BYTE');\n                    packet.put(update.worldItem.itemId, 'SHORT', 'LITTLE_ENDIAN');\n                    packet.put(update.worldItem.amount, 'SHORT');\n                    packet.put(offset, 'BYTE');\n                }\n            } else if (update.type === 'REMOVE') {\n                if (!update.object) {\n                    logger.warn('Tried to remove object that does not exist.');\n                    return;\n                }\n\n                const offset = this.getChunkPositionOffset(update.object.x, update.object.y, chunk);\n                packet.put(143, 'BYTE');\n                packet.put(offset);\n                packet.put((update.object.type << 2) + (update.object.orientation & 3));\n            }\n        });\n\n        this.queue(packet);\n    }\n\n    public clearChunk(chunk: Chunk): void {\n        const { offsetX, offsetY } = this.getChunkOffset(chunk);\n\n        const packet = new Packet(64);\n        packet.put(offsetY, 'BYTE');\n        packet.put(offsetX);\n\n        this.queue(packet);\n    }\n\n    public setWorldItem(worldItem: WorldItem, position: Position, offset: number = 0): void {\n        this.updateReferencePosition(position);\n\n        const packet = new Packet(175);\n        packet.put(worldItem.itemId, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put(worldItem.amount, 'SHORT');\n        packet.put(offset, 'BYTE');\n\n        this.queue(packet);\n    }\n\n    public removeWorldItem(worldItem: WorldItem, position: Position, offset: number = 0): void {\n        this.updateReferencePosition(position);\n\n        const packet = new Packet(74);\n        packet.put(offset, 'BYTE');\n        packet.put(worldItem.itemId, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public setLocationObject(locationObject: LandscapeObject, position: Position, offset: number = 0): void {\n        this.updateReferencePosition(position);\n\n        const packet = new Packet(241);\n        packet.put((locationObject.type << 2) + (locationObject.orientation & 3));\n        packet.put(locationObject.objectId, 'SHORT');\n        packet.put(offset);\n\n        this.queue(packet);\n    }\n\n    public removeLocationObject(locationObject: LandscapeObject, position: Position, offset: number = 0): void {\n        this.updateReferencePosition(position);\n\n        const packet = new Packet(143);\n        packet.put(offset);\n        packet.put((locationObject.type << 2) + (locationObject.orientation & 3));\n\n        this.queue(packet);\n    }\n\n    public updateReferencePosition(position: Position): void {\n        const offsetX = position.x - this.player.lastMapRegionUpdatePosition.chunkX * 8;\n        const offsetY = position.y - this.player.lastMapRegionUpdatePosition.chunkY * 8;\n\n        const packet = new Packet(254);\n        packet.put(offsetY);\n        packet.put(offsetX);\n\n        this.queue(packet);\n    }\n\n    // Text dialogs = 356, 359, 363, 368, 374\n    // Item dialogs = 519\n    // Statements (no click to continue) = 210, 211, 212, 213, 214\n    public showChatboxWidget(widgetId: number): void {\n        const packet = new Packet(208);\n        packet.put(widgetId, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public setWidgetNpcHead(widgetId: number, childId: number, modelId: number): void {\n        const packet = new Packet(160);\n        packet.put(modelId, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public setWidgetPlayerHead(widgetId: number, childId: number): void {\n        const packet = new Packet(210);\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public playWidgetAnimation(widgetId: number, childId: number, animationId: number): void {\n        const packet = new Packet(24);\n        packet.put(animationId, 'SHORT');\n        packet.put((widgetId << 16) | childId, 'INT');\n\n        this.queue(packet);\n    }\n\n    public showScreenAndTabWidgets(widgetId: number, tabWidgetId: number): void {\n        const packet = new Packet(84);\n        packet.put(tabWidgetId, 'SHORT');\n        packet.put(widgetId, 'SHORT', 'LITTLE_ENDIAN');\n        this.queue(packet);\n    }\n\n    public resetAllClientConfigs(): void {\n        const packet = new Packet(14);\n        this.queue(packet);\n    }\n\n    public updateClientConfig(configId: number, value: number): void {\n        let packet: Packet;\n        const metadata = this.player.metadata;\n        if (!metadata.configs) {\n            metadata.configs = [];\n        }\n        metadata.configs[configId] = value;\n\n        if (value > 128) {\n            packet = new Packet(2);\n            packet.put(value, 'INT');\n            packet.put(configId, 'SHORT');\n        } else {\n            packet = new Packet(222);\n            packet.put(value);\n            packet.put(configId, 'SHORT');\n        }\n\n        this.queue(packet);\n    }\n\n    public setWidgetModelRotationAndZoom(widgetId: number, childId: number, rotationX: number, rotationY: number, zoom: number): void {\n        const packet = new Packet(142);\n        packet.put(rotationX, 'SHORT');\n        packet.put(zoom, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put(rotationY, 'SHORT');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public updateWidgetModel1(widgetId: number, childId: number, modelId: number): void {\n        const packet = new Packet(250);\n        packet.put(modelId, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public updateWidgetItemModel(widgetId: number, itemId: number, scale?: number): void {\n        const packet = new Packet(21);\n\n        // TODO (Jameskmonger) what should the default value of `scale` be?\n        packet.put(scale || 0, 'SHORT');\n        packet.put(itemId, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put(widgetId, 'SHORT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public updateWidgetString(widgetId: number, childId: number, value: string): void {\n        const packet = new Packet(110, PacketType.DYNAMIC_LARGE);\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n        packet.putString(value);\n\n        this.queue(packet);\n    }\n\n    public updateWidgetColor(widgetId: number, childId: number, color: number): void {\n        const packet = new Packet(231);\n        packet.put(color, 'SHORT');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public closeActiveWidgets(): void {\n        this.queue(new Packet(180));\n    }\n\n    public showScreenOverlayWidget(widgetId: number): void {\n        const packet = new Packet(56);\n        packet.put(widgetId, 'SHORT');\n        this.queue(packet);\n    }\n\n    public showStandaloneScreenWidget(widgetId: number): void {\n        const packet = new Packet(118);\n        packet.put(widgetId, 'SHORT');\n        this.queue(packet);\n    }\n\n    // @TODO this can support multiple items/slots !!!\n    public sendUpdateSingleWidgetItem(widget: { widgetId: number; containerId: number }, slot: number, item: Item | null): void {\n        const packet = new Packet(214, PacketType.DYNAMIC_LARGE);\n        packet.put((widget.widgetId << 16) | widget.containerId, 'INT');\n        packet.put(slot, 'SMART_SHORT');\n\n        if (!item) {\n            packet.put(0, 'SHORT');\n        } else {\n            packet.put(item.itemId + 1, 'SHORT'); // +1 because 0 means an empty slot\n\n            if (item.amount >= 255) {\n                packet.put(255, 'BYTE');\n                packet.put(item.amount, 'INT');\n            } else {\n                packet.put(item.amount, 'BYTE');\n            }\n        }\n\n        this.queue(packet);\n    }\n\n    public update(packet: Packet, widget: { widgetId: number; containerId: number }, container: ItemContainer): void {\n        const packed = (widget.widgetId << 16) | widget.containerId;\n        packet.put(packed, 'INT');\n\n        const size = container.size;\n        packet.put(size, 'SHORT');\n\n        const bound = container.items.length * 7;\n        const payload = new Packet(-1, PacketType.FIXED, bound); //TODO: change default value of allocatedSize from 5000 to something reasonable (64 - 256 as most RS packets are quite small)\n\n        for (let index = 0; index < size; index += 8) {\n            const { bitset, buffer } = this.segment(container, index);\n\n            payload.put(bitset, 'BYTE');\n\n            if (bitset == 0) {\n                continue;\n            }\n\n            payload.putBytes(buffer);\n        }\n\n        packet.putBytes(this.strip(payload));\n\n        this.queue(packet);\n    }\n\n    public sendUpdateAllWidgetItems(widget: { widgetId: number; containerId: number }, container: ItemContainer): void {\n        const packet = new Packet(12, PacketType.DYNAMIC_LARGE);\n        this.update(packet, widget, container);\n    }\n\n    public sendUpdateAllWidgetItemsById(widget: { widgetId: number; containerId: number }, itemIds: number[]): void {\n        const container = new ItemContainer(itemIds.length);\n        const items = itemIds.map(id => (!id ? null : { itemId: id, amount: 1 }));\n        container.setAll(items, false);\n\n        this.sendUpdateAllWidgetItems(widget, container);\n    }\n\n    public setItemOnWidget(widgetId: number, childId: number, itemId: number, zoom: number): void {\n        const packet = new Packet(120);\n        packet.put(zoom, 'SHORT');\n        packet.put(itemId, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public toggleWidgetVisibility(widgetId: number, childId: number, hidden: boolean): void {\n        const packet = new Packet(115);\n        packet.put(hidden ? 1 : 0, 'BYTE');\n        packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public moveWidgetChild(widgetId: number, childId: number, offsetX: number, offsetY: number): void {\n        const packet = new Packet(3);\n        packet.put((widgetId << 16) | childId, 'INT');\n        packet.put(offsetY, 'SHORT', 'LITTLE_ENDIAN');\n        packet.put(offsetX, 'SHORT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public showTabWidget(widgetId: number): void {\n        const packet = new Packet(237);\n        packet.put(widgetId, 'SHORT');\n        this.queue(packet);\n    }\n\n    public sendTabWidget(tabIndex: SidebarTab, widgetId: number | null): void {\n        const packet = new Packet(140);\n        packet.put(widgetId === null || widgetId === -1 ? 65535 : widgetId, 'SHORT');\n        packet.put(tabIndex);\n\n        this.queue(packet);\n    }\n\n    public blinkTabIcon(tabIndex: number): void {\n        const packet = new Packet(88);\n        packet.put(tabIndex);\n        this.queue(packet);\n    }\n\n    public showFullscreenWidget(widgetId: number, secondaryWidgetId: number): void {\n        const packet = new Packet(195);\n        packet.put(secondaryWidgetId, 'SHORT');\n        packet.put(widgetId, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public showNumberInputDialogue(): void {\n        const packet = new Packet(132);\n        this.queue(packet);\n    }\n\n    public showTextInputDialogue(): void {\n        const packet = new Packet(124);\n        this.queue(packet);\n    }\n\n    public showChatDialogue(widgetId: number): void {\n        const packet = new Packet(185);\n        packet.put(widgetId, 'SHORT');\n        this.queue(packet);\n    }\n\n    public updateCarryWeight(weight: number): void {\n        const packet = new Packet(171);\n        packet.put(weight, 'SHORT');\n\n        this.queue(packet);\n    }\n\n    public showHintIcon(iconType: 2 | 3 | 4 | 5 | 6, position: Position, offset: number = 0): void {\n        const packet = new Packet(186);\n        packet.put(iconType, 'BYTE');\n        packet.put(position.x, 'SHORT');\n        packet.put(position.y, 'SHORT');\n        packet.put(offset, 'BYTE');\n\n        this.queue(packet);\n    }\n\n    public showPlayerHintIcon(player: Player): void {\n        const packet = new Packet(186);\n        packet.put(10, 'BYTE');\n        packet.put(player.worldIndex, 'SHORT');\n\n        // Packet requires a length of 6, so send some extra junk\n        packet.put(0);\n        packet.put(0);\n        packet.put(0);\n\n        this.queue(packet);\n    }\n\n    public showNpcHintIcon(npc: Npc): void {\n        const packet = new Packet(186);\n        packet.put(1, 'BYTE');\n        packet.put(npc.worldIndex, 'SHORT');\n\n        // Packet requires a length of 6, so send some extra junk\n        packet.put(0);\n        packet.put(0);\n        packet.put(0);\n\n        this.queue(packet);\n    }\n\n    public resetNpcHintIcon(): void {\n        const packet = new Packet(186);\n        packet.put(1, 'BYTE');\n        packet.put(-1, 'SHORT');\n\n        // Packet requires a length of 6, so send some extra junk\n        packet.put(0);\n        packet.put(0);\n        packet.put(0);\n\n        this.queue(packet);\n    }\n\n    public logout(): void {\n        this.packetQueue = [];\n        this.updatingQueue = [];\n\n        this.socket.write(new Packet(181).toBuffer(this.player.outCipher));\n    }\n\n    public chatboxMessage(message: string): void {\n        const packet = new Packet(82, PacketType.DYNAMIC_SMALL);\n        packet.putString(message);\n\n        this.queue(packet);\n    }\n\n    public consoleMessage(message: string): void {\n        const packet = new Packet(83, PacketType.DYNAMIC_SMALL);\n        packet.putString(message);\n\n        this.queue(packet);\n    }\n\n    public sendConsoleCommand(command: string, help: string): void {\n        const packet = new Packet(85, PacketType.DYNAMIC_SMALL);\n        packet.putString(command);\n        packet.putString(help);\n        this.queue(packet);\n    }\n\n    public updateSkill(skillId: number, level: number, exp: number): void {\n        const packet = new Packet(34);\n        packet.put(level);\n        packet.put(skillId);\n        packet.put(exp, 'INT', 'LITTLE_ENDIAN');\n\n        this.queue(packet);\n    }\n\n    public constructMapRegion(mapData: ConstructedRegion): void {\n        const packet = new Packet(23, PacketType.DYNAMIC_LARGE);\n\n        packet.put(this.player.position.chunkLocalY, 'short');\n        packet.put(this.player.position.chunkLocalX, 'short', 'le');\n        packet.put(this.player.position.chunkX + 6, 'short');\n        packet.put(this.player.position.level);\n        packet.put(this.player.position.chunkY + 6, 'short');\n\n        packet.openBitBuffer();\n\n        const mapWorldX = mapData.renderPosition.x;\n        const mapWorldY = mapData.renderPosition.y;\n\n        const topCornerMapChunk = activeWorld.chunkManager.getChunkForWorldPosition(\n            new Position(mapWorldX, mapWorldY, this.player.position.level),\n        );\n        const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.player.position);\n\n        const offsetX = playerChunk.position.x - (topCornerMapChunk.position.x - 2);\n        const offsetY = playerChunk.position.y - (topCornerMapChunk.position.y - 2);\n\n        mapData.drawOffsetX = offsetX - 6; // 6 === center\n        mapData.drawOffsetY = offsetY - 6; // 6 === center\n\n        for (let level = 0; level < 4; level++) {\n            for (let x = 0; x < 13; x++) {\n                for (let y = 0; y < 13; y++) {\n                    let mapTileOffsetX = x + mapData.drawOffsetX;\n                    let mapTileOffsetY = y + mapData.drawOffsetY;\n                    if (mapTileOffsetX < 0) {\n                        mapTileOffsetX = 0;\n                    }\n                    if (mapTileOffsetX > 12) {\n                        mapTileOffsetX = 12;\n                    }\n                    if (mapTileOffsetY < 0) {\n                        mapTileOffsetY = 0;\n                    }\n                    if (mapTileOffsetY > 12) {\n                        mapTileOffsetY = 12;\n                    }\n\n                    const constructedChunk: ConstructedChunk | null = mapData.chunks[level][mapTileOffsetX][mapTileOffsetY];\n                    packet.putBits(1, constructedChunk === null ? 0 : 1);\n                    if (constructedChunk !== null) {\n                        const { templatePosition, orientation } = constructedChunk;\n                        packet.putBits(2, templatePosition?.level & 0x3);\n                        packet.putBits(10, templatePosition?.x / 8);\n                        packet.putBits(11, templatePosition?.y / 8);\n                        packet.putBits(2, orientation || 0);\n                        packet.putBits(1, 0); // unused\n                    }\n                }\n            }\n        }\n\n        packet.closeBitBuffer();\n\n        const encryptionEnabled = serverConfig.encryptionEnabled === undefined ? true : serverConfig.encryptionEnabled;\n\n        // Put the xtea keys for the two construction room template maps\n        // Map coords: 29,79 && 30,79\n        for (let mapX = 29; mapX <= 30; mapX++) {\n            const xteaRegion = xteaRegions[`l${mapX}_79`];\n            for (let seeds = 0; seeds < 4; seeds++) {\n                packet.put(encryptionEnabled ? (xteaRegion?.key[seeds] ?? 0) : 0, 'int');\n            }\n        }\n\n        this.queue(packet);\n    }\n\n    public updateCurrentMapChunk(): void {\n        const packet = new Packet(166, PacketType.DYNAMIC_LARGE);\n        packet.put(this.player.position.chunkLocalY, 'short');\n        packet.put(this.player.position.chunkX + 6, 'short', 'le');\n        packet.put(this.player.position.chunkLocalX, 'short');\n        packet.put(this.player.position.chunkY + 6, 'short', 'le');\n        packet.put(this.player.position.level);\n\n        const startX = Math.floor(this.player.position.chunkX / 8);\n        const endX = Math.floor((this.player.position.chunkX + 12) / 8);\n        const startY = Math.floor(this.player.position.chunkY / 8);\n        const endY = Math.floor((this.player.position.chunkY + 12) / 8);\n\n        const encryptionEnabled = serverConfig.encryptionEnabled === undefined ? true : serverConfig.encryptionEnabled;\n\n        for (let mapX = startX; mapX <= endX; mapX++) {\n            for (let mapY = startY; mapY <= endY; mapY++) {\n                const xteaRegion = xteaRegions[`l${mapX}_${mapY}`];\n                for (let seeds = 0; seeds < 4; seeds++) {\n                    packet.put(encryptionEnabled ? (xteaRegion?.key[seeds] ?? 0) : 0, 'int');\n                }\n            }\n        }\n\n        this.queue(packet);\n    }\n\n    public updatePlayerOption(option: string, index: number = 0, placement: 'TOP' | 'BOTTOM' = 'BOTTOM'): void {\n        const packet = new Packet(223, PacketType.DYNAMIC_SMALL);\n        packet.putString(!option ? 'hidden' : option);\n        packet.put(placement === 'TOP' ? 1 : 0);\n        packet.put(index + 1);\n\n        this.queue(packet);\n    }\n\n    public flushQueue(): void {\n        if (!this.socket || this.socket.destroyed) {\n            return;\n        }\n\n        const buffer = Buffer.concat([...this.packetQueue, ...this.updatingQueue]);\n        if (buffer.length !== 0) {\n            this.socket.write(buffer);\n        }\n\n        this.updatingQueue = [];\n        this.packetQueue = [];\n    }\n\n    public queue(packet: Packet, updateTask: boolean = false): void {\n        if (!this.socket || this.socket.destroyed) {\n            return;\n        }\n\n        const queue = updateTask ? this.updatingQueue : this.packetQueue;\n\n        const packetBuffer = packet.toBuffer(this.player.outCipher);\n        queue.push(packetBuffer);\n    }\n\n    private putCameraPosition(packet: Packet, position: Position, height: number, speed: number, acceleration: number): void {\n        packet.put(position.calculateChunkLocalX(this.player.lastMapRegionUpdatePosition));\n        packet.put(position.calculateChunkLocalY(this.player.lastMapRegionUpdatePosition));\n        packet.put(height, 'SHORT');\n        packet.put(speed);\n        packet.put(acceleration);\n    }\n\n    private getChunkPositionOffset(x: number, y: number, chunk: Chunk): number {\n        const offsetX = x - (chunk.position.x + 6) * 8;\n        const offsetY = y - (chunk.position.y + 6) * 8;\n        return offsetX * 16 + offsetY;\n    }\n\n    private getChunkOffset(chunk: Chunk): { offsetX: number; offsetY: number } {\n        let offsetX = (chunk.position.x + 6) * 8;\n        let offsetY = (chunk.position.y + 6) * 8;\n        offsetX -= this.player.lastMapRegionUpdatePosition.chunkX * 8;\n        offsetY -= this.player.lastMapRegionUpdatePosition.chunkY * 8;\n\n        return { offsetX, offsetY };\n    }\n\n    private strip(packet: Packet): Buffer {\n        const size = packet.writerIndex;\n        const buffer = new ByteBuffer(size);\n        packet.copy(buffer, 0, 0, size);\n        return Buffer.from(buffer);\n    }\n\n    private segment(container: ItemContainer, start: number): { bitset: number; buffer: Buffer } {\n        const bound = 7 * 8;\n        const payload = new Packet(-1, PacketType.FIXED, bound);\n\n        let bitset: number = 0;\n\n        for (let offset = 0; offset < 8; offset++) {\n            const item = container.items[start + offset];\n\n            if (!item) {\n                continue;\n            }\n\n            bitset |= 1 << offset;\n\n            const large = item.amount >= 255;\n\n            if (large) {\n                payload.put(255, 'BYTE');\n            }\n\n            payload.put(item.amount, large ? 'INT' : 'BYTE');\n            payload.put(item.itemId + 1, 'SHORT');\n        }\n\n        return { bitset, buffer: this.strip(payload) };\n    }\n}\n"
  },
  {
    "path": "src/engine/net/packet.ts",
    "content": "import { ByteBuffer } from '@runejs/common';\nimport type { Isaac } from './isaac';\n\n/**\n * The type of packet; Fixed, Dynamic Small (sized byte), or Dynamic Large (sized short)\n */\nexport enum PacketType {\n    FIXED = 'FIXED',\n    DYNAMIC_SMALL = 'DYNAMIC_SMALL',\n    DYNAMIC_LARGE = 'DYNAMIC_LARGE',\n}\n\n/**\n * A single packet to be sent to the game client.\n */\nexport class Packet extends ByteBuffer {\n    private readonly _packetId: number;\n    private readonly _type: PacketType = PacketType.FIXED;\n\n    public constructor(packetId: number, type: PacketType = PacketType.FIXED, allocatedSize: number = 5000) {\n        super(allocatedSize);\n        this._packetId = packetId;\n        this._type = type;\n    }\n\n    public toBuffer(cipher: Isaac): Buffer {\n        const packetSize = this.writerIndex;\n        let bufferSize = packetSize + 1; // +1 for the packet id\n\n        if (this.type !== PacketType.FIXED) {\n            bufferSize += this.type === PacketType.DYNAMIC_SMALL ? 1 : 2;\n        }\n\n        const buffer = new ByteBuffer(bufferSize);\n        buffer.put((this.packetId + (cipher !== null ? cipher.rand() : 0)) & 0xff, 'BYTE');\n\n        let copyStart = 1;\n\n        if (this.type === PacketType.DYNAMIC_SMALL) {\n            buffer.put(packetSize, 'BYTE');\n            copyStart = 2;\n        } else if (this.type === PacketType.DYNAMIC_LARGE) {\n            buffer.put(packetSize, 'SHORT');\n            copyStart = 3;\n        }\n\n        this.copy(buffer, copyStart, 0, packetSize);\n        return Buffer.from(buffer);\n    }\n\n    public get packetId(): number {\n        return this._packetId;\n    }\n\n    public get type(): PacketType {\n        return this._type;\n    }\n}\n"
  },
  {
    "path": "src/engine/plugins/content-plugin.ts",
    "content": "import { join } from 'path';\nimport type { ActionHook } from '@engine/action/hook/action-hook';\nimport type { Quest } from '@engine/world/actor/player/quest';\nimport { logger } from '@runejs/common';\nimport { getFiles } from '@runejs/common/fs';\n\n/**\n * The definition of a single content plugin.\n */\nexport class ContentPlugin {\n    public pluginId: string;\n    public hooks?: ActionHook[];\n    public quests?: Quest[];\n}\n\n/**\n * Searches for and parses all plugin files within the /plugins directory.\n */\nexport async function loadPluginFiles(): Promise<ContentPlugin[]> {\n    const pluginDir = join('.', 'dist', 'plugins');\n    const relativeDir = join('..', '..', 'plugins');\n    const plugins: ContentPlugin[] = [];\n\n    for await (const path of getFiles(pluginDir, { type: 'whitelist', list: ['.plugin.js', 'index.js'] })) {\n        const location = join(relativeDir, path.substring(pluginDir.length).replace('.js', ''));\n\n        try {\n            let pluginFile = require(location);\n            if (!pluginFile) {\n                continue;\n            }\n\n            if (pluginFile.default) {\n                pluginFile = pluginFile.default;\n            }\n\n            const plugin = pluginFile as ContentPlugin;\n            if (!plugin.pluginId) {\n                logger.error(`Error loading plugin: Plugin ID not provided for .plugin file at ${path}`);\n                continue;\n            }\n\n            if (plugins.find(loadedPlugin => loadedPlugin.pluginId === plugin.pluginId)) {\n                logger.error(`Error loading plugin: Duplicate plugin ID ${plugin.pluginId} at ${path}`);\n                continue;\n            }\n\n            plugins.push(plugin);\n        } catch (error) {\n            logger.error(`Error loading plugin file at ${location}:`);\n            logger.error(error);\n        }\n    }\n\n    return plugins;\n}\n"
  },
  {
    "path": "src/engine/plugins/loader.ts",
    "content": "import type { ActionType } from '@engine/action/action-pipeline';\nimport { type ActionHook, sortActionHooks } from '@engine/action/hook/action-hook';\nimport { loadPluginFiles } from '@engine/plugins/content-plugin';\nimport { Quest } from '@engine/world/actor/player/quest';\nimport { logger } from '@runejs/common';\n\n/**\n * A type for describing the plugin action hook map.\n */\ntype PluginActionHookMap = { quest?: ActionHook[] } & {\n    [key in ActionType]?: ActionHook[];\n};\n\n/**\n * A type for describing the plugin action hook map.\n */\ninterface PluginQuestMap {\n    [key: string]: Quest;\n}\n\n/**\n * A list of action hooks imported from content plugins.\n */\nexport let actionHookMap: PluginActionHookMap = {};\n/**\n * A list of quests imported from content plugins.\n */\nexport let questMap: PluginQuestMap = {};\n\n/**\n * Searches for and loads all plugin files and their associated action hooks.\n */\nexport async function loadPlugins(): Promise<void> {\n    actionHookMap = {};\n    questMap = {};\n    const plugins = await loadPluginFiles();\n\n    const pluginActionHookList = plugins?.filter(plugin => !!plugin?.hooks)?.map(plugin => plugin.hooks);\n\n    if (pluginActionHookList && pluginActionHookList.length !== 0) {\n        pluginActionHookList\n            .reduce((a, b) => (a || []).concat(b || []))\n            ?.forEach(action => {\n                if (!(action instanceof Quest)) {\n                    if (!actionHookMap[action.type]) {\n                        actionHookMap[action.type] = [];\n                    }\n\n                    actionHookMap[action.type]!.push(action);\n                } else {\n                    if (!actionHookMap['quest']) {\n                        actionHookMap['quest'] = [];\n                    }\n\n                    actionHookMap['quest'].push(action);\n                }\n            });\n    } else {\n        logger.warn(`No action hooks detected - update plugins.`);\n    }\n\n    for (const plugin of plugins) {\n        if (!plugin.quests) {\n            continue;\n        }\n\n        for (const quest of plugin.quests) {\n            questMap[quest.id] = quest;\n        }\n    }\n\n    // @TODO implement proper sorting rules\n    Object.keys(actionHookMap).forEach(key => (actionHookMap[key] = sortActionHooks(actionHookMap[key])));\n}\n"
  },
  {
    "path": "src/engine/plugins/reload-content.ts",
    "content": "import { sep } from 'path';\nimport { loadGameConfigurations } from '@engine/config/config-handler';\nimport { loadPackets } from '@engine/net/inbound-packet-handler';\nimport { loadPlugins } from '@engine/plugins/loader';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\nexport const reloadContentCommands = ['plugins', 'reload', 'content', 'hotload', 'refresh', 'restart', 'r'];\n\nexport const reloadContent = async (player: Player, isConsole: boolean = false) => {\n    player.sendLogMessage(' ', isConsole);\n    player.sendLogMessage('Deleting content cache...', isConsole);\n\n    const includeList = ['plugins'].map(p => sep + p + sep);\n\n    const ignoreList = ['node_modules', 'engine', 'server'].map(p => sep + p + sep);\n\n    const pluginCache: string[] = [];\n    const cacheKeys = Object.keys(require.cache);\n\n    // Delete node cache for all the old JS plugins\n    cacheLoop: for (const cacheKey of cacheKeys) {\n        const cachedItem = require.cache[cacheKey];\n\n        if (!cachedItem) {\n            continue;\n        }\n\n        const path = typeof cachedItem === 'string' ? cachedItem : cachedItem?.path;\n\n        if (!path) {\n            continue;\n        }\n\n        for (const ignoreItem of ignoreList) {\n            if (path.indexOf(ignoreItem) !== -1) {\n                continue cacheLoop;\n            }\n        }\n\n        let includePath = false;\n\n        for (const includeItem of includeList) {\n            if (path.indexOf(includeItem) !== -1) {\n                includePath = true;\n                break;\n            }\n        }\n\n        if (includePath) {\n            pluginCache.push(cacheKey);\n        }\n    }\n\n    console.log(pluginCache);\n\n    for (const key of pluginCache) {\n        delete require.cache[require.resolve(key)];\n    }\n\n    try {\n        player.sendLogMessage('Reloading plugins...', isConsole);\n        await loadPlugins();\n    } catch (error) {\n        player.sendLogMessage('Error reloading content.', isConsole);\n        logger.error(error);\n    }\n\n    try {\n        player.sendLogMessage('Reloading configurations...', isConsole);\n        await loadGameConfigurations();\n    } catch (error) {\n        player.sendLogMessage('Error reloading configurations.', isConsole);\n        logger.error(error);\n    }\n\n    try {\n        player.sendLogMessage('Reloading packets...', isConsole);\n        await loadPackets();\n    } catch (error) {\n        player.sendLogMessage('Error reloading packets.', isConsole);\n        logger.error(error);\n    }\n\n    player.sendLogMessage('Reload completed.', isConsole);\n};\n"
  },
  {
    "path": "src/engine/task/README.md",
    "content": "# Task system\n\nThe task system allows you to write content which will be executed on the tick cycles of the server.\n\nYou can configure a task to execute every `n` ticks (minimum of `1` for every tick), and you can also choose a number of other behaviours, such as whether the task should execute immediately or after a delay, as well as set to repeat indefinitely.\n\n## Scheduling a task\n\nYou can schedule a task by registering it with a `TaskScheduler`.\n\nThe task in the example of below runs with an interval of `2`, i.e. it will be executed every 2 ticks.\n\n```ts\nclass MyTask extends Task {\n    public constructor() {\n        super({ interval: 2 });\n    }\n\n    public execute(): void {\n        console.log('2 ticks');\n    }\n}\n\nconst scheduler = new TaskScheduler();\n\nscheduler.addTask(new MyTask());\n\nscheduler.tick();\nscheduler.tick(); // '2 ticks'\n```\n\nEvery two times that `scheduler.tick()` is called, it will run the `execute` function of your task.\n\n## Task configuration\n\nYou can pass a `TaskConfig` object to the `Task` constructor in order to configure various aspects of your task.\n\n### Timing\n\nThe most simple configuration option for a `Task` is the `interval` option. Your task will be executed every `interval` amount of ticks.\n\n```ts\n/**\n* The number of ticks between each execution of the task.\n*/\ninterval: number;\n```\n\nFor example, with an interval of `1`, your task will run every tick. The default value is `1`.\n\n### Immediate execution\n\nYou can configure your task to execute immediately with the `immediate` option.\n\n```ts\n/**\n* Should the task be executed on the first tick after it is added?\n*/\nimmediate: boolean;\n```\n\nFor example, if `immediate` is `true` and `interval` is `5`, your task will run on the 1st and 6th ticks (and so on).\n\n### Repeating\n\nYou can use the `repeat` option to tell your task to run forever.\n\n```ts\n/**\n* Should the task be repeated indefinitely?\n*/\nrepeat: boolean;\n```\n\nYou can use `this.stop()` inside the task to stop it from repeating further.\n\n### Stacking\n\nThe `stackType` and `stackGroup` properties allow you to control how your task interacts with other, similar tasks.\n\n```ts\n/**\n* How the task should be stacked with other tasks of the same stack group.\n*/\nstackType: TaskStackType;\n\n/**\n* The stack group for this task.\n*/\nstackGroup: string;\n```\n\nWhen `stackType` is set to `TaskStackType.NEVER`, other tasks with the same `stackGroup` will be stopped when your task is enqueued. A `stackType` of `TaskStackType.STACK` will allow your task to run with others of the same group.\n\nThe default type is `TaskStackType.STACK` and the group is `TaskStackGroup.ACTION` (`'action'`)\n\n## Task Subtypes\n\nRather than extending `Task`, there are a number of subclasses you can extend which will give you some syntactic sugar around common functionality.\n\n- `ActorTask`\n\n    This is the base task to be performed by an `Actor`. It will automatically listen to the actor's walking queue, and stop the task if it has a `breakType` of `ON_MOVE`.\n\n- `ActorWalkToTask`\n\n    This task will make an actor walk to a `Position` or `LandscapeObject` and will expose the `atDestination` property for your extended task to query. You can then begin executing your task logic.\n\n- `ActorLandscapeObjectInteractionTask`\n\n    This task extends `ActorWalkToTask` and will make an actor walk to a given `LandscapeObject`, before exposing the `landscapeObject` property for your task to use.\n\n- `ActorWorldItemInteractionTask`\n\n    This task extends `ActorWalkToTask` and will make an actor walk to a given `WorldItem`, before exposing the `worldItem` property for your task to use.\n\n# Future improvements\n\n- Stalling executions for certain tasks when interface is open\n    - should we create a `PlayerTask` to contain this behaviour? The `breakType` behaviour could be moved to this base, rather than `ActorTask`\n\n- Consider refactoring this system to use functional programming patterns. Composition should be favoured over inheritance generally, and there are some examples of future tasks which may be easier if we could compose tasks from building blocks. Consider the implementation of some task which requires both a `LandscapeObject` and a `WorldItem` - we currently would need to create some custom task which borrowed behaviour from the `ActorLandscapeObjectInteractionTask` and `ActorWorldItemInteractionTask`. TypeScript mixins could be useful here.\n\n# Content requiring conversion to task system\n\nHighest priority is to convert pieces of content which make use of the old `task` system. These are:\n\n- Magic attack\n- Magic teleports\n- Prayer\n- Combat\n- Forging (smithing)\n- Woodcutting\n\nThe following areas will make interesting use of the task system and would serve as a good demonstration:\n\n- Health regen\n- NPC movement\n"
  },
  {
    "path": "src/engine/task/impl/actor-actor-interaction-task.ts",
    "content": "import type { Actor } from '@engine/world/actor/actor';\nimport type { Position } from '@engine/world/position';\nimport { ActorWalkToTask } from './actor-walk-to-task';\n\n/**\n * A task for an actor to interact with another actor.\n *\n * This task extends {@link ActorWalkToTask} and will walk the actor to the other actor.\n * Once the actor is within range of the other actor, the task will expose the {@link other} property\n *\n * @author jameskmonger\n */\nexport abstract class ActorActorInteractionTask<TActor extends Actor = Actor, TOtherActor extends Actor = Actor> extends ActorWalkToTask<\n    TActor,\n    () => Position\n> {\n    private _other: TOtherActor;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param TOtherActor The other actor to interact with.\n     * @param walkOnStart Whether to walk to the other actor on task start.\n     *                    Defaults to `false` as the client generally inits a walk on interaction.\n     */\n    constructor(actor: TActor, otherActor: TOtherActor, walkOnStart = false) {\n        super(\n            actor,\n            () => otherActor.position,\n            // TODO (jkm) handle other actor size\n            1,\n            walkOnStart,\n        );\n\n        if (!otherActor) {\n            this.stop();\n            return;\n        }\n\n        this._other = otherActor;\n    }\n\n    /**\n     * Checks for the continued presence of the other actor and stops the task if it is no longer present.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    public execute() {\n        super.execute();\n\n        if (!this.isActive || !this.atDestination) {\n            return;\n        }\n\n        if (!this._other) {\n            this.stop();\n            return;\n        }\n\n        // TODO (jkm) check if other actor was removed from world\n        // TODO (jkm) check if other actor has moved and repath player if so\n    }\n\n    /**\n     * Gets the {@link TOtherActor} that this task is interacting with.\n     *\n     * @returns If the other actor is still present, and the actor is at the destination, the other actor.\n     *              Otherwise, `null`.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    protected get other(): TOtherActor | null {\n        // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task\n        //                      as currently the subclass has to store it in a subclass property if it wants to use it\n        //                      without these checks\n        if (!this.atDestination) {\n            return null;\n        }\n\n        if (!this._other) {\n            return null;\n        }\n\n        return this._other;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/impl/actor-landscape-object-interaction-task.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { Actor } from '@engine/world/actor/actor';\nimport { Position } from '@engine/world/position';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { ActorWalkToTask } from './actor-walk-to-task';\n\n/**\n * A task for an actor to interact with a {@link LandscapeObject}.\n *\n * This task extends {@link ActorWalkToTask} and will walk the actor to the object.\n * Once the actor is within range of the object, the task will expose the {@link landscapeObject} property\n *\n * @author jameskmonger\n */\nexport abstract class ActorLandscapeObjectInteractionTask<TActor extends Actor = Actor> extends ActorWalkToTask<TActor, LandscapeObject> {\n    private _landscapeObject: LandscapeObject;\n    private _objectPosition: Position;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param landscapeObject The landscape object to interact with.\n     * @param sizeX The size of the LandscapeObject in the X direction.\n     * @param sizeY The size of the LandscapeObject in the Y direction.\n     */\n    constructor(\n        actor: TActor,\n        landscapeObject: LandscapeObject,\n        // TODO (jkm) get size/orientation automatically from the object's info\n        sizeX: number = 1,\n        sizeY: number = 1,\n    ) {\n        super(\n            actor,\n            landscapeObject,\n            // TODO (jkm) atDestination must take orientation into account\n            Math.max(sizeX, sizeY),\n        );\n\n        if (!landscapeObject) {\n            this.stop();\n            return;\n        }\n\n        // create the Position here to prevent instantiating a new Position every tick\n        this._objectPosition = new Position(landscapeObject.x, landscapeObject.y, landscapeObject.level);\n        this._landscapeObject = landscapeObject;\n    }\n\n    /**\n     * Checks for the continued presence of the {@link LandscapeObject} and stops the task if it is no longer present.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    public execute() {\n        super.execute();\n\n        if (!this.isActive || !this.atDestination) {\n            return;\n        }\n\n        if (!this._landscapeObject) {\n            this.stop();\n            return;\n        }\n\n        const { object: worldObject } = activeWorld.findObjectAtLocation(this.actor, this._landscapeObject.objectId, this._objectPosition);\n\n        if (!worldObject) {\n            this.stop();\n            return;\n        }\n    }\n\n    /**\n     * Gets the {@link LandscapeObject} that this task is interacting with.\n     *\n     * @returns If the object is still present, and the actor is at the destination, the object.\n     *              Otherwise, `null`.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    protected get landscapeObject(): LandscapeObject | null {\n        // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task\n        //                      as currently the subclass has to store it in a subclass property if it wants to use it\n        //                      without these checks\n        if (!this.atDestination) {\n            return null;\n        }\n\n        if (!this._landscapeObject) {\n            return null;\n        }\n\n        return this._landscapeObject;\n    }\n\n    /**\n     * Get the position of this task's landscape object\n     *\n     * @returns The position of this task's landscape object, or null if the landscape object is not present\n     */\n    protected get landscapeObjectPosition(): Position | null {\n        if (!this._landscapeObject) {\n            return null;\n        }\n\n        return this._objectPosition;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/impl/actor-task.ts",
    "content": "import type { Actor } from '@engine/world/actor/actor';\nimport type { Subscription } from 'rxjs';\nimport { Task } from '../task';\nimport type { TaskConfig } from '../types';\nimport { TaskBreakType } from '../types';\n\n/**\n * A task that is executed by an actor.\n *\n * If the task has a break type of ON_MOVE, the ActorTask will subscribe to the actor's\n * movement events and will stop executing when the actor moves.\n *\n * @author jameskmonger\n */\nexport abstract class ActorTask<TActor extends Actor = Actor> extends Task {\n    /**\n     * A function that is called when a movement event is queued on the actor.\n     *\n     * This will be `null` if the task does not break on movement.\n     */\n    private walkingQueueSubscription: Subscription | null = null;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param config The task configuration.\n     */\n    constructor(\n        protected readonly actor: TActor,\n        config?: TaskConfig,\n    ) {\n        super(config);\n\n        this.listenForMovement();\n    }\n\n    /**\n     * Called when the task is stopped and unsubscribes from the actor's walking queue if necessary.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    public onStop(): void {\n        if (this.walkingQueueSubscription) {\n            this.walkingQueueSubscription.unsubscribe();\n        }\n    }\n\n    /**\n     * If required, listen to the actor's walking queue to stop the task\n     *\n     * This function uses `setImmediate` to ensure that the subscription to the\n     * walking queue is not created\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    private listenForMovement(): void {\n        if (!this.breaksOn(TaskBreakType.ON_MOVE)) {\n            return;\n        }\n\n        setImmediate(() => {\n            this.walkingQueueSubscription = this.actor.walkingQueue.movementQueued$.subscribe(() => {\n                this.stop();\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/engine/task/impl/actor-teleport-task.ts",
    "content": "import { ActorTask } from '@engine/task/impl/actor-task';\nimport type { Actor } from '@engine/world/actor/actor';\nimport type { Position } from '@engine/world/position';\n\n/**\n * A task for an actor to teleport to a new position.\n *\n * @author Kat\n */\nexport class ActorTeleportTask<TActor extends Actor = Actor> extends ActorTask<TActor> {\n    private readonly _newPosition: Position;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param newPosition The position to teleport the actor to.\n     */\n    constructor(actor: TActor, newPosition: Position) {\n        super(actor, {\n            repeat: false,\n            immediate: false,\n        });\n        this._newPosition = newPosition;\n    }\n\n    /**\n     * Teleports the actor to the new position.\n     *\n     * TODO (Kat) unit test this\n     */\n    public execute() {\n        if (!this.newPosition) {\n            return;\n        }\n\n        this.actor.teleport(this.newPosition);\n    }\n\n    /**\n     * Get the position the actor will be teleported to.\n     *\n     * @returns The position that the actor will be teleported to.\n     */\n    protected get newPosition(): Position | null {\n        if (!this._newPosition) {\n            return null;\n        }\n\n        return this._newPosition;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/impl/actor-walk-to-task.ts",
    "content": "import type { Actor } from '@engine/world/actor/actor';\nimport { Position } from '@engine/world/position';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { TaskBreakType, TaskStackGroup, TaskStackType } from '../types';\nimport { ActorTask } from './actor-task';\n\n/**\n * Possible types of targets for an actor to walk to.\n */\ntype WalkToTargetType = LandscapeObject | Position;\n\n/**\n * The target can either be a {@link WalkToTargetType} or a function that returns a {@link WalkToTargetType}.\n */\ntype WalkToTarget = WalkToTargetType | (() => WalkToTargetType);\n\n/**\n * This ActorWalkToTask interface allows us to merge with the ActorWalkToTask class\n * and add optional methods to the class.\n *\n * There is no way to add optional methods directly to an abstract class.\n *\n * @author jameskmonger\n */\nexport interface ActorWalkToTask<TActor extends Actor = Actor, TTarget extends WalkToTarget = Position> extends ActorTask<TActor> {\n    /**\n     * An optional function that is called when the actor arrives at the destination.\n     */\n    onArrive?(): void;\n}\n\n/**\n * An abstract task that will make an Actor walk to a specific position,\n * before calling the `arrive` function and continuing execution.\n *\n * The task will be stopped if the adds a new movement to their walking queue.\n *\n * @author jameskmonger\n */\nexport abstract class ActorWalkToTask<TActor extends Actor = Actor, TTarget extends WalkToTarget = Position> extends ActorTask<TActor> {\n    private _atDestination: boolean = false;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param destination The destination position/object, or a function that returns the destination position/object.\n     * @param distance The distance from the destination position that the actor must be within to arrive.\n     * @param walkOnStart Whether to walk to the destination on task start.\n     */\n    constructor(\n        actor: TActor,\n        protected readonly destination: TTarget,\n        protected readonly distance = 1,\n        walkOnStart = true,\n    ) {\n        super(actor, {\n            interval: 1,\n            stackType: TaskStackType.NEVER,\n            stackGroup: TaskStackGroup.ACTION,\n            breakTypes: [TaskBreakType.ON_MOVE],\n            immediate: false,\n            repeat: true,\n        });\n\n        // TODO (jkm) should this be in constructor? or on first execute?\n        if (walkOnStart) {\n            this.actor.pathfinding.walkTo(this.getTargetPosition(), {});\n        }\n    }\n\n    /**\n     * Every tick of the task, check if the actor has arrived at the destination.\n     *\n     * You can check `this.arrived` to see if the actor has arrived.\n     *\n     * If the actor has previously arrived at the destination, but is no longer within distance,\n     * the task will be stopped.\n     *\n     * @returns `true` if the task was stopped this tick, `false` otherwise.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    public execute() {\n        if (!this.isActive) {\n            return;\n        }\n\n        const destination = this.getTargetPosition();\n\n        // TODO this uses actual distances rather than tile distances\n        //      is this correct?\n        const withinDistance = this.actor.position.withinInteractionDistance(destination, this.distance);\n\n        // the WalkToTask itself is complete when the actor has arrived at the destination\n        // execution will now continue in the extended class\n        if (this._atDestination) {\n            // TODO consider making this optional\n            if (!withinDistance) {\n                this._atDestination = false;\n                this.stop();\n            }\n\n            return;\n        }\n\n        if (withinDistance) {\n            this._atDestination = true;\n\n            if (this.onArrive) {\n                this.onArrive();\n            }\n        }\n    }\n\n    private getTargetPosition(): Position {\n        const destination: WalkToTargetType = typeof this.destination === 'function' ? this.destination() : this.destination;\n\n        if (destination instanceof Position) {\n            return destination;\n        }\n\n        return new Position(destination.x, destination.y);\n    }\n\n    /**\n     * `true` if the actor has arrived at the destination.\n     */\n    protected get atDestination(): boolean {\n        return this._atDestination;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/impl/actor-world-item-interaction-task.ts",
    "content": "import type { WorldItem } from '@engine/world/items/world-item';\nimport type { Actor } from '../../world/actor/actor';\nimport { ActorWalkToTask } from './actor-walk-to-task';\n\n/**\n * A task for an actor to interact with a world item.\n *\n * This task extends {@link ActorWalkToTask} and will walk the actor to the world item.\n * Once the actor is within range of the world item, the task will expose the {@link worldItem} property\n *\n * @author jameskmonger\n */\nexport abstract class ActorWorldItemInteractionTask<TActor extends Actor = Actor> extends ActorWalkToTask<TActor> {\n    private _worldItem: WorldItem;\n\n    /**\n     * @param actor The actor executing this task.\n     * @param worldItem The world item to interact with.\n     */\n    constructor(actor: TActor, worldItem: WorldItem) {\n        super(actor, worldItem.position, 1);\n\n        if (!worldItem) {\n            this.stop();\n            return;\n        }\n\n        this._worldItem = worldItem;\n    }\n\n    /**\n     * Checks for the continued presence of the world item and stops the task if it is no longer present.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    public execute() {\n        super.execute();\n\n        if (!this.isActive || !this.atDestination) {\n            return;\n        }\n\n        if (!this._worldItem || this._worldItem.removed) {\n            this.stop();\n            return;\n        }\n    }\n\n    /**\n     * Gets the world item that this task is interacting with.\n     *\n     * @returns If the world item is still present, and the actor is at the destination, the world item.\n     *              Otherwise, `null`.\n     *\n     * TODO (jameskmonger) unit test this\n     */\n    protected get worldItem(): WorldItem | null {\n        // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task\n        //                      as currently the subclass has to store it in a subclass property if it wants to use it\n        //                      without these checks\n        if (!this.atDestination) {\n            return null;\n        }\n\n        if (!this._worldItem || this._worldItem.removed) {\n            return null;\n        }\n\n        return this._worldItem;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/task-scheduler.test.ts",
    "content": "import type { Task } from './task';\nimport { TaskScheduler } from './task-scheduler';\nimport { TaskStackType } from './types';\nimport { createMockTask } from './utils/_testing';\n\ndescribe('TaskScheduler', () => {\n    let taskScheduler: TaskScheduler;\n    beforeEach(() => {\n        taskScheduler = new TaskScheduler();\n    });\n\n    describe('when enqueueing a task', () => {\n        let executeMock: jest.Mock;\n        let task: Task;\n        beforeEach(() => {\n            ({ task, executeMock } = createMockTask());\n        });\n\n        it('should add the task to the running list when ticked', () => {\n            taskScheduler.enqueue(task);\n            taskScheduler.tick();\n            expect(executeMock).toHaveBeenCalled();\n        });\n\n        it('should not add the task to the running list until the next tick', () => {\n            taskScheduler.enqueue(task);\n            expect(executeMock).not.toHaveBeenCalled();\n        });\n\n        describe('when ticked multiple times', () => {\n            beforeEach(() => {\n                taskScheduler.enqueue(task);\n                taskScheduler.tick();\n                taskScheduler.tick();\n            });\n\n            it('should tick the task twice', () => {\n                expect(executeMock).toHaveBeenCalledTimes(2);\n            });\n        });\n\n        describe('when the task is stopped', () => {\n            beforeEach(() => {\n                taskScheduler.enqueue(task);\n                taskScheduler.tick();\n            });\n\n            it('should not tick the task after stopping', () => {\n                task.stop();\n                taskScheduler.tick();\n                expect(executeMock).toHaveBeenCalledTimes(1);\n            });\n        });\n    });\n\n    describe('when enqueueing a task that cannot stack', () => {\n        const interval = 0;\n        const stackType = TaskStackType.NEVER;\n        const stackGroup = 'foo';\n\n        let firstExecuteMock: jest.Mock;\n        let firstTask: Task;\n        beforeEach(() => {\n            ({ task: firstTask, executeMock: firstExecuteMock } = createMockTask(interval, stackType, stackGroup));\n        });\n\n        it('should stop any other tasks with the same stack group', () => {\n            const { task: secondTask, executeMock: secondExecuteMock } = createMockTask(interval, stackType, stackGroup);\n\n            taskScheduler.enqueue(firstTask);\n            taskScheduler.enqueue(secondTask);\n            taskScheduler.tick();\n\n            expect(firstExecuteMock).not.toHaveBeenCalled();\n            expect(secondExecuteMock).toHaveBeenCalled();\n        });\n\n        it('should not stop any other tasks with a different stack group', () => {\n            const otherStackGroup = 'bar';\n            const { task: secondTask, executeMock: secondExecuteMock } = createMockTask(interval, stackType, otherStackGroup);\n\n            taskScheduler.enqueue(firstTask);\n            taskScheduler.enqueue(secondTask);\n            taskScheduler.tick();\n\n            expect(firstExecuteMock).toHaveBeenCalled();\n            expect(secondExecuteMock).toHaveBeenCalled();\n        });\n    });\n\n    describe('when clearing the scheduler', () => {\n        let executeMock: jest.Mock;\n        let task: Task;\n        beforeEach(() => {\n            ({ task, executeMock } = createMockTask());\n        });\n\n        it('should stop all tasks', () => {\n            taskScheduler.enqueue(task);\n            taskScheduler.tick();\n            taskScheduler.clear();\n            taskScheduler.tick();\n            expect(executeMock).toHaveBeenCalledTimes(1);\n        });\n    });\n});\n"
  },
  {
    "path": "src/engine/task/task-scheduler.ts",
    "content": "import { Queue } from '@engine/util/queue';\nimport type { Task } from './task';\nimport { TaskStackType } from './types';\n\n/**\n * A class that ticks tasks in a queue, and removes them when they are no longer active.\n *\n * @author jameskmonger\n */\nexport class TaskScheduler {\n    /**\n     * A queue of tasks that are waiting to be added to the running list.\n     */\n    private pendingTasks = new Queue<Task>();\n\n    /**\n     * The list of tasks that are currently running.\n     */\n    private runningTasks: Task[] = [];\n\n    /**\n     * Register any pending tasks, and tick any running tasks.\n     */\n    public tick(): void {\n        // Add any pending tasks to the running list\n        while (this.pendingTasks.isNotEmpty) {\n            const task = this.pendingTasks.dequeue();\n\n            if (!task || !task.isActive) {\n                continue;\n            }\n\n            this.runningTasks.push(task);\n        }\n\n        // Use an iterator so that we can remove tasks from the list while iterating\n        for (const [index, task] of this.runningTasks.entries()) {\n            if (!task) {\n                continue;\n            }\n\n            task.tick();\n\n            if (!task.isActive) {\n                this.runningTasks.splice(index, 1);\n            }\n        }\n    }\n\n    /**\n     * Add a task to the end of the pending queue.\n     *\n     * If the task has a stack type of `NEVER`, any other tasks in the scheduler\n     * with the same stack group will be stopped.\n     *\n     * @param task The task to add.\n     */\n    public enqueue(task: Task): void {\n        if (!task.isActive) {\n            return;\n        }\n\n        // if the task can't stack with others of a similar type, we need to stop them\n        if (task.stackType === TaskStackType.NEVER) {\n            // Use an iterator so that we can remove tasks from the list while iterating\n            for (const [index, otherTask] of this.runningTasks.entries()) {\n                if (!otherTask) {\n                    continue;\n                }\n\n                if (otherTask.stackGroup === task.stackGroup) {\n                    otherTask.stop();\n                    this.runningTasks.splice(index, 1);\n                }\n            }\n\n            for (const otherTask of this.pendingTasks.items) {\n                if (!otherTask) {\n                    continue;\n                }\n\n                if (otherTask.stackGroup === task.stackGroup) {\n                    otherTask.stop();\n                }\n            }\n        }\n\n        this.pendingTasks.enqueue(task);\n    }\n\n    /**\n     * Clear all tasks from the scheduler.\n     */\n    public clear(): void {\n        this.pendingTasks.clear();\n        this.runningTasks = [];\n    }\n}\n"
  },
  {
    "path": "src/engine/task/task.test.ts",
    "content": "import { Task } from './task';\nimport { TaskStackType } from './types';\nimport { createMockTask } from './utils/_testing';\n\ndescribe('Task', () => {\n    // stacking mechanics are tested in the scheduler\n    const stackType = TaskStackType.NEVER;\n    const stackGroup = 'foo';\n    const breakType = [];\n\n    describe('when interval is 0', () => {\n        const interval = 0;\n\n        // no point setting this to true as the interval is 0\n        const immediate = false;\n\n        let executeMock: jest.Mock;\n        let task: Task;\n\n        describe('and repeat is true', () => {\n            const repeat = false;\n            beforeEach(() => {\n                ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat));\n            });\n\n            describe('when ticked once', () => {\n                beforeEach(() => {\n                    task.tick();\n                });\n\n                it('should execute twice', () => {\n                    expect(executeMock).toHaveBeenCalled();\n                });\n            });\n\n            describe('when ticked twice', () => {\n                beforeEach(() => {\n                    task.tick();\n                    task.tick();\n                });\n\n                it('should execute twice', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n        });\n\n        describe('and repeat is false', () => {\n            const repeat = false;\n            beforeEach(() => {\n                ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat));\n            });\n\n            describe('when ticked once', () => {\n                beforeEach(() => {\n                    task.tick();\n                });\n\n                it('should execute once', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n\n            describe('when ticked twice', () => {\n                beforeEach(() => {\n                    task.tick();\n                    task.tick();\n                });\n\n                it('should execute once', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n        });\n    });\n\n    describe('when interval is 2', () => {\n        const interval = 2;\n\n        // not testing repeat here as it is tested above\n        const repeat = false;\n\n        let executeMock: jest.Mock;\n        let task: Task;\n\n        describe('and immediate is true', () => {\n            const immediate = true;\n\n            beforeEach(() => {\n                ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat));\n            });\n\n            describe('when ticked once', () => {\n                beforeEach(() => {\n                    task.tick();\n                });\n\n                it('should execute once', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n\n            describe('when ticked twice', () => {\n                beforeEach(() => {\n                    task.tick();\n                    task.tick();\n                });\n\n                // task will execute on ticks 1 and 3\n                it('should execute once', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n        });\n\n        describe('and immediate is false', () => {\n            const immediate = false;\n\n            beforeEach(() => {\n                ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat));\n            });\n\n            describe('when ticked once', () => {\n                beforeEach(() => {\n                    task.tick();\n                });\n\n                it('should not execute', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(0);\n                });\n            });\n\n            describe('when ticked twice', () => {\n                beforeEach(() => {\n                    task.tick();\n                    task.tick();\n                });\n\n                // task will execute on ticks 2 and 4\n                it('should execute once', () => {\n                    expect(executeMock).toHaveBeenCalledTimes(1);\n                });\n            });\n        });\n    });\n\n    describe('when there is an onStop callback', () => {\n        let task: Task;\n        let onStopMock: jest.Mock;\n        let executeMock: jest.Mock;\n\n        beforeEach(() => {\n            onStopMock = jest.fn();\n            executeMock = jest.fn();\n            task = new (class extends Task {\n                constructor() {\n                    super();\n                }\n\n                public execute(): void {\n                    executeMock();\n                }\n\n                public onStop(): void {\n                    onStopMock();\n                }\n            })();\n        });\n\n        describe('when the task is stopped', () => {\n            beforeEach(() => {\n                task.stop();\n            });\n\n            it('should call the onStop callback', () => {\n                expect(onStopMock).toHaveBeenCalled();\n            });\n\n            it('should not call the execute callback', () => {\n                expect(executeMock).not.toHaveBeenCalled();\n            });\n\n            describe('when the task is ticked', () => {\n                beforeEach(() => {\n                    task.tick();\n                });\n\n                it('should not call the onStop callback', () => {\n                    expect(onStopMock).toHaveBeenCalledTimes(1);\n                });\n\n                it('should not call the execute callback', () => {\n                    expect(executeMock).not.toHaveBeenCalled();\n                });\n            });\n\n            describe('when the task is stopped again', () => {\n                beforeEach(() => {\n                    task.stop();\n                });\n\n                it('should not call the onStop callback', () => {\n                    expect(onStopMock).toHaveBeenCalledTimes(1);\n                });\n\n                it('should not call the execute callback', () => {\n                    expect(executeMock).not.toHaveBeenCalled();\n                });\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "src/engine/task/task.ts",
    "content": "import type { TaskBreakType, TaskConfig } from './types';\nimport { TaskStackGroup, TaskStackType } from './types';\n\nconst DEFAULT_TASK_CONFIG: Required<TaskConfig> = {\n    interval: 1,\n    stackType: TaskStackType.STACK,\n    stackGroup: TaskStackGroup.ACTION,\n    immediate: false,\n    breakTypes: [],\n    repeat: true,\n};\n\nfunction readConfigValue(key: keyof TaskConfig, config?: TaskConfig): any {\n    if (!config) {\n        return DEFAULT_TASK_CONFIG[key];\n    }\n\n    return config[key] !== undefined ? config[key] : DEFAULT_TASK_CONFIG[key];\n}\n\n/**\n * This Task interface allows us to merge with the Task class\n * and add optional methods to the class.\n *\n * There is no way to add optional methods directly to an abstract class.\n *\n * @author jameskmonger\n */\nexport interface Task {\n    /**\n     * A callback that is called when the task is stopped.\n     */\n    onStop?(): void;\n}\n\n/**\n * A Task which can be ticked and executes after a specified number of ticks.\n *\n * The task can be configured to execute once, or repeatedly, and can also be executed immediately.\n *\n * @author jameskmonger\n */\nexport abstract class Task {\n    /**\n     * How the task should be stacked with other tasks of the same stack group.\n     */\n    public readonly stackType: TaskStackType;\n\n    /**\n     * The stack group for this task.\n     */\n    public readonly stackGroup: string;\n\n    /**\n     * Conditions under which the task should be broken.\n     */\n    public readonly breakTypes: TaskBreakType[];\n\n    /**\n     * The number of ticks between each execution of the task.\n     */\n    private interval: number;\n\n    /**\n     * The number of ticks remaining before the task is executed.\n     */\n    private ticksRemaining: number;\n\n    /**\n     * Should the task be repeated indefinitely?\n     */\n    private repeat: boolean;\n\n    private _isActive = true;\n\n    /**\n     * @param config the configuration options for the task\n     *\n     * @see TaskConfig for more information on the configuration options\n     */\n    public constructor(config?: TaskConfig) {\n        this.interval = readConfigValue('interval', config);\n        this.stackType = readConfigValue('stackType', config);\n        this.stackGroup = readConfigValue('stackGroup', config);\n\n        const immediate = readConfigValue('immediate', config);\n        this.ticksRemaining = immediate ? 0 : this.interval;\n        this.breakTypes = readConfigValue('breakTypes', config);\n        this.repeat = readConfigValue('repeat', config);\n    }\n\n    /**\n     * The task's execution logic.\n     *\n     * Ensure that you call `super.execute()` if you override this method!\n     *\n     * TODO (jameskmonger) consider some kind of workaround to enforce a super call\n     *              https://github.com/microsoft/TypeScript/issues/21388#issuecomment-360214959\n     */\n    public abstract execute(): void;\n\n    /**\n     * Whether this task breaks on the specified {@link TaskBreakType}.\n     *\n     * @param breakType the break type to check\n     *\n     * @returns true if the task breaks on the specified break type\n     */\n    public breaksOn(breakType: TaskBreakType): boolean {\n        return this.breakTypes.includes(breakType);\n    }\n\n    /**\n     * Stop the task from executing.\n     *\n     * @returns true if the task was stopped, false if the task was already stopped\n     */\n    public stop(): boolean {\n        // can't stop a task that's already stopped\n        if (!this._isActive) {\n            return false;\n        }\n\n        this._isActive = false;\n\n        if (this.onStop) {\n            this.onStop();\n        }\n\n        return true;\n    }\n\n    /**\n     * Tick the task, decrementing the number of ticks remaining.\n     *\n     * If the number of ticks remaining reaches zero, the task is executed.\n     *\n     * If the task is configured to repeat, the number of ticks remaining is reset to the interval.\n     * Otherwise, the task is stopped.\n     */\n    public tick(): void {\n        if (!this._isActive) {\n            return;\n        }\n\n        this.ticksRemaining--;\n\n        if (this.ticksRemaining <= 0) {\n            // TODO maybe track and expose executionCount to this child function\n            this.execute();\n\n            // TODO should we allow the repeat count to be specified?\n            if (this.repeat) {\n                this.ticksRemaining = this.interval;\n            } else {\n                // TODO should I be calling a public function rather than setting the private variable?\n                this.stop();\n            }\n        }\n    }\n\n    /**\n     * Is the task active?\n     */\n    public get isActive(): boolean {\n        return this._isActive;\n    }\n}\n"
  },
  {
    "path": "src/engine/task/types.ts",
    "content": "/**\n * An enum to control the different stacking modes for tasks.\n *\n * @author jameskmonger\n */\nexport enum TaskStackType {\n    /**\n     * This task cannot be stacked with other tasks of the same stack group.\n     */\n    NEVER,\n\n    /**\n     * This task can be stacked with other tasks of the same stack group.\n     */\n    STACK,\n}\n\n/**\n * An enum to control the different stack groups for tasks.\n *\n * When a task has a stack type of `NEVER`, other tasks with the same stack group will be cancelled.\n *\n * @author jameskmonger\n */\nexport enum TaskStackGroup {\n    /**\n     * An action task undertaken by an actor.\n     */\n    ACTION = 'action',\n}\n\n/**\n * An enum to control the different breaking modes for tasks.\n *\n * @author jameskmonger\n */\nexport enum TaskBreakType {\n    /**\n     * This task gets stopped when the player moves\n     */\n    ON_MOVE,\n}\n\n/**\n * The configuration options for a Task.\n *\n * All options are optional as they have default values.\n *\n * @author jameskmonger\n */\nexport type TaskConfig = Partial<\n    Readonly<{\n        /**\n         * How the task should be stacked with other tasks of the same stack group.\n         */\n        stackType: TaskStackType;\n\n        /**\n         * The stack group for this task.\n         */\n        stackGroup: string;\n\n        /**\n         * Conditions under which the task should be broken.\n         */\n        breakTypes: TaskBreakType[];\n\n        /**\n         * The number of ticks between each execution of the task.\n         */\n        interval: number;\n\n        /**\n         * Should the task be executed on the first tick after it is added?\n         */\n        immediate: boolean;\n\n        /**\n         * Should the task be repeated indefinitely?\n         */\n        repeat: boolean;\n    }>\n>;\n"
  },
  {
    "path": "src/engine/task/utils/_testing.ts",
    "content": "import { Task } from '../task';\nimport type { TaskBreakType } from '../types';\nimport { TaskStackGroup, TaskStackType } from '../types';\n\nexport function createMockTask(\n    interval: number = 0,\n    stackType: TaskStackType = TaskStackType.STACK,\n    stackGroup: string = TaskStackGroup.ACTION,\n    immediate: boolean = false,\n    breakTypes: TaskBreakType[] = [],\n    repeat: boolean = true,\n) {\n    const executeMock = jest.fn();\n    const task = new (class extends Task {\n        constructor() {\n            super({\n                interval,\n                stackType,\n                stackGroup,\n                immediate,\n                breakTypes,\n                repeat,\n            });\n        }\n\n        public execute(): void {\n            executeMock();\n        }\n    })();\n\n    return { task, executeMock };\n}\n"
  },
  {
    "path": "src/engine/util/address.ts",
    "content": "export const addressToInt = (address: string): number => {\n    if (!address) {\n        return 0;\n    }\n\n    const parts = address.split('.');\n    if (!parts || parts.length !== 4) {\n        return 0;\n    }\n\n    const num = parts.map(p => parseInt(p));\n\n    return ((num[0] & 0xff) << 24) + ((num[1] & 0xff) << 16) + ((num[2] & 0xff) << 8) + (num[3] & 0xff);\n};\n"
  },
  {
    "path": "src/engine/util/colors.ts",
    "content": "export function hexToRgb(hex: number): { r: number; b: number; g: number } {\n    return {\n        r: (hex >> 16) & 0xff,\n        g: (hex >> 8) & 0xff,\n        b: hex & 0xff,\n    };\n}\n\nexport function hexToHexString(hex: number): string {\n    let r = ((hex >> 16) & 0xff).toString(16);\n    let g = ((hex >> 8) & 0xff).toString(16);\n    let b = (hex & 0xff).toString(16);\n    if (r === '0') {\n        r = '00';\n    }\n    if (g === '0') {\n        g = '00';\n    }\n    if (b === '0') {\n        b = '00';\n    }\n    return r + g + b;\n}\n\nexport function rgbTo16Bit(r: number, g: number, b: number): number {\n    return ((r & 0x1f) << 11) | ((g & 0x3f) << 5) | ((b & 0x1f) << 0);\n}\n\nexport const colors = {\n    green: 0x00ff00,\n    yellow: 0xffff00,\n    red: 0xff0000,\n    black: 0x000000,\n    blue: 0x01bdfe,\n    lightred: 0xef101f,\n};\n"
  },
  {
    "path": "src/engine/util/data.ts",
    "content": "export function hasValueNotNull(variable: unknown): boolean {\n    return typeof variable !== 'undefined' && variable !== null;\n}\n"
  },
  {
    "path": "src/engine/util/error-handling.ts",
    "content": "import { logger } from '@runejs/common';\n\n/*\n * Error handling! Feel free to add other types of errors or warnings here. :)\n */\n\nexport class WidgetsClosedWarning extends Error {\n    constructor() {\n        super();\n        this.name = 'WidgetsClosedWarning';\n        this.message = 'The active widget was closed before the action could be completed.';\n    }\n}\n\nexport class ActionsCancelledWarning extends Error {\n    constructor() {\n        super();\n        this.name = 'ActionsCancelledWarning';\n        this.message = 'Pending and active actions were cancelled before they could be completed.';\n    }\n}\n\nconst warnings = [WidgetsClosedWarning, ActionsCancelledWarning];\n\nexport function initErrorHandling(): void {\n    process.on('unhandledRejection', (error: any, promise) => {\n        for (const t of warnings) {\n            if (error instanceof t) {\n                logger.warn(`Promise cancelled with warning: ${error.name}`);\n                return;\n            }\n        }\n\n        logger.error(`Unhandled promise rejection from ${promise}, reason: ${error}`);\n        if (error && error['stack']) {\n            logger.error((error as any).stack);\n        }\n    });\n}\n"
  },
  {
    "path": "src/engine/util/files.ts",
    "content": "import fs from 'fs';\nimport util from 'util';\nimport { watch } from 'chokidar';\nimport type { Observable } from 'rxjs';\nimport { Subject } from 'rxjs';\n\nconst readdir = util.promisify(fs.readdir);\nconst stat = util.promisify(fs.stat);\n\nexport async function getFiles(directory: string, blacklist: string[]);\nexport async function getFiles(directory: string, whitelist: string[], useWhitelist: boolean);\nexport async function* getFiles(directory: string, list: string[] = [], useWhitelist?: boolean): AsyncGenerator<string> {\n    const files = await readdir(directory);\n\n    for (const file of files) {\n        const path = directory + '/' + file;\n        const statistics = await stat(path);\n\n        if (statistics.isDirectory()) {\n            // (Jameskmonger) I set the default value of `true` here, not sure if it is correct.\n            for await (const child of getFiles(path, list, useWhitelist || false)) {\n                yield child;\n            }\n        } else {\n            if (!useWhitelist) {\n                // blacklist\n                const invalid = list.some(item => file === item);\n\n                if (invalid) {\n                    continue;\n                }\n            } else {\n                // whitelist\n                const invalid = !list.some(item => file.endsWith(item));\n\n                if (invalid) {\n                    continue;\n                }\n            }\n\n            yield path;\n        }\n    }\n}\n\nexport function watchSource(dir: string): Observable<void> {\n    const subject = new Subject<void>();\n    const watcher = watch(dir);\n    watcher.on('ready', () => {\n        watcher.on('all', () => {\n            subject.next();\n        });\n    });\n\n    return subject.asObservable();\n}\n\nexport function watchForChanges(dir: string, regex: RegExp): void {\n    const watcher = watch(dir);\n    watcher.on('ready', () => {\n        watcher.on('all', () => {\n            Object.keys(require.cache).forEach(id => {\n                if (regex.test(id)) {\n                    delete require.cache[id];\n                }\n            });\n        });\n    });\n}\n"
  },
  {
    "path": "src/engine/util/num.ts",
    "content": "export const randomBetween = (min: number, max: number): number => {\n    return Math.floor(Math.random() * (max - min + 1) + min);\n};\n"
  },
  {
    "path": "src/engine/util/objects.ts",
    "content": "/**\n * Merge two objects or arrays, first object takes priority in case where the values cannot be merged\n * @param objectA\n * @param objectB\n * @return objectC combination of objectA and objectB\n */\nexport function deepMerge<T>(objectA: T, objectB: T): T {\n    if (!objectA) {\n        return objectB;\n    }\n    if (!objectB) {\n        return objectA;\n    }\n    if (Array.isArray(objectA)) {\n        return [...new Set([...(objectA as any), ...(objectB as any)])] as any;\n    }\n    if (typeof objectA === 'object') {\n        const newObject: T = { ...objectA };\n        const keys = [...new Set([...Object.keys(objectA), ...Object.keys(objectB)])];\n        keys.forEach(key => {\n            if (!objectA[key]) {\n                newObject[key] = objectB[key];\n                return;\n            }\n            if (!objectB[key]) {\n                newObject[key] = objectA[key];\n            }\n            if (Array.isArray(objectA[key])) {\n                if (!Array.isArray(objectB[key])) {\n                    newObject[key] = [...objectA[key], objectB[key]];\n                    return;\n                }\n                newObject[key] = deepMerge(objectA[key], objectB[key]);\n                return;\n            }\n            if (Array.isArray(objectB[key])) {\n                if (!Array.isArray(objectA[key])) {\n                    newObject[key] = [...objectB[key], objectA[key]];\n                    return;\n                }\n                console.error('Something is wrong with deepmerger', key, objectA, objectB);\n            }\n            if (typeof objectA[key] === 'object' || typeof objectB === 'object') {\n                newObject[key] = deepMerge(objectA[key], objectB[key]);\n                return;\n            }\n            newObject[key] = objectA[key];\n        });\n        return newObject;\n    }\n    return objectA;\n}\n"
  },
  {
    "path": "src/engine/util/queue.test.ts",
    "content": "import { Queue } from './queue';\n\ndescribe('Queue', () => {\n    let queue: Queue<number>;\n    beforeEach(() => {\n        queue = new Queue<number>();\n    });\n\n    describe('checking Queue length', () => {\n        it('should be empty when created', () => {\n            expect(queue.isEmpty).toBe(true);\n            expect(queue.isNotEmpty).toBe(false);\n        });\n\n        it('should be not empty when an item is added', () => {\n            queue.enqueue(1);\n            expect(queue.isEmpty).toBe(false);\n            expect(queue.isNotEmpty).toBe(true);\n        });\n\n        it('should be empty when all items are removed', () => {\n            queue.enqueue(1);\n            queue.dequeue();\n            expect(queue.isEmpty).toBe(true);\n            expect(queue.isNotEmpty).toBe(false);\n        });\n\n        it('should return the correct length', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            expect(queue.length).toBe(3);\n        });\n    });\n\n    it('should return the correct items', () => {\n        queue.enqueue(1);\n        queue.enqueue(2);\n        queue.enqueue(3);\n        expect(queue.items).toEqual([1, 2, 3]);\n    });\n\n    describe('when peeking', () => {\n        it('should return the correct item', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            expect(queue.peek()).toBe(1);\n        });\n\n        it('should not remove the item', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            queue.peek();\n            expect(queue.items).toEqual([1, 2, 3]);\n        });\n\n        it('should return undefined when the queue is empty', () => {\n            expect(queue.peek()).toBeUndefined();\n        });\n    });\n\n    describe('when dequeuing', () => {\n        it('should return the correct item', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            expect(queue.dequeue()).toBe(1);\n        });\n\n        it('should remove the item', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            queue.dequeue();\n            expect(queue.items).toEqual([2, 3]);\n        });\n\n        it('should return undefined when the queue is empty', () => {\n            expect(queue.dequeue()).toBeUndefined();\n        });\n    });\n\n    describe('when clearing', () => {\n        it('should remove all items', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            queue.clear();\n            expect(queue.items).toEqual([]);\n        });\n    });\n\n    describe('when iterating', () => {\n        it('should iterate over all items', () => {\n            queue.enqueue(1);\n            queue.enqueue(2);\n            queue.enqueue(3);\n            const items: number[] = [];\n            for (const item of queue.items) {\n                items.push(item);\n            }\n            expect(items).toEqual([1, 2, 3]);\n        });\n    });\n});\n"
  },
  {
    "path": "src/engine/util/queue.ts",
    "content": "/**\n * A first-in-first-out queue.\n *\n * @author jameskmonger\n */\nexport class Queue<TItem> {\n    private _items: TItem[] = [];\n\n    /**\n     * Add an item to the end of the queue.\n     * @param item The item to add.\n     */\n    public enqueue(item: TItem): void {\n        this._items.push(item);\n    }\n\n    /**\n     * Remove an item from the front of the queue.\n     * @returns The item removed.\n     */\n    public dequeue(): TItem | undefined {\n        const item = this._items.shift();\n\n        if (!item) {\n            return undefined;\n        }\n\n        return item;\n    }\n\n    /**\n     * Get the item at the front of the queue without removing it.\n     * @returns The item at the front of the queue.\n     */\n    public peek(): TItem {\n        return this._items[0];\n    }\n\n    /**\n     * Remove all items from the queue.\n     */\n    public clear(): void {\n        this._items = [];\n    }\n\n    /**\n     * Get the items in the queue.\n     */\n    public get items(): TItem[] {\n        return this._items;\n    }\n\n    /**\n     * Get the length of the queue.\n     */\n    public get length(): number {\n        return this._items.length;\n    }\n\n    /**\n     * Is the queue empty?\n     */\n    public get isEmpty(): boolean {\n        return this._items.length === 0;\n    }\n\n    /**\n     * Does the queue contain items?\n     */\n    public get isNotEmpty(): boolean {\n        return this._items.length > 0;\n    }\n}\n"
  },
  {
    "path": "src/engine/util/strings.ts",
    "content": "import { hexToHexString } from '@engine/util/colors';\nimport { FontName } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\n\nexport const startsWithVowel = (str: string): boolean => {\n    str = str.trim().toLowerCase();\n\n    const firstChar = str.charAt(0);\n\n    return firstChar === 'a' || firstChar === 'e' || firstChar === 'i' || firstChar === 'o' || firstChar === 'u';\n};\n\nfunction getFont(font?: number | string) {\n    if (font && typeof font === 'number') {\n        return filestore.fontStore.getFontById(font);\n    } else if (font && typeof font === 'string') {\n        return filestore.fontStore.getFontByName(FontName[font]);\n    } else {\n        // Default font, subject to change\n        return filestore.fontStore.getFontByName(FontName.p12_full);\n    }\n}\n\nexport enum TextDecoration {\n    Color,\n    Decoration,\n}\n\nfunction getStylingType(tag: string) {\n    let _tag = tag;\n    if (_tag.charAt(0) === '/') {\n        _tag = _tag.substring(1);\n    }\n\n    if (_tag.startsWith('col')) {\n        return TextDecoration.Color;\n    } else {\n        return TextDecoration.Decoration;\n    }\n}\n\n// Thank you to the Apollo team for these values. :)\nconst charWidths = [\n    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 7, 14, 9, 12, 12, 4, 5, 5, 10, 8,\n    4, 8, 4, 7, 9, 7, 9, 8, 8, 8, 9, 7, 9, 9, 4, 5, 7, 9, 7, 9, 14, 9, 8, 8, 8, 7, 7, 9, 8, 6, 8, 8, 7, 10, 9, 9, 8, 9, 8, 8, 6, 9, 8, 10,\n    8, 8, 8, 6, 7, 6, 9, 10, 5, 8, 8, 7, 8, 8, 7, 8, 8, 4, 7, 7, 4, 10, 8, 8, 8, 8, 6, 8, 6, 8, 8, 9, 8, 8, 8, 6, 4, 6, 12, 3, 10, 3, 3, 3,\n    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 8, 11, 8, 8, 4, 8, 7, 12, 6, 7, 9, 5, 12, 5,\n    6, 10, 6, 6, 6, 8, 8, 4, 5, 5, 6, 7, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 13, 8, 8, 8, 8, 8, 4, 4, 5, 4, 8, 9, 9, 9, 9, 9, 9, 8, 10, 9, 9,\n    9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 6, 8, 8, 8, 8, 4, 4, 5, 4, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,\n];\n\n// TODO refactor a bit\nexport function wrapText(text: string, maxWidth: number, font?: number | string): string[] {\n    const lines: string[] = [];\n    const selectedFont = getFont(font);\n    const colorQueue: string[] = [];\n    const decorationQueue: string[] = [];\n    const remainingText = text.split('').reverse();\n    let currentLine = '';\n    let currentWidth = 0;\n    let currentTagIndex = -1;\n\n    while (remainingText.length > 0) {\n        const char = remainingText.pop();\n\n        let hidden = false;\n        let rendered = true;\n\n        switch (char) {\n            case '<':\n                hidden = true;\n                currentTagIndex = currentLine.length + 1;\n                break;\n            case '>':\n                hidden = true;\n                const currentTag = currentLine.substring(currentTagIndex, currentLine.length);\n                currentTagIndex = -1;\n                const isClosing = currentTag.charAt(0) === '/';\n                const type = getStylingType(currentTag);\n                if (type === TextDecoration.Decoration) {\n                    if (!isClosing) {\n                        decorationQueue.push(currentTag);\n                    } else {\n                        decorationQueue.pop();\n                    }\n                } else {\n                    if (!isClosing) {\n                        colorQueue.push(currentTag);\n                    } else {\n                        colorQueue.pop();\n                    }\n                }\n                break;\n            case '@':\n                break;\n            case '\\n':\n                hidden = true;\n                currentWidth = maxWidth;\n                rendered = false;\n                break;\n            case ' ':\n                if (currentLine[currentLine.length - 1] === ' ' || currentWidth === 0) {\n                    hidden = true;\n                    rendered = false;\n                }\n                break;\n            default:\n                break;\n        }\n        if (rendered) {\n            currentLine += char;\n        }\n        if (!hidden && currentTagIndex == -1 && char !== undefined) {\n            const charWidth = selectedFont.getCharWidth(char);\n            currentWidth += charWidth;\n        }\n\n        if (currentWidth >= maxWidth) {\n            let lastSpace = currentLine.lastIndexOf(' ');\n            const lastTag = currentLine.lastIndexOf('<');\n            if (lastTag > lastSpace && char !== '\\n') {\n                lastSpace = lastTag;\n                const type = getStylingType(currentLine.substring(lastTag + 1));\n                if (type === TextDecoration.Decoration) {\n                    decorationQueue.pop();\n                } else {\n                    colorQueue.pop();\n                }\n            }\n            let lineToPush = currentLine;\n            let remainder = '';\n            if (lastSpace != -1 && char != '\\n') {\n                lineToPush = lineToPush.substring(0, lastSpace);\n                remainder = currentLine.substring(lastSpace);\n            }\n\n            decorationQueue\n                .slice(0)\n                .reverse()\n                .map(tag => (lineToPush += `</${tag}>`));\n            colorQueue\n                .slice(0)\n                .reverse()\n                .map(tag => (lineToPush += `</${tag}>`));\n            lines.push(lineToPush.trim());\n            currentLine = '';\n            decorationQueue.slice(0).map(tag => (currentLine += `<${tag}>`));\n            colorQueue.slice(0).map(tag => (currentLine += `<${tag}>`));\n            remainingText.push(...remainder.split('').reverse());\n            currentWidth = 0;\n        }\n    }\n    if (currentLine !== '\\n') {\n        lines.push(currentLine);\n    }\n\n    // logger.info('split lines: ' + lines)\n    return lines;\n}\n\nconst VALID_CHARS = [\n    '_',\n    'a',\n    'b',\n    'c',\n    'd',\n    'e',\n    'f',\n    'g',\n    'h',\n    'i',\n    'j',\n    'k',\n    'l',\n    'm',\n    'n',\n    'o',\n    'p',\n    'q',\n    'r',\n    's',\n    't',\n    'u',\n    'v',\n    'w',\n    'x',\n    'y',\n    'z',\n    '0',\n    '1',\n    '2',\n    '3',\n    '4',\n    '5',\n    '6',\n    '7',\n    '8',\n    '9',\n    '!',\n    '@',\n    '#',\n    '$',\n    '%',\n    '^',\n    '&',\n    '*',\n    '(',\n    ')',\n    '-',\n    '+',\n    '=',\n    ':',\n    ';',\n    '.',\n    '>',\n    '<',\n    ',',\n    '\"',\n    '[',\n    ']',\n    '|',\n    '?',\n    '/',\n    '`',\n];\n\nexport function longToString(nameLong: bigint): string {\n    let ac: string = '';\n    while (nameLong !== BigInt(0)) {\n        const l1 = nameLong;\n        nameLong = BigInt(nameLong) / BigInt(37);\n        ac += VALID_CHARS[parseInt(l1.toString()) - parseInt(nameLong.toString()) * 37];\n    }\n\n    return ac.split('').reverse().join('');\n}\n\nexport function stringToLong(s: string): bigint {\n    let l: bigint = BigInt(0);\n\n    for (let i = 0; i < s.length && i < 12; i++) {\n        const c = s.charAt(i);\n        const cc = s.charCodeAt(i);\n        l *= BigInt(37);\n        if (c >= 'A' && c <= 'Z') l += BigInt(1 + cc - 65);\n        else if (c >= 'a' && c <= 'z') l += BigInt(1 + cc - 97);\n        else if (c >= '0' && c <= '9') l += BigInt(27 + cc - 48);\n    }\n    while (l % BigInt(37) == BigInt(0) && l != BigInt(0)) l /= BigInt(37);\n    return l;\n}\n\nexport const colorText = (s: string, hexColor: number): string => `<col=${hexToHexString(hexColor)}>${s}</col>`;\n"
  },
  {
    "path": "src/engine/util/time.ts",
    "content": "export const rsTime = (date: Date): number => {\n    const days = Math.round(date.getTime() / 0x5265c00);\n    return days - 11745;\n};\n\nexport const daysSinceLastLogin = (lastLogin: Date): number => {\n    if (!lastLogin) {\n        return -1;\n    }\n\n    return Math.floor(Math.abs(new Date().valueOf() - lastLogin.valueOf()) / (1000 * 60 * 60 * 24));\n};\n"
  },
  {
    "path": "src/engine/util/varbits.ts",
    "content": "import { filestore } from '@server/game/game-server';\n\nconst varbitMasks: number[] = [];\n\n/**\n * Returns the index to morph actor/object into, based on set config\n * @param varbitId\n * @param playerConfig\n * @return index to morph into\n */\nexport function getVarbitMorphIndex(varbitId, playerConfig) {\n    if (varbitMasks.length === 0) {\n        let i = 2;\n        for (let i_7_ = 0; i_7_ < 32; i_7_++) {\n            varbitMasks[i_7_] = -1 + i;\n            i += i;\n        }\n    }\n    const varbitDefinition = filestore.configStore.varbitStore.getVarbit(varbitId);\n\n    if (!varbitDefinition) {\n        throw new Error(`Could not find varbit definition for id ${varbitId}`);\n    }\n\n    const mostSignificantBit = varbitDefinition.mostSignificantBit;\n    const configId = varbitDefinition.index;\n    const leastSignificantBit = varbitDefinition.leastSignificantBit;\n    // TODO: Unknown\n    const i_8_ = varbitMasks[mostSignificantBit - leastSignificantBit];\n    const configValue = playerConfig && playerConfig[configId] ? playerConfig[configId] : 0;\n    return (configValue >> leastSignificantBit) & i_8_;\n}\n"
  },
  {
    "path": "src/engine/world/actor/actor.ts",
    "content": "import type { ActionCancelType } from '@engine/action/action-pipeline';\nimport { ActionPipeline } from '@engine/action/action-pipeline';\nimport { QueueableTask } from '@engine/action/pipe/task/queueable-task';\nimport type { DefensiveBonuses, OffensiveBonuses, SkillBonuses } from '@engine/config/item-config';\nimport type { Task } from '@engine/task/task';\nimport { TaskScheduler } from '@engine/task/task-scheduler';\nimport { activeWorld } from '@engine/world';\nimport { directionFromIndex } from '@engine/world/direction';\nimport type { WorldInstance } from '@engine/world/instances';\nimport type { Item } from '@engine/world/items/item';\nimport { ItemContainer } from '@engine/world/items/item-container';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\nimport { Subject } from 'rxjs';\nimport { filter, take } from 'rxjs/operators';\nimport type { ActorMetadata } from './metadata';\nimport { Pathfinding } from './pathfinding';\nimport { Skills } from './skills';\nimport type { Animation, Graphic } from './update-flags';\nimport { UpdateFlags } from './update-flags';\nimport { isNpc } from './util';\nimport { WalkingQueue } from './walking-queue';\n\nexport type ActorType = 'player' | 'npc';\n\n/**\n * Handles an entity within the game world.\n */\nexport abstract class Actor {\n    public readonly type: ActorType;\n    public readonly updateFlags: UpdateFlags = new UpdateFlags();\n    public readonly skills: Skills = new Skills(this);\n    public readonly walkingQueue: WalkingQueue = new WalkingQueue(this);\n    public readonly inventory: ItemContainer = new ItemContainer(28);\n    public readonly bank: ItemContainer = new ItemContainer(376);\n    public readonly actionPipeline = new ActionPipeline(this);\n\n    /**\n     * The map of available metadata for this actor.\n     *\n     * You cannot guarantee that this will be populated with data, so you should always check for the existence of the\n     * metadata you are looking for before using it.\n     *\n     * @author jameskmonger\n     */\n    public readonly metadata: Partial<ActorMetadata> = {};\n\n    /**\n     * @deprecated - use new action system instead\n     */\n    public readonly actionsCancelled: Subject<ActionCancelType> = new Subject<ActionCancelType>();\n\n    public pathfinding: Pathfinding = new Pathfinding(this);\n    public lastMovementPosition: Position;\n\n    protected randomMovementInterval;\n    protected _instance: WorldInstance | null = null;\n\n    /**\n     * Is this actor currently active? If true, the actor will have its task queue processed.\n     *\n     * This is true for players that are currently logged in, and NPCs that are currently in the world.\n     */\n    protected active: boolean;\n\n    /**\n     * @deprecated - use new action system instead\n     */\n    private _busy: boolean = false;\n    private _position: Position;\n    private _lastMapRegionUpdatePosition: Position;\n    private _worldIndex: number;\n    private _walkDirection: number;\n    private _runDirection: number;\n    private _faceDirection: number;\n    private _bonuses: { offensive: OffensiveBonuses; defensive: DefensiveBonuses; skill: SkillBonuses };\n\n    private readonly scheduler = new TaskScheduler();\n\n    protected constructor(actorType: ActorType) {\n        this.type = actorType;\n        this._walkDirection = -1;\n        this._runDirection = -1;\n        this._faceDirection = 6;\n        this.clearBonuses();\n    }\n\n    public abstract equals(actor: Actor): boolean;\n\n    /**\n     * Instantiate a task with the Actor instance and a set of arguments.\n     *\n     * @param taskClass The task class to instantiate. Must be a subclass of {@link Task}\n     * @param args The arguments to pass to the task constructor\n     *\n     * If the task has a stack type of `NEVER`, other tasks in the same {@link TaskStackGroup} will be cancelled.\n     */\n    public enqueueTask(taskClass: new (actor: Actor) => Task, ...args: never[]): void;\n    public enqueueTask<T1, T2, T3, T4, T5, T6>(\n        taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Task,\n        args: [T1, T2, T3, T4, T5, T6],\n    ): void;\n    public enqueueTask<T1, T2, T3, T4, T5>(\n        taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Task,\n        args: [T1, T2, T3, T4, T5],\n    ): void;\n    public enqueueTask<T1, T2, T3, T4>(\n        taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Task,\n        args: [T1, T2, T3, T4],\n    ): void;\n    public enqueueTask<T1, T2, T3>(taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3) => Task, args: [T1, T2, T3]): void;\n    public enqueueTask<T1, T2>(taskClass: new (actor: Actor, arg1: T1, arg2: T2) => Task, args: [T1, T2]): void;\n    public enqueueTask<T1>(taskClass: new (actor: Actor, arg1: T1) => Task, args: [T1]): void;\n    public enqueueTask<T>(taskClass: new (actor: Actor, ...args: T[]) => Task, args: T[]): void {\n        if (!this.active) {\n            logger.warn(`Attempted to instantiate task for inactive actor`);\n            return;\n        }\n\n        if (args) {\n            this.enqueueBaseTask(new taskClass(this, ...args));\n        } else {\n            this.enqueueBaseTask(new taskClass(this));\n        }\n    }\n\n    /**\n     * Adds a task to the actor's scheduler queue. These tasks will be stopped when they become inactive.\n     *\n     * If the task has a stack type of `NEVER`, other tasks in the same group will be cancelled.\n     *\n     * @param task The task to add\n     */\n    public enqueueBaseTask(task: Task): void {\n        if (!this.active) {\n            logger.warn(`Attempted to enqueue task for  inactive actor`);\n            return;\n        }\n\n        this.scheduler.enqueue(task);\n    }\n\n    /**\n     * Instantly teleports the actor to the specified location.\n     * @param newPosition The actor's new position.\n     */\n    public teleport(newPosition: Position): void {\n        this.walkingQueue.clear();\n        this.metadata['lastPosition'] = this.position.copy();\n        this.position = newPosition;\n        this.metadata.teleporting = true;\n    }\n\n    public clearBonuses(): void {\n        this._bonuses = {\n            offensive: {\n                speed: 0,\n                stab: 0,\n                slash: 0,\n                crush: 0,\n                magic: 0,\n                ranged: 0,\n            },\n            defensive: {\n                stab: 0,\n                slash: 0,\n                crush: 0,\n                magic: 0,\n                ranged: 0,\n            },\n            skill: {\n                strength: 0,\n                prayer: 0,\n            },\n        };\n    }\n\n    public moveBehind(target: Actor): boolean {\n        if (this.position.level !== target.position.level) {\n            return false;\n        }\n\n        const distance = Math.floor(this.position.distanceBetween(target.position));\n        if (distance > 16) {\n            this.clearFaceActor();\n            return false;\n        }\n\n        let ignoreDestination = true;\n        let desiredPosition = target.position;\n        if (target.lastMovementPosition) {\n            desiredPosition = target.lastMovementPosition;\n            ignoreDestination = false;\n        }\n\n        this.pathfinding.walkTo(desiredPosition, {\n            pathingSearchRadius: distance + 2,\n            ignoreDestination,\n        });\n\n        return true;\n    }\n\n    public moveTo(target: Actor): boolean {\n        if (this.position.level !== target.position.level) {\n            return false;\n        }\n\n        const distance = Math.floor(this.position.distanceBetween(target.position));\n        if (distance > 16) {\n            this.clearFaceActor();\n            return false;\n        }\n\n        this.pathfinding.walkTo(target.position, {\n            pathingSearchRadius: distance + 2,\n            ignoreDestination: true,\n        });\n\n        return true;\n    }\n\n    public follow(target: Actor): void {\n        this.face(target, false, false, false);\n        this.metadata.following = target;\n\n        this.moveBehind(target);\n        const subscription = target.walkingQueue.movementEvent.subscribe(() => {\n            if (!this.moveBehind(target)) {\n                // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now\n                this.actionsCancelled.next(null as unknown as ActionCancelType);\n            }\n        });\n\n        this.actionsCancelled\n            .pipe(\n                filter(type => type !== 'pathing-movement'),\n                take(1),\n            )\n            .subscribe(() => {\n                subscription.unsubscribe();\n                this.face(null);\n                delete this.metadata.following;\n            });\n    }\n\n    public walkTo(target: Actor): boolean;\n    public walkTo(position: Position): boolean;\n    public walkTo(target: Actor | Position): boolean {\n        const desiredPosition = target instanceof Position ? target : target.position;\n\n        const distance = Math.floor(this.position.distanceBetween(desiredPosition));\n\n        if (distance <= 1) {\n            return false;\n        }\n\n        if (distance > 16) {\n            this.clearFaceActor();\n            this.metadata.faceActorClearedByWalking = true;\n            return false;\n        }\n\n        this.pathfinding.walkTo(desiredPosition, {\n            pathingSearchRadius: distance + 2,\n            ignoreDestination: true,\n        });\n\n        return true;\n    }\n\n    public face(\n        face: Position | Actor | null,\n        clearWalkingQueue: boolean = true,\n        autoClear: boolean = true,\n        clearedByWalking: boolean = true,\n    ): void {\n        if (face === null) {\n            this.clearFaceActor();\n            this.updateFlags.facePosition = null;\n            return;\n        }\n\n        if (face instanceof Position) {\n            this.updateFlags.facePosition = face;\n        } else if (face instanceof Actor) {\n            this.updateFlags.faceActor = face;\n            this.metadata.faceActor = face;\n            this.metadata.faceActorClearedByWalking = clearedByWalking;\n\n            if (autoClear) {\n                setTimeout(() => {\n                    this.clearFaceActor();\n                }, 20000);\n            }\n        }\n\n        if (clearWalkingQueue) {\n            this.walkingQueue.clear();\n            this.walkingQueue.valid = false;\n        }\n    }\n\n    public clearFaceActor(): void {\n        if (this.metadata.faceActor) {\n            this.updateFlags.faceActor = 'CLEAR';\n            this.metadata.faceActor = undefined;\n        }\n    }\n\n    public playAnimation(animation: number | Animation | null): void {\n        if (typeof animation === 'number') {\n            animation = { id: animation, delay: 0 };\n        }\n\n        this.updateFlags.animation = animation;\n    }\n\n    public stopAnimation(): void {\n        this.updateFlags.animation = { id: -1, delay: 0 };\n    }\n\n    public playGraphics(graphics: number | Graphic): void {\n        if (typeof graphics === 'number') {\n            graphics = { id: graphics, delay: 0, height: 120 };\n        }\n\n        this.updateFlags.graphics = graphics;\n    }\n\n    public stopGraphics(): void {\n        this.updateFlags.graphics = { id: -1, delay: 0, height: 120 };\n    }\n\n    public removeItem(slot: number): void {\n        this.inventory.remove(slot);\n    }\n\n    public removeBankItem(slot: number): void {\n        this.bank.remove(slot);\n    }\n\n    public giveItem(item: number | Item): boolean {\n        return this.inventory.add(item) !== null;\n    }\n    public giveBankItem(item: number | Item): boolean {\n        return this.bank.add(item) !== null;\n    }\n\n    public hasItemInInventory(item: number | Item): boolean {\n        return this.inventory.has(item);\n    }\n    public hasItemInBank(item: number | Item): boolean {\n        return this.bank.has(item);\n    }\n\n    public hasItemOnPerson(item: number | Item): boolean {\n        return this.hasItemInInventory(item);\n    }\n\n    public canMove(): boolean {\n        // In the future, there will undoubtedly be various reasons for the\n        // actor to not be able to move, but for now we are returning true.\n        return true;\n    }\n\n    public initiateRandomMovement(): void {\n        this.enqueueBaseTask(\n            new QueueableTask(\n                [],\n                this,\n                () => {\n                    this.moveSomewhere();\n                    return {\n                        callbackResult: true,\n                        shouldContinueLooping: true,\n                    };\n                },\n                null,\n                null,\n            ),\n        );\n    }\n\n    public moveSomewhere(): void {\n        if (!this.canMove()) {\n            return;\n        }\n\n        if (isNpc(this)) {\n            const nearbyPlayers = activeWorld.findNearbyPlayers(this.position, 24, this.instance.instanceId);\n            if (nearbyPlayers.length === 0) {\n                // No need for this actor to move if there are no players nearby to witness it, save some memory. :)\n                return;\n            }\n        }\n\n        const movementChance = Math.floor(Math.random() * 10);\n\n        if (movementChance < 7) {\n            return;\n        }\n\n        let px = this.position.x;\n        let py = this.position.y;\n        let movementAllowed = false;\n\n        while (!movementAllowed) {\n            px = this.position.x;\n            py = this.position.y;\n\n            const moveXChance = Math.floor(Math.random() * 10);\n\n            if (moveXChance > 6) {\n                const moveXAmount = Math.floor(Math.random() * 5);\n                const moveXMod = Math.floor(Math.random() * 2);\n\n                if (moveXMod === 0) {\n                    px -= moveXAmount;\n                } else {\n                    px += moveXAmount;\n                }\n            }\n\n            const moveYChance = Math.floor(Math.random() * 10);\n\n            if (moveYChance > 6) {\n                const moveYAmount = Math.floor(Math.random() * 5);\n                const moveYMod = Math.floor(Math.random() * 2);\n\n                if (moveYMod === 0) {\n                    py -= moveYAmount;\n                } else {\n                    py += moveYAmount;\n                }\n            }\n\n            let valid = true;\n\n            if (!this.withinBounds(px, py)) {\n                valid = false;\n            }\n\n            movementAllowed = valid;\n        }\n\n        if (px !== this.position.x || py !== this.position.y) {\n            this.walkingQueue.clear();\n            this.walkingQueue.valid = true;\n            this.walkingQueue.add(px, py);\n        }\n    }\n\n    public forceMovement(direction: number, steps: number): void {\n        if (!this.canMove()) {\n            return;\n        }\n\n        let px = this.position.x;\n        let py = this.position.y;\n        let movementAllowed = false;\n\n        while (!movementAllowed) {\n            px = this.position.x;\n            py = this.position.y;\n\n            const movementDirection = directionFromIndex(direction);\n            if (!movementDirection) {\n                return;\n            }\n            let valid = true;\n            for (let step = 0; step < steps; step++) {\n                px += movementDirection.deltaX;\n                py += movementDirection.deltaY;\n\n                if (!this.withinBounds(px, py)) {\n                    valid = false;\n                }\n            }\n\n            movementAllowed = valid;\n        }\n\n        if (px !== this.position.x || py !== this.position.y) {\n            this.walkingQueue.clear();\n            this.walkingQueue.valid = true;\n            this.walkingQueue.add(px, py);\n        }\n    }\n\n    public withinBounds(x: number, y: number): boolean {\n        return true;\n    }\n\n    /**\n     * Initialise the actor.\n     */\n    protected init() {\n        this.active = true;\n    }\n\n    /**\n     * Destroy this actor.\n     *\n     * This will stop the processing of its action queue.\n     */\n    protected destroy() {\n        this.active = false;\n\n        this.scheduler.clear();\n    }\n\n    protected tick() {\n        this.scheduler.tick();\n    }\n\n    public get position(): Position {\n        return this._position;\n    }\n\n    public set position(value: Position) {\n        if (!this._position) {\n            this._lastMapRegionUpdatePosition = value;\n        }\n\n        this._position = value;\n    }\n\n    public get lastMapRegionUpdatePosition(): Position {\n        return this._lastMapRegionUpdatePosition;\n    }\n\n    public set lastMapRegionUpdatePosition(value: Position) {\n        this._lastMapRegionUpdatePosition = value;\n    }\n\n    public get worldIndex(): number {\n        return this._worldIndex;\n    }\n\n    public set worldIndex(value: number) {\n        this._worldIndex = value;\n    }\n\n    public get walkDirection(): number {\n        return this._walkDirection;\n    }\n\n    public set walkDirection(value: number) {\n        this._walkDirection = value;\n    }\n\n    public get runDirection(): number {\n        return this._runDirection;\n    }\n\n    public set runDirection(value: number) {\n        this._runDirection = value;\n    }\n\n    public get faceDirection(): number {\n        return this._faceDirection;\n    }\n\n    public set faceDirection(value: number) {\n        this._faceDirection = value;\n    }\n\n    public get busy(): boolean {\n        return this._busy;\n    }\n\n    public set busy(value: boolean) {\n        this._busy = value;\n    }\n\n    public get instance(): WorldInstance {\n        return this._instance || activeWorld.globalInstance;\n    }\n\n    public set instance(value: WorldInstance | null) {\n        this._instance = value;\n    }\n\n    public get bonuses(): { offensive: OffensiveBonuses; defensive: DefensiveBonuses; skill: SkillBonuses } {\n        return this._bonuses;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/combat.ts",
    "content": "import type { SkillName } from '@engine/world/actor/skills';\nimport * as combatStylesImport from '../../../../data/config/combat-styles.json';\n\nexport interface CombatStyles {\n    [key: string]: CombatStyle[];\n}\n\nexport interface CombatStyle {\n    type: 'slash' | 'stab' | 'crush';\n    exp: SkillName | SkillName[];\n    anim: string | string[];\n    button_id: number;\n}\n\nexport const combatStyles: CombatStyles = combatStylesImport as CombatStyles;\n"
  },
  {
    "path": "src/engine/world/actor/dialogue.ts",
    "content": "import { findNpc } from '@engine/config/config-handler';\nimport { wrapText } from '@engine/util/strings';\nimport type { Npc } from '@engine/world/actor/npc';\nimport { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\nimport type { ParentWidget, TextWidget } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\nimport _ from 'lodash';\n\nexport enum Emote {\n    POMPOUS = 'POMPOUS',\n    UNKOWN_CREATURE = 'UNKOWN_CREATURE',\n    VERY_SAD = 'VERY_SAD',\n    HAPPY = 'HAPPY',\n    SHOCKED = 'SHOCKED',\n    WONDERING = 'WONDERING',\n    GOBLIN = 'GOBLIN',\n    TREE = 'TREE',\n    GENERIC = 'GENERIC',\n    SKEPTICAL = 'SKEPTICAL',\n    WORRIED = 'WORRIED',\n    DROWZY = 'DROWZY',\n    LAUGH = 'LAUGH',\n    SAD = 'SAD',\n    ANGRY = 'ANGRY',\n    EASTER_BUNNY = 'EASTER_BUNNY',\n\n    BLANK_STARE = 'BLANK_STARE',\n    SINGLE_WORD = 'SINGLE_WORD',\n    EVIL_STARE = 'EVIL_STARE',\n    LAUGH_EVIL = 'LAUGH_EVIL',\n}\n\n// A big thanks to Dust R I P for all these emotes!\nenum EmoteAnimation {\n    POMPOUS_1LINE = 554,\n    POMPOUS_2LINE = 555,\n    POMPOUS_3LINE = 556,\n    POMPOUS_4LINE = 557,\n    UNKOWN_CREATURE_1LINE = 558,\n    UNKOWN_CREATURE_2LINE = 559,\n    UNKOWN_CREATURE_3LINE = 560,\n    UNKOWN_CREATURE_4LINE = 561,\n    VERY_SAD1LINE = 562,\n    VERY_SAD2LINE = 563,\n    VERY_SAD3LINE = 564,\n    VERY_SAD4LINE = 565,\n    SINGLE_WORD = 566,\n    HAPPY_1LINE = 567,\n    HAPPY_2LINE = 568,\n    HAPPY_3LINE = 569,\n    HAPPY_4LINE = 570,\n    SHOCKED_1LINE = 571,\n    SHOCKED_2LINE = 572,\n    SHOCKED_3LINE = 573,\n    SHOCKED_4LINE = 574,\n    WONDERING_1LINE = 575,\n    WONDERING_2LINE = 576,\n    WONDERING_3LINE = 577,\n    WONDERING_4LINE = 578,\n    BLANK_STARE = 579,\n    GOBLIN_1LINE = 580,\n    GOBLIN_2LINE = 581,\n    GOBLIN_3LINE = 582,\n    GOBLIN_4LINE = 583,\n    TREE_1LINE = 584,\n    TREE_2LINE = 585,\n    TREE_3LINE = 586,\n    TREE_4LINE = 587,\n    GENERIC_1LINE = 588,\n    GENERIC_2LINE = 589,\n    GENERIC_3LINE = 590,\n    GENERIC_4LINE = 591,\n    SKEPTICAL_1LINE = 592,\n    SKEPTICAL_2LINE = 593,\n    SKEPTICAL_3LINE = 594,\n    SKEPTICAL_4LINE = 595,\n    WORRIED_1LINE = 596,\n    WORRIED_2LINE = 597,\n    WORRIED_3LINE = 598,\n    WORRIED_4LINE = 599,\n    DROWZY_1LINE = 600,\n    DROWZY_2LINE = 601,\n    DROWZY_3LINE = 602,\n    DROWZY_4LINE = 603,\n    EVIL_STARE = 604,\n    LAUGH_1LINE = 605,\n    LAUGH_2LINE = 606,\n    LAUGH_3LINE = 607,\n    LAUGH_4LINE = 608,\n    LAUGH_EVIL = 609,\n    SAD_1LINE = 610,\n    SAD_2LINE = 611,\n    SAD_3LINE = 612,\n    SAD_4LINE = 613,\n    ANGRY_1LINE = 614,\n    ANGRY_2LINE = 615,\n    ANGRY_3LINE = 616,\n    ANGRY_4LINE = 617,\n    EASTER_BUNNY_1LINE = 1824,\n    EASTER_BUNNY_2LINE = 1825,\n    EASTER_BUNNY_3LINE = 1826,\n    EASTER_BUNNY_4LINE = 1827,\n}\n\nconst nonLineEmotes = [Emote.BLANK_STARE, Emote.SINGLE_WORD, Emote.EVIL_STARE, Emote.LAUGH_EVIL];\nconst playerWidgetIds = [64, 65, 66, 67];\nconst npcWidgetIds = [241, 242, 243, 244];\nconst optionWidgetIds = [228, 230, 232, 234, 235];\nconst continuableTextWidgetIds = [210, 211, 212, 213, 214];\nconst textWidgetIds = [215, 216, 217, 218, 219];\nconst titledTextWidgetId = 372;\n\n/**\n * Wraps dialogue text into multiple lines.\n * @param text - The text to wrap.\n * @param type - 'ACTOR' if the widget has a chat-head or an item sprite on the left, 'TEXT' if the dialogue is text only\n */\nfunction wrapDialogueText(text: string, type: 'ACTOR' | 'TEXT'): string[] {\n    let widget: TextWidget;\n    let width = 0;\n\n    switch (type) {\n        case 'ACTOR':\n            widget = (filestore.widgetStore.decodeWidget(playerWidgetIds[0]) as ParentWidget).children[2] as TextWidget;\n            width = widget.width;\n            break;\n        case 'TEXT':\n            widget = filestore.widgetStore.decodeWidget(textWidgetIds[0]) as TextWidget;\n            width = widget.width;\n            break;\n        default:\n            throw new Error(`Unhandled widget type: ${type}`);\n    }\n\n    return wrapText(text, width, widget.fontId);\n}\n\nfunction parseDialogueFunctionArgs(func: Function): string[] | null {\n    const str = func.toString();\n\n    if (!str) {\n        return null;\n    }\n\n    const argEndIndex = str.indexOf('=>');\n\n    if (argEndIndex === -1) {\n        return null;\n    }\n\n    const arg = str\n        .substring(0, argEndIndex)\n        .replace(/[\\\\(\\\\) ]/g, '')\n        .trim();\n    if (!arg || arg.length === 0) {\n        return null;\n    }\n\n    return arg.split(',');\n}\n\nexport type DialogueTree = (Function | DialogueFunction | GoToAction)[];\n\nexport interface AdditionalOptions {\n    closeOnWalk?: boolean;\n    permanent?: boolean;\n    title?: string;\n}\n\ninterface NpcParticipant {\n    npc: Npc | number | string;\n    key: string;\n}\n\nclass DialogueFunction {\n    constructor(\n        public type: string,\n        public execute: Function,\n    ) {}\n}\n\nexport const execute = (execute: Function): DialogueFunction => new DialogueFunction('execute', execute);\nexport const goto = (to: string | Function): GoToAction => new GoToAction(to);\n\ntype ParsedDialogueTree = (DialogueAction | DialogueFunction | string)[];\n\ninterface DialogueAction {\n    tag: string;\n    type: string;\n}\n\nclass GoToAction implements DialogueAction {\n    public tag: string;\n    public type = 'GOTO';\n\n    constructor(public to: string | Function) {}\n}\n\ninterface ActorDialogueAction extends DialogueAction {\n    animation: number;\n    lines: string[];\n}\n\ninterface NpcDialogueAction extends ActorDialogueAction {\n    npcId: number;\n}\n\ninterface PlayerDialogueAction extends ActorDialogueAction {\n    player: Player;\n}\n\ninterface TextDialogueAction extends DialogueAction {\n    lines: string[];\n    canContinue: boolean;\n}\n\ninterface TitledTextDialogueAction extends DialogueAction {\n    title: string;\n    lines: string[];\n}\n\ninterface OptionsDialogueAction extends DialogueAction {\n    options: { [key: string]: ParsedDialogueTree };\n}\n\ninterface SubDialogueTreeAction extends DialogueAction {\n    subTree: DialogueTree;\n    npcParticipants?: NpcParticipant[];\n}\n\nfunction parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], dialogueTree: DialogueTree): ParsedDialogueTree {\n    const parsedDialogueTree: ParsedDialogueTree = [];\n\n    let carryoverDialogue: string[] = [];\n    for (let i = 0; i < dialogueTree.length; i++) {\n        const dialogueAction = dialogueTree[i];\n\n        if (dialogueAction instanceof DialogueFunction) {\n            // Code execution dialogue.\n            parsedDialogueTree.push(dialogueAction as DialogueFunction);\n            continue;\n        }\n\n        if (dialogueAction instanceof GoToAction) {\n            parsedDialogueTree.push(dialogueAction);\n            continue;\n        }\n\n        let args = parseDialogueFunctionArgs(dialogueAction);\n        if (args === null) {\n            args = ['()'];\n        }\n\n        const dialogueType = args[0];\n        let tag: string | null = null;\n\n        if (args.length === 2 && typeof args[1] === 'string') {\n            if (player.metadata.dialogueIndices) {\n                player.metadata.dialogueIndices[args[1]] = i;\n            } else {\n                logger.warn('Player metadata does not contain dialogueIndices');\n            }\n            tag = args[1];\n        }\n\n        if (!dialogueType) {\n            logger.error('No arguments passed to dialogue function.');\n            continue;\n        }\n\n        let isOptions = false;\n\n        if (dialogueType === 'options' || dialogueType === '()') {\n            // Options or custom function dialogue.\n\n            let result = dialogueAction();\n\n            if (dialogueType === '()') {\n                const funcResult = result();\n\n                if (!Array.isArray(funcResult) || funcResult.length === 0) {\n                    logger.error('Invalid dialogue function response type.');\n                    continue;\n                }\n\n                if (typeof funcResult[0] === 'function') {\n                    // given function returned a dialogue tree\n                    parsedDialogueTree.push(...parseDialogueTree(player, npcParticipants, funcResult));\n                } else {\n                    // given function returned an option list\n                    result = funcResult;\n                    isOptions = true;\n                }\n            } else {\n                isOptions = true;\n            }\n\n            if (isOptions) {\n                const options = (result as any[]).filter((option, index) => index % 2 === 0);\n                const trees = (result as any[]).filter((option, index) => index % 2 !== 0);\n                const optionsDialogueAction: OptionsDialogueAction = {\n                    options: {},\n                    tag: tag || '',\n                    type: 'OPTIONS',\n                };\n\n                if (!tag) {\n                    logger.warn('No tag provided for options dialogue.');\n                }\n\n                for (let j = 0; j < options.length; j++) {\n                    const option = options[j];\n                    const tree = parseDialogueTree(player, npcParticipants, trees[j]);\n                    optionsDialogueAction.options[option] = tree;\n                }\n\n                parsedDialogueTree.push(optionsDialogueAction);\n            }\n        } else if (dialogueType === 'text') {\n            // Text-only dialogue (with the option to click continue).\n\n            const text: string = dialogueAction();\n            const lines = wrapDialogueText(text, 'TEXT');\n            parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: true } as TextDialogueAction);\n        } else if (dialogueType === 'overlay') {\n            // Text-only dialogue (no option to continue).\n\n            const text: string = dialogueAction();\n            const lines = wrapDialogueText(text, 'TEXT');\n            parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: false } as TextDialogueAction);\n        } else if (dialogueType === 'titled') {\n            // Text-only dialogue (no option to continue).\n\n            const [title, text] = dialogueAction();\n            const lines = wrapDialogueText(text, 'TEXT');\n\n            while (lines.length < 4) {\n                lines.push('');\n            }\n\n            parsedDialogueTree.push({ lines, title, tag, type: 'TITLED' } as TitledTextDialogueAction);\n        } else if (dialogueType === 'subtree') {\n            // Dialogue sub-tree.\n\n            const subTree: DialogueTree = dialogueAction();\n            parsedDialogueTree.push({ tag, type: 'SUBTREE', subTree, npcParticipants } as SubDialogueTreeAction);\n        } else {\n            // Player or Npc dialogue.\n\n            let dialogueDetails: [Emote, string];\n            let npc: Npc | number | string = -1;\n\n            if (dialogueType !== 'player') {\n                const participant = npcParticipants.find(p => p.key === dialogueType) as NpcParticipant;\n                if (!participant || !participant.npc) {\n                    logger.error('No matching npc found for npc dialogue action.');\n                    continue;\n                }\n\n                npc = participant.npc;\n                if (typeof npc !== 'number') {\n                    if (typeof npc === 'string') {\n                        npc = findNpc(npc).gameId;\n                    } else {\n                        npc = npc.id;\n                    }\n                }\n\n                dialogueDetails = dialogueAction(npc);\n\n                if (npc === -1) {\n                    throw new Error('No npc found for dialogue action.');\n                }\n            } else {\n                dialogueDetails = dialogueAction(player);\n            }\n\n            const emote = dialogueDetails[0] as Emote;\n            const text = (carryoverDialogue.join(' ') + dialogueDetails[1]) as string;\n            carryoverDialogue = [];\n\n            let lines = wrapDialogueText(text, 'ACTOR');\n            // logger.info('length = ' + lines.length + ' - lines equals this: ' + lines);\n\n            const animation = nonLineEmotes.indexOf(emote) !== -1 ? EmoteAnimation[emote] : EmoteAnimation[`${emote}_${lines.length}LINE`];\n\n            if (!tag) {\n                logger.warn('No tag provided for npc dialogue.');\n            }\n\n            if (dialogueType !== 'player') {\n                if (lines.length > 4) {\n                    while (lines.length > 4) {\n                        const copyOfLines = lines.slice(0, lines.length);\n                        lines = lines.slice(0, 4);\n\n                        const npcDialogueAction: NpcDialogueAction = {\n                            npcId: npc as number,\n                            animation,\n                            lines,\n                            tag: tag || '',\n                            type: 'NPC',\n                        };\n\n                        parsedDialogueTree.push(npcDialogueAction);\n\n                        lines = copyOfLines.slice(0, copyOfLines.length);\n                        carryoverDialogue = lines.slice(4, lines.length) as string[];\n                        lines = carryoverDialogue;\n\n                        if (i === dialogueTree.length - 1 && lines.length <= 4) {\n                            const npcDialogueAction: NpcDialogueAction = {\n                                npcId: npc as number,\n                                animation,\n                                lines,\n                                tag: tag || '',\n                                type: 'NPC',\n                            };\n\n                            parsedDialogueTree.push(npcDialogueAction);\n                        }\n                    }\n                } else {\n                    const npcDialogueAction: NpcDialogueAction = {\n                        npcId: npc as number,\n                        animation,\n                        lines,\n                        tag: tag || '',\n                        type: 'NPC',\n                    };\n\n                    parsedDialogueTree.push(npcDialogueAction);\n                }\n            } else {\n                const playerDialogueAction: PlayerDialogueAction = {\n                    player,\n                    animation,\n                    lines,\n                    tag: tag || '',\n                    type: 'PLAYER',\n                };\n\n                parsedDialogueTree.push(playerDialogueAction);\n            }\n        }\n    }\n\n    return parsedDialogueTree;\n}\n\nasync function runDialogueAction(\n    player: Player,\n    dialogueAction: string | DialogueFunction | DialogueAction,\n    tag?: string | undefined | false,\n    additionalOptions?: AdditionalOptions,\n): Promise<string | undefined | false> {\n    if (dialogueAction instanceof DialogueFunction && !tag) {\n        // Code execution dialogue.\n        dialogueAction.execute();\n        return tag;\n    }\n\n    dialogueAction = dialogueAction as DialogueAction;\n\n    if (dialogueAction.type === 'GOTO' && !tag) {\n        // Goto dialogue.\n        const goToAction = dialogueAction as GoToAction;\n        if (typeof goToAction.to === 'function') {\n            const goto: string = goToAction.to();\n            await runParsedDialogue(player, player.metadata.dialogueTree, goto, additionalOptions);\n        } else {\n            await runParsedDialogue(player, player.metadata.dialogueTree, goToAction.to, additionalOptions);\n        }\n        return tag;\n    }\n\n    let widgetId: number = -1;\n    let isOptions = false;\n\n    if (dialogueAction.type === 'OPTIONS') {\n        // Option dialogue.\n        const optionsAction = dialogueAction as OptionsDialogueAction;\n        isOptions = true;\n        const options = Object.keys(optionsAction.options);\n        const trees = options.map(option => optionsAction.options[option]);\n        if (tag === undefined || dialogueAction.tag === tag) {\n            tag = undefined;\n\n            widgetId = optionWidgetIds[options.length - 2];\n\n            for (let i = 0; i < options.length; i++) {\n                player.outgoingPackets.updateWidgetString(widgetId, 1 + i, options[i]);\n            }\n        } else if (tag !== undefined) {\n            for (let i = 0; i < options.length; i++) {\n                const tree = trees[i];\n                const didRun = await runParsedDialogue(player, tree, tag, additionalOptions);\n                if (didRun) {\n                    return;\n                }\n            }\n        }\n    } else if (dialogueAction.type === 'TEXT') {\n        // Text-only dialogue.\n\n        if (tag === undefined || dialogueAction.tag === tag) {\n            tag = undefined;\n\n            const textDialogueAction = dialogueAction as TextDialogueAction;\n            const lines = textDialogueAction.lines;\n\n            if (lines.length > 5) {\n                throw new Error(\n                    `Too many lines for text dialogue! Dialogue has ${lines.length} lines but ` +\n                        `the maximum is 5: ${JSON.stringify(lines)}`,\n                );\n            }\n\n            widgetId = (textDialogueAction.canContinue ? continuableTextWidgetIds : textWidgetIds)[lines.length - 1];\n\n            for (let i = 0; i < lines.length; i++) {\n                player.outgoingPackets.updateWidgetString(widgetId, i, lines[i]);\n            }\n        }\n    } else if (dialogueAction.type === 'TITLED') {\n        // Text-only dialogue.\n\n        if (tag === undefined || dialogueAction.tag === tag) {\n            tag = undefined;\n\n            const titledDialogueAction = dialogueAction as TitledTextDialogueAction;\n            const { title, lines } = titledDialogueAction;\n\n            if (lines.length > 4) {\n                throw new Error(\n                    `Too many lines for titled dialogue! Dialogue has ${lines.length} lines but ` +\n                        `the maximum is 4: ${JSON.stringify(lines)}`,\n                );\n            }\n\n            widgetId = titledTextWidgetId;\n\n            player.outgoingPackets.updateWidgetString(widgetId, 0, title);\n\n            for (let i = 0; i < lines.length; i++) {\n                player.outgoingPackets.updateWidgetString(widgetId, i + 1, lines[i]);\n            }\n        }\n    } else if (dialogueAction.type === 'SUBTREE') {\n        // Dialogue sub-tree.\n        const action = dialogueAction as SubDialogueTreeAction;\n\n        if (!action.npcParticipants) {\n            // (Jameskmonger) I added this log because the TypeScript types allow for this to be undefined, but\n            //             parseDialogueTree requires it. I'm not sure if it can be undefined in practice.\n            logger.warn('No NPC participants for dialogue action');\n        }\n        // (Jameskmonger) default value added here\n        const npcParticipants = action.npcParticipants || [];\n\n        if (dialogueAction.tag === tag) {\n            const originalIndices = _.cloneDeep(player.metadata.dialogueIndices || {});\n            const originalTree = _.cloneDeep(player.metadata.dialogueTree || []);\n            player.metadata.dialogueIndices = {};\n            const parsedSubTree = parseDialogueTree(player, npcParticipants, action.subTree);\n            player.metadata.dialogueTree = parsedSubTree;\n\n            await runParsedDialogue(player, parsedSubTree, undefined, additionalOptions);\n\n            player.metadata.dialogueIndices = originalIndices;\n            player.metadata.dialogueTree = originalTree;\n        } else if (tag && dialogueAction.tag !== tag) {\n            const originalIndices = _.cloneDeep(player.metadata.dialogueIndices || {});\n            const originalTree = _.cloneDeep(player.metadata.dialogueTree || []);\n            player.metadata.dialogueIndices = {};\n            const parsedSubTree = parseDialogueTree(player, npcParticipants, action.subTree);\n            player.metadata.dialogueTree = parsedSubTree;\n\n            await runParsedDialogue(player, parsedSubTree, tag, additionalOptions);\n\n            player.metadata.dialogueIndices = originalIndices;\n            player.metadata.dialogueTree = originalTree;\n        }\n    } else {\n        // Player or Npc dialogue.\n\n        if (tag === undefined || dialogueAction.tag === tag) {\n            tag = undefined;\n\n            let npcId: number = -1;\n\n            if (dialogueAction.type === 'NPC') {\n                npcId = (dialogueAction as NpcDialogueAction).npcId;\n\n                if (npcId === -1) {\n                    throw new Error('No npc found for dialogue action.');\n                }\n            }\n\n            const actorDialogueAction = dialogueAction as ActorDialogueAction;\n            const lines = actorDialogueAction.lines;\n\n            if (lines.length > 4) {\n                throw new Error(\n                    `Too many lines for actor dialogue! Dialogue has ${lines.length} lines but ` +\n                        `the maximum is 4: ${JSON.stringify(lines)}`,\n                );\n            }\n\n            const animation = actorDialogueAction.animation;\n\n            if (dialogueAction.type === 'NPC') {\n                widgetId = npcWidgetIds[lines.length - 1];\n                player.outgoingPackets.setWidgetNpcHead(widgetId, 0, npcId);\n\n                const npcDetails = filestore.configStore.npcStore.getNpc(npcId);\n\n                if (npcDetails && npcDetails.name) {\n                    player.outgoingPackets.updateWidgetString(widgetId, 1, npcDetails.name);\n                }\n            } else {\n                widgetId = playerWidgetIds[lines.length - 1];\n                player.outgoingPackets.setWidgetPlayerHead(widgetId, 0);\n                player.outgoingPackets.updateWidgetString(widgetId, 1, player.username);\n            }\n            player.outgoingPackets.playWidgetAnimation(widgetId, 0, animation);\n\n            for (let i = 0; i < lines.length; i++) {\n                player.outgoingPackets.updateWidgetString(widgetId, 2 + i, lines[i]);\n            }\n        }\n    }\n\n    if (tag === undefined && widgetId !== -1) {\n        const permanent = additionalOptions?.permanent || false;\n\n        if (permanent) {\n            player.interfaceState.openChatOverlayWidget(widgetId);\n        } else {\n            player.interfaceState.openWidget(widgetId, {\n                slot: 'chatbox',\n                multi: false,\n            });\n\n            const widgetClosedEvent = await player.interfaceState.widgetClosed('chatbox');\n            if (widgetClosedEvent.data !== undefined) {\n                if (isOptions && typeof widgetClosedEvent.data === 'number') {\n                    const optionsAction = dialogueAction as OptionsDialogueAction;\n                    const options = Object.keys(optionsAction.options);\n                    const trees = options.map(option => optionsAction.options[option]);\n                    const tree: ParsedDialogueTree = trees[widgetClosedEvent.data - 1];\n                    if (tree && tree.length !== 0) {\n                        await runParsedDialogue(player, tree, tag, additionalOptions);\n                    }\n                }\n            } else {\n                return false;\n            }\n        }\n    }\n    return tag;\n}\n\nasync function runParsedDialogue(\n    player: Player,\n    dialogueTree: ParsedDialogueTree,\n    tag?: string | undefined | false,\n    additionalOptions?: AdditionalOptions,\n): Promise<boolean> {\n    for (let i = 0; i < dialogueTree.length; i++) {\n        tag = await runDialogueAction(player, dialogueTree[i], tag, additionalOptions);\n        if (tag === false) {\n            break;\n        }\n    }\n\n    return tag === undefined;\n}\n\nexport async function dialogue(\n    participants: (Player | NpcParticipant)[],\n    dialogueTree: DialogueTree,\n    additionalOptions?: AdditionalOptions,\n): Promise<boolean> {\n    const player: Player | undefined = participants.find(p => p instanceof Player);\n\n    if (!player) {\n        throw new Error('Player instance not provided to dialogue action.');\n    }\n\n    let npcParticipants = participants.filter(p => !(p instanceof Player)) as NpcParticipant[];\n    if (!npcParticipants) {\n        npcParticipants = [];\n    }\n\n    player.metadata.dialogueIndices = {};\n    const parsedDialogueTree = parseDialogueTree(player, npcParticipants, dialogueTree);\n    player.metadata.dialogueTree = parsedDialogueTree;\n\n    try {\n        await runParsedDialogue(player, parsedDialogueTree, undefined, additionalOptions);\n        player.interfaceState.closeAllSlots();\n        return true;\n    } catch (error) {\n        player.interfaceState.closeAllSlots();\n        logger.warn(error);\n        return false;\n    }\n}\n\nconst itemSelectionDialogueAmounts = [1, 5, 'X', 'All'];\nconst itemSelectionDialogues = {\n    // 303-306 - what would you like to make?\n    303: {\n        items: [2, 3],\n        text: [7, 11],\n        options: [\n            [7, 6, 5, 4],\n            [11, 10, 9, 8],\n        ],\n    },\n    304: {\n        items: [2, 3, 4],\n        text: [8, 12, 16],\n        options: [\n            [8, 7, 6, 5],\n            [12, 11, 10, 9],\n            [16, 15, 14, 13],\n        ],\n    },\n    305: {\n        items: [2, 3, 4, 5],\n        text: [9, 13, 17, 21],\n        options: [\n            [9, 8, 7, 6],\n            [13, 12, 11, 10],\n            [17, 16, 15, 14],\n            [21, 20, 19, 18],\n        ],\n    },\n    306: {\n        items: [2, 3, 4, 5, 6],\n        text: [10, 14, 18, 22, 26],\n        options: [\n            [10, 9, 8, 7],\n            [14, 13, 12, 11],\n            [18, 17, 16, 15],\n            [22, 21, 20, 19],\n            [26, 25, 24, 23],\n        ],\n    },\n    307: {\n        // 307 - how many would you like to cook?\n        items: [2],\n        text: [6],\n        options: [[6, 5, 4, 3]],\n    },\n    309: {\n        // 309 - how many would you like to make?\n        items: [2],\n        text: [6],\n        options: [[6, 5, 4, 3]],\n    },\n};\n\nexport interface SelectableItem {\n    itemId: number;\n    itemName: string;\n    offset?: number;\n    zoom?: number;\n}\n\nexport interface ItemSelection {\n    itemId: number;\n    amount: number;\n}\n\nexport async function itemSelectionDialogue(player: Player, type: 'COOKING' | 'MAKING', items: SelectableItem[]): Promise<ItemSelection> {\n    let widgetId = 307;\n\n    if (type === 'MAKING') {\n        if (items.length === 1) {\n            widgetId = 309;\n        } else {\n            if (items.length > 5) {\n                throw new Error(`Too many items provided to the item selection action!`);\n            }\n\n            widgetId = 301 + items.length;\n        }\n    }\n\n    const childIds = itemSelectionDialogues[widgetId].items;\n    childIds.forEach((childId, index) => {\n        const itemInfo = items[index];\n\n        if (itemInfo.offset === undefined) {\n            itemInfo.offset = -12;\n        }\n\n        if (itemInfo.zoom === undefined) {\n            itemInfo.zoom = 180;\n        }\n\n        player.outgoingPackets.setItemOnWidget(widgetId, childId, itemInfo.itemId, itemInfo.zoom);\n        player.outgoingPackets.moveWidgetChild(widgetId, childId, 0, itemInfo.offset);\n        player.outgoingPackets.updateWidgetString(\n            widgetId,\n            itemSelectionDialogues[widgetId].text[index],\n            '\\\\n\\\\n\\\\n\\\\n' + itemInfo.itemName,\n        );\n    });\n\n    return new Promise((resolve, reject) => {\n        player.interfaceState.openWidget(widgetId, {\n            slot: 'chatbox',\n            multi: true,\n        });\n\n        let actionsSub = player.actionsCancelled.subscribe(() => {\n            actionsSub.unsubscribe();\n            reject('Pending Actions Cancelled');\n        });\n\n        const interactionSub = player.dialogueInteractionEvent.subscribe(childId => {\n            if (!player.interfaceState.widgetOpen('chatbox', widgetId)) {\n                interactionSub.unsubscribe();\n                actionsSub.unsubscribe();\n                reject('Active Widget Mismatch');\n                return;\n            }\n\n            const options = itemSelectionDialogues[widgetId].options;\n\n            const choiceIndex = options.findIndex(arr => arr.indexOf(childId) !== -1);\n\n            if (choiceIndex === -1) {\n                interactionSub.unsubscribe();\n                actionsSub.unsubscribe();\n                reject('Choice Index Not Found');\n                return;\n            }\n\n            const optionIndex = options[choiceIndex].indexOf(childId);\n\n            if (optionIndex === -1) {\n                interactionSub.unsubscribe();\n                actionsSub.unsubscribe();\n                reject('Option Index Not Found');\n                return;\n            }\n\n            const itemId = items[choiceIndex].itemId;\n            let amount = itemSelectionDialogueAmounts[optionIndex];\n\n            if (amount === 'X') {\n                actionsSub.unsubscribe();\n\n                player.outgoingPackets.showNumberInputDialogue();\n\n                actionsSub = player.actionsCancelled.subscribe(() => {\n                    actionsSub.unsubscribe();\n                    reject('Pending Actions Cancelled');\n                });\n\n                const inputSub = player.numericInputEvent.subscribe(input => {\n                    inputSub.unsubscribe();\n                    actionsSub.unsubscribe();\n                    interactionSub.unsubscribe();\n\n                    if (input < 1 || input > 2147483647) {\n                        player.interfaceState.closeWidget('chatbox');\n                        reject('Invalid User Amount Input');\n                    } else {\n                        player.interfaceState.closeWidget('chatbox');\n                        resolve({ itemId, amount: input } as ItemSelection);\n                    }\n                });\n            } else {\n                if (amount === 'All') {\n                    amount = player.inventory.findAll(itemId).length;\n                }\n\n                actionsSub.unsubscribe();\n                interactionSub.unsubscribe();\n                player.interfaceState.closeWidget('chatbox');\n                resolve({ itemId, amount } as ItemSelection);\n            }\n        });\n    });\n}\n"
  },
  {
    "path": "src/engine/world/actor/magic.ts",
    "content": "export interface Magic {\n    Name: string;\n    ButtonID: number;\n    CoolDown: number;\n    BaseDamage: number;\n    EffectID: number;\n    DamageCalculation(): number;\n}\nexport abstract class Magic {}\n"
  },
  {
    "path": "src/engine/world/actor/metadata.ts",
    "content": "import type { ConstructedRegion } from '@engine/world/map/region';\nimport type { Position } from '../position';\nimport type { Actor } from './actor';\n\n/**\n * The definition of the metadata available on an {@link Actor}.\n *\n * You cannot guarantee that all of these properties will be present on an actor,\n * so you should always check for their existence before using them.\n *\n * @author jameskmonger\n */\nexport type ActorMetadata = {\n    /**\n     * The custom constructed map region for this actor.\n     *\n     * TODO (jameskmonger) Should this live on Actor rather than on {@link Player}? I don't think NPCs can have a custom map.\n     */\n    customMap: ConstructedRegion;\n\n    /**\n     * The actor currently being followed by this actor.\n     */\n    following: Actor;\n\n    /**\n     * The actor which the local actor is facing towards.\n     */\n    faceActor: Actor;\n\n    /**\n     * Whether a walk action has cleared the actor which the local actor is facing towards.\n     *\n     * TODO (jameskmonger) does this belong on this metadata?\n     */\n    faceActorClearedByWalking: boolean;\n\n    /**\n     * The actor's last position before teleporting.\n     */\n    lastPosition: Position;\n\n    /**\n     * Set to true if the actor is currently teleporting.\n     */\n    teleporting: boolean;\n};\n"
  },
  {
    "path": "src/engine/world/actor/npc.ts",
    "content": "import EventEmitter from 'events';\nimport { findItem, findNpc } from '@engine/config/config-handler';\nimport type { NpcCombatAnimations, NpcDetails } from '@engine/config/npc-config';\nimport type { NpcSpawn } from '@engine/config/npc-spawn-config';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { isPlayer } from '@engine/world/actor/util';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { directionData } from '@engine/world/direction';\nimport type { WorldInstance } from '@engine/world/instances';\nimport type { Position } from '@engine/world/position';\nimport type { QuadtreeKey } from '@engine/world/world';\nimport { logger } from '@runejs/common';\nimport { filestore } from '@server/game/game-server';\nimport { v4 } from 'uuid';\nimport { Actor } from './actor';\nimport type { SkillName } from './skills';\n\n/**\n * Represents a non-player character within the game world.\n */\nexport class Npc extends Actor {\n    public readonly uuid: string;\n    public readonly options: string[];\n    public readonly initialPosition: Position;\n    public readonly key: string;\n    public readonly varbitId: number = -1;\n    public readonly settingId: number = -1;\n    public readonly childrenIds?: number[];\n    public parent?: Npc;\n    public id: number;\n    public animations: NpcCombatAnimations & {\n        walk?: number;\n        turnAround?: number;\n        turnLeft?: number;\n        turnRight?: number;\n        stand?: number;\n    };\n    //ToDo: this should either be calculated by the level or from a config\n    public experienceValue: number = 10;\n    public npcEvents: EventEmitter = new EventEmitter();\n\n    private _name: string;\n    private _combatLevel: number;\n    private _movementRadius: number = 0;\n    private quadtreeKey: QuadtreeKey | null = null;\n    private _exists: boolean = true;\n    private npcSpawn: NpcSpawn;\n    private _initialized: boolean = false;\n\n    public constructor(npcDetails: NpcDetails | number, npcSpawn: NpcSpawn, instance: WorldInstance | null = null) {\n        super('npc');\n\n        this.key = npcSpawn.npcKey;\n        this.uuid = v4();\n        this.position = npcSpawn.spawnPosition.clone();\n        this.initialPosition = this.position.clone();\n        this.npcSpawn = npcSpawn;\n\n        if (instance) {\n            this.instance = instance;\n        }\n\n        if (npcSpawn.movementRadius) {\n            this._movementRadius = npcSpawn.movementRadius;\n        }\n\n        if (npcSpawn.faceDirection) {\n            this.faceDirection = directionData[npcSpawn.faceDirection].index;\n        }\n\n        if (typeof npcDetails === 'number') {\n            this.id = npcDetails;\n        } else {\n            this.id = npcDetails.gameId;\n            this._combatLevel = npcDetails.combatLevel;\n            this.animations = npcDetails.combatAnimations || {};\n            this.options = npcDetails.options || [];\n\n            if (npcDetails.skills) {\n                const skillNames = Object.keys(npcDetails.skills);\n                skillNames.forEach(skillName => this.skills.setLevel(skillName as SkillName, npcDetails.skills?.[skillName] ?? 1));\n            }\n        }\n\n        const cacheDetails = filestore.configStore.npcStore.getNpc(this.id);\n        if (cacheDetails) {\n            // NPC not registered on the server, but exists in the game cache - use that for our info and assume it's\n            // Not a combatant NPC since we have no useful combat information for it.\n\n            this._name = cacheDetails.name || '';\n            this._combatLevel = cacheDetails.combatLevel;\n            this.options = cacheDetails.options || [];\n            this.varbitId = cacheDetails.varbitId;\n            this.settingId = cacheDetails.settingId;\n            this.childrenIds = cacheDetails.childrenIds;\n            this.animations = {\n                walk: cacheDetails.animations?.walk || undefined,\n                turnAround: cacheDetails.animations?.turnAround || undefined,\n                turnLeft: cacheDetails.animations?.turnLeft || undefined,\n                turnRight: cacheDetails.animations?.turnRight || undefined,\n                stand: cacheDetails.animations?.stand || undefined,\n            };\n        } else {\n            this._name = 'Unknown';\n        }\n\n        this.npcEvents.on('death', this.processDeath);\n    }\n\n    public async init(): Promise<void> {\n        super.init();\n\n        activeWorld.chunkManager.getChunkForWorldPosition(this.position).addNpc(this);\n\n        if (this.movementRadius > 0) {\n            this.initiateRandomMovement();\n        }\n\n        await this.actionPipeline.call('npc_init', { npc: this });\n\n        this._initialized = true;\n    }\n\n    //This is useful so that we can tie into things like \"spell casts\" or events, or traps, etc to finish quests or whatever\n    public async processDeath(assailant: Actor, defender: Actor): Promise<void> {\n        return new Promise<void>(resolve => {\n            const deathPosition = defender.position;\n\n            let deathAnim: number = animationIds.death;\n            deathAnim = findNpc((defender as Npc).id).combatAnimations?.death || animationIds.death;\n\n            defender.playAnimation(deathAnim);\n            activeWorld.playLocationSound(deathPosition, defender.instance.instanceId, soundIds.npc.human.maleDeath, 5);\n            const npcDetails = findNpc((defender as Npc).id);\n\n            if (!npcDetails.dropTable) {\n                return;\n            }\n\n            if (isPlayer(assailant)) {\n                const itemDrops = calculateNpcDrops(assailant, npcDetails);\n                itemDrops.forEach(drop => {\n                    const droppedItem = findItem(drop.itemKey);\n\n                    if (!droppedItem) {\n                        logger.error(`Unable to find item with key: ${drop.itemKey}`);\n                        return;\n                    }\n\n                    if (!drop.amount) {\n                        logger.error(`Unable to drop item with key: ${drop.itemKey} - no amount specified`);\n                        return;\n                    }\n\n                    activeWorld.globalInstance.spawnWorldItem({ itemId: droppedItem.gameId, amount: drop.amount }, deathPosition, {\n                        owner: assailant,\n                        expires: 300,\n                    });\n                });\n            }\n        });\n    }\n\n    public withinBounds(x: number, y: number): boolean {\n        return !(\n            x > this.initialPosition.x + this.movementRadius ||\n            x < this.initialPosition.x - this.movementRadius ||\n            y > this.initialPosition.y + this.movementRadius ||\n            y < this.initialPosition.y - this.movementRadius\n        );\n    }\n\n    public kill(respawn: boolean = true): void {\n        this.destroy();\n\n        activeWorld.chunkManager.getChunkForWorldPosition(this.position).removeNpc(this);\n        clearInterval(this.randomMovementInterval);\n        activeWorld.deregisterNpc(this);\n\n        if (respawn) {\n            const npcDetails = findNpc(this.id);\n            activeWorld.scheduleNpcRespawn(new Npc(npcDetails, this.npcSpawn));\n        }\n    }\n\n    public async tick(): Promise<void> {\n        super.tick();\n\n        return new Promise<void>(resolve => {\n            this.walkingQueue.process();\n            resolve();\n        });\n    }\n\n    public async reset(): Promise<void> {\n        return new Promise<void>(resolve => {\n            this.updateFlags.reset();\n            resolve();\n        });\n    }\n\n    /**\n     * Forces the Npc to speak the given message to the open world.\n     * @param message The message for the Npc to say.\n     */\n    public say(message: string): void {\n        this.updateFlags.addChatMessage({ message });\n    }\n\n    /**\n     * Whether or not the Npc can currently move.\n     */\n    public canMove(): boolean {\n        if (this.metadata.following) {\n            return false;\n        }\n        return this.updateFlags.faceActor === null && this.updateFlags.animation === null;\n    }\n\n    /**\n     * Plays a sound at the Npc's location for all nearby players.\n     * @param soundId The ID of the sound effect.\n     * @param volume The volume to play the sound at.\n     */\n    public playSound(soundId: number, volume: number): void {\n        activeWorld.playLocationSound(this.position, this.instance.instanceId, soundId, volume);\n    }\n\n    /**\n     * Transforms the Npc visually into a different Npc.\n     * @param npcKey The unique string key of the Npc to transform into.\n     */\n    public transformInto(npcKey: string): void {\n        const npcDetails = findNpc(npcKey);\n        this.id = npcDetails.gameId;\n        this.updateFlags.appearanceUpdateRequired = true;\n    }\n\n    /**\n     * Transforms the Npc visually into a different Npc.\n     * @param id The id of the Npc to transform into.\n     */\n    public setNewId(id: number): void {\n        this.id = id;\n        this.updateFlags.appearanceUpdateRequired = true;\n    }\n\n    public equals(other: Npc): boolean {\n        if (!other) {\n            return false;\n        }\n\n        return other.id === this.id && other.uuid === this.uuid;\n    }\n\n    public set position(position: Position) {\n        super.position = position;\n\n        if (this.quadtreeKey !== null) {\n            activeWorld.npcTree.remove(this.quadtreeKey);\n        }\n\n        this.quadtreeKey = { x: position.x, y: position.y, actor: this };\n        activeWorld.npcTree.push(this.quadtreeKey);\n    }\n\n    public get position(): Position {\n        return super.position;\n    }\n\n    public get name(): string {\n        return this._name;\n    }\n\n    public get combatLevel(): number {\n        return this._combatLevel;\n    }\n\n    public get movementRadius(): number {\n        return this._movementRadius;\n    }\n\n    public get exists(): boolean {\n        return this._exists;\n    }\n\n    public set exists(value: boolean) {\n        this._exists = value;\n    }\n\n    public get initialized(): boolean {\n        return this._initialized;\n    }\n\n    public get instanceId(): string | null {\n        return this.instance?.instanceId ?? null;\n    }\n}\n\n/**\n * A basic attempt at handling the odds of receiving an item from an NPCs DropTable.\n *\n * This method gets the odds defined in the DropTable, and rolls a random number to see if the odds are met.\n * Also checks whether or not the drop has a quest requirement, and accounts for that.\n *\n * @param player The player receiving the drop.\n * @param npcDetails The NpcDetails of the NPC that contains the DropTable data.\n */\nexport function calculateNpcDrops(player: Player, npcDetails: NpcDetails): { itemKey: string; amount?: number }[] {\n    const itemDrops: { itemKey: string; amount?: number }[] = [];\n    const npcDropTable = npcDetails.dropTable;\n    if (!npcDropTable) {\n        return itemDrops;\n    }\n\n    npcDropTable.forEach(drop => {\n        let meetsQuestRequirements = true;\n        if (drop.questRequirement) {\n            meetsQuestRequirements = player.getQuest(drop.questRequirement.questId).progress === drop.questRequirement.stage;\n        }\n        drop.amount = drop.amount || 1;\n        drop.amountMax = drop.amountMax || 1;\n\n        let odds: { numerator: number; denominator: number };\n        if (drop.frequency === 'always') {\n            odds = { numerator: 1, denominator: 1 };\n        } else {\n            const dividedFrequency = drop.frequency.split('/');\n            odds = { numerator: Number(dividedFrequency[0]), denominator: Number(dividedFrequency[1]) };\n        }\n        const randomNumber = getRandomInt(odds.denominator);\n        if (randomNumber === 1 && meetsQuestRequirements) {\n            const randomNumberOfItems = getRandomInt(drop.amountMax, drop.amount);\n            itemDrops.push({ itemKey: drop.itemKey, amount: randomNumberOfItems });\n        }\n    });\n\n    return itemDrops;\n}\n\n/**\n * Generates a random integer between a maximum and minimum value.\n * @param max The largest value to generate to.\n * @param min The smallest value to generate from.\n */\n\nfunction getRandomInt(max, min = 1): number {\n    return Math.floor(Math.random() * max) + min;\n}\n"
  },
  {
    "path": "src/engine/world/actor/pathfinding.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { Actor } from '@engine/world/actor/actor';\nimport { isPlayer } from '@engine/world/actor/util';\nimport type { WorldInstance } from '@engine/world/instances';\nimport type { Chunk } from '@engine/world/map/chunk';\nimport type { Tile } from '@engine/world/map/chunk-manager';\nimport { logger } from '@runejs/common';\nimport { Position } from '../position';\n\nclass Point {\n    private _parent: Point | null = null;\n    private _cost: number = 0;\n\n    public constructor(\n        private readonly _x: number,\n        private readonly _y: number,\n    ) {}\n\n    public equals(point: Point | null): boolean {\n        if (point === null) {\n            return false;\n        }\n\n        if (this._cost === point._cost) {\n            if (this._parent === null && point._parent !== null) {\n                return false;\n            } else if (this._parent !== null && !this._parent.equals(point._parent)) {\n                return false;\n            }\n\n            return this._x === point._x && this._y === point._y;\n        }\n\n        return false;\n    }\n\n    public get x(): number {\n        return this._x;\n    }\n\n    public get y(): number {\n        return this._y;\n    }\n\n    public get parent(): Point | null {\n        return this._parent;\n    }\n\n    public set parent(value: Point | null) {\n        this._parent = value;\n    }\n\n    public get cost(): number {\n        return this._cost;\n    }\n\n    public set cost(value: number) {\n        this._cost = value;\n    }\n}\n\nexport interface PathingOptions {\n    pathingSearchRadius?: number;\n    ignoreDestination?: boolean;\n}\n\nexport class Pathfinding {\n    public stopped = false;\n    private currentPoint: Point;\n    private points: Point[][];\n    private closedPoints = new Set<Point>();\n    private openPoints = new Set<Point>();\n\n    public constructor(private actor: Actor) {}\n\n    public walkTo(position: Position, options: PathingOptions): void {\n        if (!options.pathingSearchRadius) {\n            options.pathingSearchRadius = 16;\n        }\n\n        try {\n            const path = this.pathTo(position.x, position.y, options.pathingSearchRadius);\n\n            if (!path) {\n                return;\n            }\n\n            const walkingQueue = this.actor.walkingQueue;\n\n            walkingQueue.clear();\n            walkingQueue.valid = true;\n\n            if (options.ignoreDestination) {\n                path.splice(path.length - 1, 1);\n            }\n\n            for (const point of path) {\n                walkingQueue.add(point.x, point.y);\n            }\n        } catch (error) {\n            logger.error(error);\n        }\n    }\n\n    public createTileMap(searchRadius: number = 8): { [key: string]: Tile } {\n        const position = this.actor.position;\n        const lowestX = position.x - searchRadius;\n        const lowestY = position.y - searchRadius;\n        const highestX = position.x + searchRadius;\n        const highestY = position.y + searchRadius;\n\n        const tiles: Tile[] = [];\n        for (let x = lowestX; x < highestX; x++) {\n            for (let y = lowestY; y < highestY; y++) {\n                tiles.push(activeWorld.chunkManager.getTile(new Position(x, y, this.actor.position.level)));\n            }\n        }\n\n        return Object.fromEntries(tiles.map(tile => [`${tile.x},${tile.y}`, tile]));\n    }\n\n    public pathTo(destinationX: number, destinationY: number, searchRadius: number = 16): Point[] | null {\n        const position = this.actor.position;\n        const lowestX = position.x - searchRadius;\n        const lowestY = position.y - searchRadius;\n        const highestX = position.x + searchRadius;\n        const highestY = position.y + searchRadius;\n\n        if (destinationX < lowestX || destinationX > highestX || destinationY < lowestY || destinationY > highestY) {\n            throw new Error(`Out of range.`);\n        }\n\n        const destinationIndexX = destinationX - position.x + searchRadius;\n        const destinationIndexY = destinationY - position.y + searchRadius;\n        const startingIndexX = searchRadius;\n        const startingIndexY = searchRadius;\n\n        const pointLen = searchRadius * 2;\n\n        if (pointLen <= 0) {\n            throw new Error(`Why is your search radius zero?`);\n        }\n\n        this.points = [...Array(pointLen)].map(e => Array(pointLen));\n\n        for (let x = 0; x < pointLen; x++) {\n            for (let y = 0; y < pointLen; y++) {\n                this.points[x][y] = new Point(lowestX + x, lowestY + y);\n            }\n        }\n\n        // Starting point\n        this.openPoints = new Set<Point>();\n        this.closedPoints = new Set<Point>();\n        this.openPoints.add(this.points[startingIndexX][startingIndexY]);\n\n        while (this.openPoints.size > 0) {\n            if (this.stopped) {\n                return null;\n            }\n\n            const bestPoint = this.calculateBestPoint();\n\n            if (!bestPoint || bestPoint.equals(this.points[destinationIndexX][destinationIndexY])) {\n                break;\n            }\n\n            this.currentPoint = bestPoint;\n\n            this.openPoints.delete(this.currentPoint);\n            this.closedPoints.add(this.currentPoint);\n\n            const level = this.actor.position.level;\n            const { x, y } = this.currentPoint;\n            const indexX = x - lowestX;\n            const indexY = y - lowestY;\n\n            // North-West\n            if (indexX > 0 && this.points[indexX - 1] && indexY < this.points[indexX - 1].length - 1) {\n                if (this.canPathDiagonally(x, y, new Position(x - 1, y + 1, level), -1, 1, 0x1280138, 0x1280108, 0x1280120)) {\n                    this.calculateCost(this.points[indexX - 1][indexY + 1]);\n                }\n            }\n\n            // North-East\n            if (indexX < this.points.length - 1 && this.points[indexX + 1] && indexY < this.points[indexX + 1].length - 1) {\n                if (this.canPathDiagonally(x, y, new Position(x + 1, y + 1, level), 1, 1, 0x12801e0, 0x1280180, 0x1280120)) {\n                    this.calculateCost(this.points[indexX + 1][indexY + 1]);\n                }\n            }\n\n            // South-West\n            if (indexX > 0 && indexY > 0 && this.points[indexX - 1]) {\n                if (this.canPathDiagonally(x, y, new Position(x - 1, y - 1, level), -1, -1, 0x128010e, 0x1280108, 0x1280102)) {\n                    this.calculateCost(this.points[indexX - 1][indexY - 1]);\n                }\n            }\n\n            // South-East\n            if (indexX < this.points.length - 1 && indexY > 0 && this.points[indexX + 1]) {\n                if (this.canPathDiagonally(x, y, new Position(x + 1, y - 1, level), 1, -1, 0x1280183, 0x1280180, 0x1280102)) {\n                    this.calculateCost(this.points[indexX + 1][indexY - 1]);\n                }\n            }\n\n            // West\n            if (indexX > 0 && this.canPathNSEW(new Position(x - 1, y, level), 0x1280108)) {\n                this.calculateCost(this.points[indexX - 1][indexY]);\n            }\n\n            // East\n            if (indexX < this.points.length - 1 && this.canPathNSEW(new Position(x + 1, y, level), 0x1280180)) {\n                this.calculateCost(this.points[indexX + 1][indexY]);\n            }\n\n            // South\n            if (indexY > 0 && this.canPathNSEW(new Position(x, y - 1, level), 0x1280102)) {\n                this.calculateCost(this.points[indexX][indexY - 1]);\n            }\n\n            // North\n            if (\n                this.points[indexX] &&\n                indexY < this.points[indexX].length - 1 &&\n                this.canPathNSEW(new Position(x, y + 1, level), 0x1280120)\n            ) {\n                this.calculateCost(this.points[indexX][indexY + 1]);\n            }\n        }\n\n        const destinationPoint = this.points[destinationIndexX][destinationIndexY];\n\n        if (!destinationPoint || !destinationPoint.parent) {\n            // throw new Error(`Unable to find destination point.`);\n            return null;\n        }\n\n        // build path\n        const path: Point[] = [];\n        let point: Point | null = destinationPoint;\n        let iterations = 0;\n\n        do {\n            if (this.stopped) {\n                return null;\n            }\n\n            path.push(new Point(point.x, point.y));\n            point = point.parent;\n            iterations++;\n\n            if (iterations > 1000) {\n                throw new Error(`Path iteration overflow, path can not be found.`);\n            }\n\n            if (point === null) {\n                break;\n            }\n        } while (!point.equals(this.points[startingIndexX][startingIndexY]));\n\n        return path.reverse();\n    }\n\n    public canMoveTo(origin: Position, destination: Position): boolean {\n        const destinationChunk: Chunk = activeWorld.chunkManager.getChunkForWorldPosition(destination);\n        const tile: Tile = activeWorld.chunkManager.getTile(destination);\n\n        if (tile?.blocked) {\n            return false;\n        }\n\n        const initialX: number = origin.x;\n        const initialY: number = origin.y;\n        const destinationLocalX: number = destination.x - destinationChunk.collisionMap.insetX;\n        const destinationLocalY: number = destination.y - destinationChunk.collisionMap.insetY;\n\n        // West\n        if (destination.x < initialX && destination.y == initialY) {\n            if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280108)) {\n                return false;\n            }\n        }\n\n        // East\n        if (destination.x > initialX && destination.y == initialY) {\n            if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280180)) {\n                return false;\n            }\n        }\n\n        // South\n        if (destination.y < initialY && destination.x == initialX) {\n            if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280102)) {\n                return false;\n            }\n        }\n\n        // North\n        if (destination.y > initialY && destination.x == initialX) {\n            if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280120)) {\n                return false;\n            }\n        }\n\n        // South-West\n        if (destination.x < initialX && destination.y < initialY) {\n            if (\n                !this.diagonalMovementPermitted(\n                    this.instance,\n                    origin,\n                    destinationChunk,\n                    destinationLocalX,\n                    destinationLocalY,\n                    initialX,\n                    initialY,\n                    -1,\n                    -1,\n                    0x128010e,\n                    0x1280108,\n                    0x1280102,\n                )\n            ) {\n                return false;\n            }\n        }\n\n        // South-East\n        if (destination.x > initialX && destination.y < initialY) {\n            if (\n                !this.diagonalMovementPermitted(\n                    this.instance,\n                    origin,\n                    destinationChunk,\n                    destinationLocalX,\n                    destinationLocalY,\n                    initialX,\n                    initialY,\n                    1,\n                    -1,\n                    0x1280183,\n                    0x1280180,\n                    0x1280102,\n                )\n            ) {\n                return false;\n            }\n        }\n\n        // North-West\n        if (destination.x < initialX && destination.y > initialY) {\n            if (\n                !this.diagonalMovementPermitted(\n                    this.instance,\n                    origin,\n                    destinationChunk,\n                    destinationLocalX,\n                    destinationLocalY,\n                    initialX,\n                    initialY,\n                    -1,\n                    1,\n                    0x1280138,\n                    0x1280108,\n                    0x1280120,\n                )\n            ) {\n                return false;\n            }\n        }\n\n        // North-East\n        if (destination.x > initialX && destination.y > initialY) {\n            if (\n                !this.diagonalMovementPermitted(\n                    this.instance,\n                    origin,\n                    destinationChunk,\n                    destinationLocalX,\n                    destinationLocalY,\n                    initialX,\n                    initialY,\n                    1,\n                    1,\n                    0x12801e0,\n                    0x1280180,\n                    0x1280120,\n                )\n            ) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public movementPermitted(\n        instance: WorldInstance,\n        globalChunk: Chunk,\n        destinationLocalX: number,\n        destinationLocalY: number,\n        i: number,\n    ): boolean {\n        const instancedAdjacency = instance.getInstancedChunk(globalChunk.position.x, globalChunk.position.y, globalChunk.position.level)\n            .collisionMap.adjacency;\n        const globalAdjacency = globalChunk.collisionMap.adjacency;\n\n        try {\n            const instancedAdjacencyForTile = instancedAdjacency[destinationLocalX][destinationLocalY];\n\n            const instancedTileFlags = instancedAdjacencyForTile === null ? null : instancedAdjacencyForTile & i;\n\n            const globalAdjacencyForTile = globalAdjacency[destinationLocalX][destinationLocalY];\n\n            const globalTileFlags = globalAdjacencyForTile === null ? null : globalAdjacencyForTile & i;\n\n            return instancedTileFlags === null ? globalTileFlags === 0 : instancedTileFlags === 0;\n        } catch (error) {\n            logger.error(`Unable to calculate movement permission for local coordinates ${destinationLocalX},${destinationLocalY}.`);\n            return false;\n        }\n    }\n\n    public diagonalMovementPermitted(\n        instance: WorldInstance,\n        origin: Position,\n        destinationGlobalChunk: Chunk,\n        destinationLocalX: number,\n        destinationLocalY: number,\n        initialX: number,\n        initialY: number,\n        offsetX: number,\n        offsetY: number,\n        destMask: number,\n        cornerMask1: number,\n        cornerMask2: number,\n    ): boolean {\n        const corner1 = this.findLocalCornerChunk(initialX + offsetX, initialY, origin);\n        const corner2 = this.findLocalCornerChunk(initialX, initialY + offsetY, origin);\n\n        return (\n            this.movementPermitted(instance, destinationGlobalChunk, destinationLocalX, destinationLocalY, destMask) &&\n            this.movementPermitted(instance, corner1.chunk, corner1.localX, corner1.localY, cornerMask1) &&\n            this.movementPermitted(instance, corner2.chunk, corner2.localX, corner2.localY, cornerMask2)\n        );\n    }\n\n    public findLocalCornerChunk(cornerX: number, cornerY: number, origin: Position): { localX: number; localY: number; chunk: Chunk } {\n        const cornerPosition: Position = new Position(cornerX, cornerY, origin.level + 1);\n        let cornerChunk: Chunk = activeWorld.chunkManager.getChunkForWorldPosition(cornerPosition);\n        const tileAbove: Tile = activeWorld.chunkManager.getTile(cornerPosition);\n        if (!tileAbove?.bridge) {\n            cornerPosition.level = cornerPosition.level - 1;\n            cornerChunk = activeWorld.chunkManager.getChunkForWorldPosition(cornerPosition);\n        }\n        const localX: number = cornerX - cornerChunk.collisionMap.insetX;\n        const localY: number = cornerY - cornerChunk.collisionMap.insetY;\n\n        return { localX, localY, chunk: cornerChunk };\n    }\n\n    private calculateCost(point: Point): void {\n        if (!this.currentPoint || !point) {\n            return;\n        }\n\n        const nextStepCost = this.currentPoint.cost + this.calculateCostBetween(this.currentPoint, point);\n\n        if (nextStepCost < point.cost) {\n            this.openPoints.delete(point);\n            this.closedPoints.delete(point);\n        }\n\n        if (!this.openPoints.has(point) && !this.closedPoints.has(point)) {\n            point.parent = this.currentPoint;\n            point.cost = nextStepCost;\n            this.openPoints.add(point);\n        }\n    }\n\n    private calculateCostBetween(current: Point, destination: Point): number {\n        const deltaX = current.x - destination.x;\n        const deltaY = current.y - destination.y;\n        return (Math.abs(deltaX) + Math.abs(deltaY)) * 10;\n    }\n\n    private calculateBestPoint(): Point | null {\n        let bestPoint: Point | null = null;\n\n        this.openPoints.forEach(point => {\n            if (!bestPoint) {\n                bestPoint = point;\n            } else if (point.cost < bestPoint.cost) {\n                bestPoint = point;\n            }\n        });\n\n        return bestPoint;\n    }\n\n    private canPathNSEW(position: Position, i: number): boolean {\n        const chunk = activeWorld.chunkManager.getChunkForWorldPosition(position);\n        const destinationLocalX: number = position.x - chunk.collisionMap.insetX;\n        const destinationLocalY: number = position.y - chunk.collisionMap.insetY;\n        return this.movementPermitted(this.instance, chunk, destinationLocalX, destinationLocalY, i);\n    }\n\n    private canPathDiagonally(\n        originX: number,\n        originY: number,\n        position: Position,\n        offsetX: number,\n        offsetY: number,\n        destMask: number,\n        cornerMask1: number,\n        cornerMask2: number,\n    ): boolean {\n        const chunk = activeWorld.chunkManager.getChunkForWorldPosition(position);\n        const destinationLocalX: number = position.x - chunk.collisionMap.insetX;\n        const destinationLocalY: number = position.y - chunk.collisionMap.insetY;\n        return this.diagonalMovementPermitted(\n            this.instance,\n            position,\n            chunk,\n            destinationLocalX,\n            destinationLocalY,\n            originX,\n            originY,\n            offsetX,\n            offsetY,\n            destMask,\n            cornerMask1,\n            cornerMask2,\n        );\n    }\n\n    private get instance(): WorldInstance {\n        return isPlayer(this.actor) ? this.actor.instance : activeWorld.globalInstance;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/achievements.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport { gfxIds } from '@engine/world/config/gfx-ids';\nimport { serverConfig } from '@server/game/game-server';\n\nexport const achievementSeries = {\n    lumbridge: {\n        name: 'Lumbridge',\n    },\n    varrock: {\n        name: 'Varrock',\n    },\n};\n\nexport enum AchievementSeries {\n    LUMBRIDGE = 'lumbridge',\n    VARROCK = 'varrock',\n}\n\nexport interface Achievement {\n    id: string;\n    name: string;\n    description: string;\n    longDescription: string;\n    series: AchievementSeries;\n}\n\nexport const Achievements: { [key: string]: Achievement } = {\n    WELCOME: {\n        id: 'lumbridge-hans-welcome',\n        name: 'Welcome!',\n        description: 'Talk to Hans.',\n        longDescription: `Speak with Hans in the Lumbridge Castle's courtyard.`,\n        series: AchievementSeries.LUMBRIDGE,\n    },\n    BURY_BONES: {\n        id: 'bury-bones',\n        name: 'Grave Digger',\n        description: 'Bury the bones of the dead.',\n        longDescription: `Bury the remains of a deceased enemy.`,\n        series: AchievementSeries.LUMBRIDGE,\n    },\n};\n\nexport function giveAchievement(achievement: Achievement, player: Player): boolean {\n    if (!serverConfig.giveAchievements) {\n        return false;\n    }\n\n    if (hasAchievement(achievement, player)) {\n        return false;\n    }\n\n    player.achievements.push(achievement.id);\n    player.playGraphics({ id: gfxIds.levelUpFireworks, delay: 0, height: 125 });\n    player.sendMessage(\n        `<col=ffff00><shad>You've completed an Achievement in the ` + `${achievementSeries[achievement.series].name} series!</shad></col>`,\n    );\n    player.sendMessage(`<col=255>${achievement.name}</col> - <i>${achievement.description}</i>`);\n    return true;\n}\n\nexport function hasAchievement(achievement: Achievement, player: Player): boolean {\n    if (!player.achievements || player.achievements.length === 0) {\n        return false;\n    }\n\n    return player.achievements.indexOf(achievement.id) !== -1;\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/attack.ts",
    "content": "//This will be used to pass information required to calulate attack and defense like weapon damage etc\n\nexport class Attack {\n    damageType: AttackDamageType;\n    attackRoll: number = 0;\n    defenseRoll: number = 0;\n    hitChance: number = 0;\n    damage: number = 0;\n    maximumHit: number;\n}\n\nexport enum AttackDamageType {\n    Stab,\n    Slash,\n    Crush,\n    Magic,\n    Range,\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/cutscenes.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\n\n/**\n * Various camera options for cutscenes.\n */\nexport interface CameraOptions {\n    cameraX?: number;\n    cameraY?: number;\n    cameraHeight?: number;\n    cameraMovementSpeed?: number;\n    cameraAcceleration?: number;\n    lookX?: number;\n    lookY?: number;\n    lookHeight?: number;\n    lookMovementSpeed?: number;\n    lookAcceleration?: number;\n}\n\n/**\n * Controls a game cutscene for a specific player.\n */\nexport class Cutscene {\n    public readonly player: Player;\n    private _cameraX: number;\n    private _cameraY: number;\n    private _cameraHeight: number;\n    private _cameraMovementSpeed: number;\n    private _cameraAcceleration: number;\n    private _lookX: number;\n    private _lookY: number;\n    private _lookHeight: number;\n    private _lookMovementSpeed: number;\n    private _lookAcceleration: number;\n\n    public constructor(player: Player, options?: CameraOptions) {\n        this.player = player;\n\n        if (options) {\n            this.setCamera(options);\n        }\n    }\n\n    /**\n     * Sets the cutscene camera to the specified options.\n     * @param options The camera options to use.\n     */\n    public setCamera(options: CameraOptions): void {\n        const {\n            cameraX,\n            cameraY,\n            cameraHeight,\n            cameraMovementSpeed,\n            cameraAcceleration,\n            lookX,\n            lookY,\n            lookHeight,\n            lookMovementSpeed,\n            lookAcceleration,\n        } = options;\n\n        if (cameraX && cameraY) {\n            this.snapCameraTo(cameraX, cameraY, cameraHeight || 400, cameraMovementSpeed || 0, cameraAcceleration || 100);\n        }\n        if (lookX && lookY) {\n            this.lookAt(lookX, lookY, lookHeight || 400, lookMovementSpeed || 0, lookAcceleration || 100);\n        }\n    }\n\n    /**\n     * Snaps the cutscene to a specific location.\n     * @param cameraX The world X coordinate to snap the camera to.\n     * @param cameraY The world Y coordinate to snap the camera to.\n     * @param height The height of the camera relative to the ground. Defaults to 400.\n     * @param movementSpeed The general speed of the camera movement. Defaults to 0 (immediate).\n     * @param acceleration The acceleration speed of the camera movement. Defaults to 100 (instantaneous).\n     */\n    public snapCameraTo(\n        cameraX: number,\n        cameraY: number,\n        height: number = 400,\n        movementSpeed: number = 0,\n        acceleration: number = 100,\n    ): void {\n        this._cameraX = cameraX;\n        this._cameraY = cameraY;\n        this._cameraHeight = height;\n        this._cameraMovementSpeed = movementSpeed;\n        this._cameraAcceleration = acceleration;\n        this.player.outgoingPackets.snapCameraTo(new Position(cameraX, cameraY), height, movementSpeed, acceleration);\n    }\n\n    /**\n     * Makes the camera look at a specific location from it's current snap point.\n     * @param lookX The world X coordinate to look towards.\n     * @param lookY The world Y coordinate to look towards.\n     * @param height The height that the camera should be looking at, relative to the ground. Defaults to 400.\n     * @param movementSpeed The general speed of the camera movement. Defaults to 0 (immediate).\n     * @param acceleration The acceleration speed of the camera movement. Defaults to 100 (instantaneous).\n     */\n    public lookAt(lookX: number, lookY: number, height: number = 400, movementSpeed: number = 0, acceleration: number = 100): void {\n        this._lookX = lookX;\n        this._lookY = lookY;\n        this._lookHeight = height;\n        this._lookMovementSpeed = movementSpeed;\n        this._lookAcceleration = acceleration;\n        this.player.outgoingPackets.turnCameraTowards(new Position(lookX, lookY), height, movementSpeed, acceleration);\n    }\n\n    /**\n     * Ends the current cutscene and snaps the camera back to the player character.\n     */\n    public endCutscene(): void {\n        this.player.outgoingPackets.resetCamera();\n        this.player.cutscene = null;\n    }\n\n    public get cameraX(): number {\n        return this._cameraX;\n    }\n\n    public get cameraY(): number {\n        return this._cameraY;\n    }\n\n    public get cameraHeight(): number {\n        return this._cameraHeight;\n    }\n\n    public get cameraMovementSpeed(): number {\n        return this._cameraMovementSpeed;\n    }\n\n    public get cameraAcceleration(): number {\n        return this._cameraAcceleration;\n    }\n\n    public get lookX(): number {\n        return this._lookX;\n    }\n\n    public get lookY(): number {\n        return this._lookY;\n    }\n\n    public get lookHeight(): number {\n        return this._lookHeight;\n    }\n\n    public get lookMovementSpeed(): number {\n        return this._lookMovementSpeed;\n    }\n\n    public get lookAcceleration(): number {\n        return this._lookAcceleration;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/dialogue-action.ts",
    "content": "import type { Npc } from '@engine/world/actor/npc';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { filestore } from '@server/game/game-server';\n\nexport const dialogueWidgetIds = {\n    PLAYER: [64, 65, 66, 67],\n    NPC: [241, 242, 243, 244],\n    OPTIONS: [228, 230, 232, 234],\n    TEXT: [210, 211, 212, 213, 214],\n};\n\ntype LineConstraint = [number, number];\n\n/**\n * Min -> max lines for a specific dialogue type.\n */\nconst lineConstraints: { [key: string]: LineConstraint } = {\n    PLAYER: [1, 4],\n    NPC: [1, 4],\n    OPTIONS: [2, 5],\n    TEXT: [1, 5],\n};\n\nexport enum DialogueEmote {\n    JOYFUL = 588,\n    CALM_TALK_1 = 589,\n    CALM_TALK_2 = 590,\n    DEFAULT = 591,\n    EVIL_1 = 592,\n    EVIL_2 = 593,\n    EVIL_3 = 594,\n    ANNOYED = 595,\n    DISTRESSED_1 = 596,\n    DISTRESSED_2 = 597,\n    BOWS_HEAD_SAD = 598,\n    DRUNK_LEFT = 600,\n    DRUNK_RIGHT = 601,\n    NOT_INTERESTED = 602,\n    SLEEPY = 603,\n    DEVILISH = 604,\n    LAUGH_1 = 605,\n    LAUGH_2 = 606,\n    LAUGH_3 = 607,\n    LAUGH_4 = 608,\n    EVIL_LAUGH = 609,\n    SAD_1 = 610,\n    SAD_2 = 611,\n    SAD_3 = 598,\n    SAD_4 = 613,\n    CONSIDERING = 612,\n    ANGRY_1 = 614,\n    ANGRY_2 = 615,\n    ANGRY_3 = 616,\n    ANGRY_4 = 617,\n}\n\nexport type DialogueType = 'PLAYER' | 'NPC' | 'OPTIONS' | 'TEXT';\n\nexport interface DialogueOptions {\n    type: DialogueType;\n    npc?: number;\n    emote?: DialogueEmote;\n    title?: string;\n    skillId?: number;\n    lines: string[];\n}\n\n// @DEPRECATED\nexport class DialogueAction {\n    private _action: number | null = null;\n\n    public constructor(private readonly p: Player) {}\n\n    public async player(emote: DialogueEmote, lines: string[]): Promise<DialogueAction> {\n        return this.dialogue({ emote, lines, type: 'PLAYER' });\n    }\n\n    public async npc(npc: Npc | number, emote: DialogueEmote, lines: string[]): Promise<DialogueAction> {\n        return this.dialogue({ emote, lines, type: 'NPC', npc: typeof npc === 'number' ? npc : npc.id });\n    }\n\n    public async options(title: string, options: string[]): Promise<DialogueAction> {\n        return this.dialogue({ type: 'OPTIONS', title, lines: options });\n    }\n\n    public async dialogue(options: DialogueOptions): Promise<DialogueAction> {\n        if (options.lines.length < lineConstraints[options.type][0] || options.lines.length > lineConstraints[options.type][1]) {\n            throw new Error('Invalid line length.');\n        }\n\n        if (options.type === 'NPC' && options.npc === undefined) {\n            throw new Error('NPC not supplied.');\n        }\n\n        this._action = null;\n\n        let widgetIndex = options.lines.length - 1;\n        if (options.type === 'OPTIONS') {\n            widgetIndex--;\n        }\n\n        const widgetId = dialogueWidgetIds[options.type][widgetIndex];\n\n        if (widgetId === undefined || widgetId === null || widgetId === -1) {\n            return Promise.resolve(this);\n        }\n\n        let textOffset = 0;\n\n        if (options.type === 'PLAYER' || options.type === 'NPC') {\n            if (!options.emote) {\n                options.emote = DialogueEmote.DEFAULT;\n            }\n\n            if (options.type === 'NPC') {\n                const npc = options.npc;\n\n                if (npc === undefined) {\n                    // TODO (Jameskmonger) how can this error be handled in a better way?\n                    throw new Error('NPC not supplied.');\n                }\n\n                const cacheNpc = filestore.configStore.npcStore.getNpc(npc);\n\n                if (!cacheNpc) {\n                    throw new Error(`NPC ${npc} not found in cache.`);\n                }\n\n                this.p.outgoingPackets.setWidgetNpcHead(widgetId, 0, npc);\n                this.p.outgoingPackets.updateWidgetString(widgetId, 1, cacheNpc.name || 'Unknown');\n            } else if (options.type === 'PLAYER') {\n                this.p.outgoingPackets.setWidgetPlayerHead(widgetId, 0);\n                this.p.outgoingPackets.updateWidgetString(widgetId, 1, this.p.username);\n            }\n\n            this.p.outgoingPackets.playWidgetAnimation(widgetId, 0, options.emote);\n            textOffset = 2;\n        } else if (options.type === 'OPTIONS') {\n            this.p.outgoingPackets.updateWidgetString(widgetId, 0, options.title || 'No Title');\n            textOffset = 1;\n        } else if (options.type === 'TEXT') {\n            textOffset = 0;\n        }\n\n        for (let i = 0; i < options.lines.length; i++) {\n            this.p.outgoingPackets.updateWidgetString(widgetId, textOffset + i, options.lines[i]);\n        }\n\n        return new Promise<DialogueAction>((resolve, reject) => {\n            this.p.interfaceState.openWidget(widgetId, {\n                slot: 'chatbox',\n            });\n            const sub = this.p.interfaceState.closed.subscribe(action => {\n                sub.unsubscribe();\n                this._action = action?.data ?? null;\n                resolve(this);\n            });\n        });\n    }\n\n    public close(): void {\n        this.p.outgoingPackets.closeActiveWidgets();\n    }\n\n    public get action(): number | null {\n        return this._action;\n    }\n\n    public set action(value: number | null) {\n        this._action = value;\n    }\n}\n\nexport const dialogueAction = async (player: Player, options?: DialogueOptions): Promise<DialogueAction> => {\n    if (options) {\n        return new DialogueAction(player).dialogue(options);\n    } else {\n        return Promise.resolve(new DialogueAction(player));\n    }\n};\n"
  },
  {
    "path": "src/engine/world/actor/player/metadata.ts",
    "content": "import type { Chunk } from '@engine/world/map/chunk';\nimport type { Position } from '@engine/world/position';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport type { Subject, Subscription } from 'rxjs';\n\n/**\n * The definition of the metadata directly available on a {@link Player}.\n *\n * This is a subset of the metadata available on an {@link Actor}. See {@link ActorMetadata} for more information.\n *\n * You cannot guarantee that all of these properties will be present on an actor,\n * so you should always check for their existence before using them.\n *\n * @author jameskmonger\n */\nexport type PlayerMetadata = {\n    /**\n     * The player's client configuration options (varps).\n     */\n    configs: number[];\n\n    /**\n     * The player's current and previous chunks.\n     */\n    updateChunk: {\n        oldChunk: Chunk;\n        newChunk: Chunk;\n    };\n\n    /**\n     * The player's last position before teleporting.\n     */\n    lastPosition: Position;\n\n    /**\n     * Used to prevent the `object_interaction` pipe from running.\n     *\n     * TODO (jameskmonger) We should probably deprecate this, it seems like it's already been\n     *          replaced by the `busy` property which is itself deprecated. This is only\n     *          used in Goblin Diplomacy.\n     */\n    blockObjectInteractions: boolean;\n\n    /**\n     * The player's currently open shop.\n     */\n    lastOpenedShopKey: string;\n\n    /**\n     * A subscription to the player's \"widget closed\" events.\n     *\n     * Used to remove a player from a shop when they close the shop's widget.\n     */\n    shopCloseListener: Subscription;\n\n    /**\n     * Allows listening to a player clicking on the tab at the specified index.\n     *\n     * The `event` property is a `Subject` that will emit a `boolean` value when the player clicks on the tab.\n     *\n     * TODO (jameskmonger) This is only used in Goblin Diplomacy. It is only present when the player is taking part\n     *                  in Goblin Displomacy.\n     */\n    tabClickEvent: {\n        tabIndex: number;\n        event: Subject<boolean>;\n    };\n\n    /**\n     * Used to process dialogue trees.\n     */\n    dialogueIndices: Record<string, number>;\n\n    /**\n     * The player's current dialogue tree.\n     *\n     * This is a `ParsedDialogueTree` type, but that type is not exported.\n     */\n    dialogueTree: any;\n\n    /**\n     * A list of custom landscape objects that have been spawned by the player.\n     *\n     * Initialised by, and used by, the `spawn-scenery` command.\n     */\n    spawnedScenery: LandscapeObject[];\n\n    /**\n     * The last custom landscape object that was spawned by the player.\n     *\n     * Used to provide `undo` functionality to the `spawn-scenery` command.\n     */\n    lastSpawnedScenery: LandscapeObject;\n\n    /**\n     * The timestamp of the last time the player lit a fire.\n     *\n     * Used to prevent the player from lighting fires too quickly.\n     *\n     * TODO (jameskmonger) this should not be using Dates for timing and will be converted in the new task system\n     */\n    lastFire: number;\n\n    /**\n     * The ID of the player's currently open skill guide.\n     */\n    activeSkillGuide: number;\n\n    /**\n     * Whether or not the player is casting a spell that immobilizes them.\n     * Different from the base Actor class's teleport metadata property. A use\n     * case for this is to prevent the player from teleporting multiple times\n     * before the teleport animation finishes (it takes a few ticks).\n     */\n    castingStationarySpell: boolean;\n};\n"
  },
  {
    "path": "src/engine/world/actor/player/model.ts",
    "content": "/**\n * Options for sending chat messages to a player.\n */\nexport interface SendMessageOptions {\n    dialogue?: boolean;\n    console?: boolean;\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/player-data.ts",
    "content": "import { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport type { PlayerQuest } from '@engine/config/quest-config';\nimport { hasValueNotNull } from '@engine/util/data';\nimport type { SkillValue } from '@engine/world/actor/skills';\nimport type { Item } from '@engine/world/items/item';\nimport { MusicPlayerLoopMode, MusicPlayerMode } from '@engine/world/sound/music';\nimport { logger } from '@runejs/common';\nimport type { Player } from './player';\n\nexport interface Appearance {\n    gender: number;\n    head: number;\n    torso: number;\n    arms: number;\n    legs: number;\n    hands: number;\n    feet: number;\n    facialHair: number;\n    hairColor: number;\n    torsoColor: number;\n    legColor: number;\n    feetColor: number;\n    skinColor: number;\n}\n\nexport class PlayerSettings {\n    musicVolume: number = 0;\n    musicPlayerMode: number = MusicPlayerMode.AUTO;\n    musicPlayerLoopMode: number = MusicPlayerLoopMode.ENABLED;\n    soundEffectVolume: number = 0;\n    areaEffectVolume: number = 0;\n    splitPrivateChatEnabled: boolean = false;\n    twoMouseButtonsEnabled: boolean = true;\n    screenBrightness: number = 2;\n    chatEffectsEnabled: boolean = true;\n    acceptAidEnabled: boolean = true;\n    runEnabled: boolean = false;\n    autoRetaliateEnabled: boolean = true;\n    attackStyle: number = 0;\n    bankInsertMode: number = 0;\n    bankWithdrawNoteMode: number = 0;\n    publicChatMode: number = 0;\n    privateChatMode: number = 0;\n    tradeMode: number = 0;\n}\n\nexport interface PlayerSave {\n    username: string;\n    passwordHash: string;\n    rights: number;\n    position: {\n        x: number;\n        y: number;\n        level: number;\n    };\n    lastLogin: {\n        date: Date;\n        address: string;\n    };\n    appearance: Appearance;\n    inventory: (Item | null)[];\n    bank: (Item | null)[];\n    equipment: (Item | null)[];\n    skills: SkillValue[];\n    settings: PlayerSettings;\n    savedMetadata: { [key: string]: any };\n    questList: PlayerQuest[];\n    musicTracks: Array<number>;\n    achievements: string[];\n    friendsList: string[];\n    ignoreList: string[];\n}\n\nexport const defaultAppearance = (): Appearance => {\n    return {\n        gender: 0,\n        head: 0,\n        torso: 18,\n        arms: 26,\n        legs: 36,\n        hands: 33,\n        feet: 42,\n        facialHair: 10,\n        hairColor: 0,\n        torsoColor: 0,\n        legColor: 0,\n        feetColor: 0,\n        skinColor: 0,\n    } as Appearance;\n};\n\nexport const defaultSettings = (): PlayerSettings => {\n    return new PlayerSettings();\n};\n\nexport const validateSettings = (player: Player): void => {\n    const existingKeys = Object.keys(player.settings);\n    const newSettings = new PlayerSettings();\n    const newKeys = Object.keys(newSettings);\n\n    if (newKeys.length === existingKeys.length) {\n        return;\n    }\n\n    const missingKeys = newKeys.filter(key => existingKeys.indexOf(key) === -1);\n    for (const key of missingKeys) {\n        player.settings[key] = newSettings[key];\n    }\n};\n\nexport function savePlayerData(player: Player): boolean {\n    const fileName = player.username.toLowerCase() + '.json';\n    const filePath = join('data/saves', fileName);\n\n    const playerSave: PlayerSave = {\n        username: player.username,\n        passwordHash: player.passwordHash,\n        position: {\n            x: player.position.x,\n            y: player.position.y,\n            level: player.position.level > 3 ? 0 : player.position.level,\n        },\n        lastLogin: {\n            date: player.loginDate,\n            address: player.lastAddress,\n        },\n        rights: player.rights.valueOf(),\n        appearance: player.appearance,\n        inventory: player.inventory.items,\n        bank: player.bank.items.filter(item => {\n            return hasValueNotNull(item);\n        }),\n        equipment: player.equipment.items,\n        skills: player.skills.values,\n        settings: player.settings,\n        savedMetadata: player.savedMetadata,\n        questList: player.quests,\n        musicTracks: player.musicTracks,\n        achievements: player.achievements,\n        friendsList: player.friendsList,\n        ignoreList: player.ignoreList,\n    };\n\n    try {\n        writeFileSync(filePath, JSON.stringify(playerSave, null, 4));\n        return true;\n    } catch (error) {\n        logger.error(`Error saving player data for ${player.username}.`);\n        return false;\n    }\n}\n\nexport function playerExists(username: string): boolean {\n    const fileName = username.toLowerCase() + '.json';\n    const filePath = join('data/saves', fileName);\n    return existsSync(filePath);\n}\n\nexport function loadPlayerSave(username: string): PlayerSave | null {\n    const fileName = username.toLowerCase() + '.json';\n    const filePath = join('data/saves', fileName);\n\n    if (!existsSync(filePath)) {\n        return null;\n    }\n\n    const fileData = readFileSync(filePath, 'utf8');\n\n    if (!fileData) {\n        return null;\n    }\n\n    try {\n        const playerSave = JSON.parse(fileData) as PlayerSave;\n        if (playerSave?.position?.level > 3) {\n            playerSave.position.level = 0;\n        }\n        return playerSave;\n    } catch (error) {\n        logger.error(`Malformed player save data for ${username}.`);\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/player.ts",
    "content": "import EventEmitter from 'events';\nimport type { AddressInfo, Socket } from 'net';\nimport type { PlayerCommandActionHook } from '@engine/action/pipe/player-command.action';\nimport { regionChangeActionFactory } from '@engine/action/pipe/region-change.action';\nimport {\n    findItem,\n    findMusicTrack,\n    findNpc,\n    findQuest,\n    findSongIdByRegionId,\n    itemMap,\n    musicRegions,\n    npcIdMap,\n    widgets,\n} from '@engine/config/config-handler';\nimport type { EquipmentSlot, ItemDetails } from '@engine/config/item-config';\nimport { equipmentIndex, getEquipmentSlot } from '@engine/config/item-config';\nimport type { NpcDetails } from '@engine/config/npc-config';\nimport type { QuestKey } from '@engine/config/quest-config';\nimport { PlayerQuest } from '@engine/config/quest-config';\nimport { InterfaceState } from '@engine/interface/interface-state';\nimport type { Isaac } from '@engine/net/isaac';\nimport { OutboundPacketHandler } from '@engine/net/outbound-packet-handler';\nimport { actionHookMap, questMap } from '@engine/plugins/loader';\nimport { colors, hexToRgb, rgbTo16Bit } from '@engine/util/colors';\nimport { daysSinceLastLogin } from '@engine/util/time';\nimport { getVarbitMorphIndex } from '@engine/util/varbits';\nimport { activeWorld } from '@engine/world';\nimport type { Appearance, PlayerSettings } from '@engine/world/actor/player/player-data';\nimport { defaultAppearance, defaultSettings, loadPlayerSave, playerExists, savePlayerData } from '@engine/world/actor/player/player-data';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport type { PlayerWidget } from '@engine/world/config/widget';\nimport { widgetScripts } from '@engine/world/config/widget';\nimport type { TileModifications } from '@engine/world/instances';\nimport { WorldInstance } from '@engine/world/instances';\nimport type { Item } from '@engine/world/items/item';\nimport type { ContainerUpdateEvent } from '@engine/world/items/item-container';\nimport { ItemContainer, getItemFromContainer } from '@engine/world/items/item-container';\nimport type { Chunk, ChunkUpdateItem } from '@engine/world/map/chunk';\nimport { Position } from '@engine/world/position';\nimport { MusicPlayerMode } from '@engine/world/sound/music';\nimport type { QuadtreeKey } from '@engine/world/world';\nimport { logger } from '@runejs/common';\nimport { filestore, serverConfig } from '@server/game/game-server';\nimport { Subject } from 'rxjs';\nimport { v4 } from 'uuid';\nimport { Actor } from '../actor';\nimport { dialogue } from '../dialogue';\nimport type { Npc } from '../npc';\nimport type { SkillName } from '../skills';\nimport type { Cutscene } from './cutscenes';\nimport type { PlayerMetadata } from './metadata';\nimport type { SendMessageOptions } from './model';\nimport { NpcSyncTask } from './sync/npc-sync-task';\nimport { PlayerSyncTask } from './sync/player-sync-task';\n\nexport const playerOptions: { option: string; index: number; placement: 'TOP' | 'BOTTOM' }[] = [\n    {\n        option: 'Yeet',\n        index: 1,\n        placement: 'TOP',\n    },\n    {\n        option: 'Follow',\n        index: 0,\n        placement: 'BOTTOM',\n    },\n];\n\nexport const defaultPlayerTabWidgets = () => [\n    -1,\n    widgets.skillsTab,\n    widgets.questTab,\n    widgets.inventory.widgetId,\n    widgets.equipment.widgetId,\n    widgets.prayerTab,\n    widgets.standardSpellbookTab,\n    null,\n    widgets.friendsList,\n    widgets.ignoreList,\n    widgets.logoutTab,\n    widgets.settingsTab,\n    widgets.emotesTab,\n    widgets.musicPlayerTab,\n];\n\nexport enum SidebarTab {\n    COMBAT,\n    SKILL,\n    QUEST,\n    INVENTORY,\n    EQUIMENT,\n    PRAYER,\n    MAGIC,\n    FRIENDS,\n    IGNORE,\n    LOGOUT,\n    SETTINGS,\n    EMOTES,\n    MUSIC,\n}\n\nexport enum Rights {\n    ADMIN = 2,\n    MOD = 1,\n    USER = 0,\n}\n\n/**\n * A player character within the game world.\n */\nexport class Player extends Actor {\n    public readonly clientUuid: number;\n    public readonly username: string;\n    public readonly passwordHash: string;\n    public readonly playerUpdateTask: PlayerSyncTask;\n    public readonly npcUpdateTask: NpcSyncTask;\n    public readonly numericInputEvent: Subject<number>;\n    public readonly dialogueInteractionEvent: Subject<number>;\n    public readonly personalInstance = new WorldInstance(v4());\n    public readonly interfaceState = new InterfaceState(this);\n    public isLowDetail: boolean;\n    public trackedPlayers: Player[];\n    public trackedNpcs: Npc[];\n    public savedMetadata: { [key: string]: any } = {};\n    public sessionMetadata: { [key: string]: any } = {};\n    public quests: PlayerQuest[] = [];\n    public musicTracks: Array<number> = [0, 400, 547, 321];\n    public achievements: string[] = [];\n    public friendsList: string[] = [];\n    public ignoreList: string[] = [];\n    public cutscene: Cutscene | null = null;\n    public playerEvents: EventEmitter = new EventEmitter();\n\n    /**\n     * Override the Actor's `metadata` property to provide a more specific type.\n     *\n     * You cannot guarantee that this will be populated with data, so you should always check for the existence of the\n     * metadata you are looking for before using it.\n     *\n     * @author jameskmonger\n     */\n    public readonly metadata: Actor['metadata'] & Partial<PlayerMetadata> = {};\n\n    private readonly _socket: Socket;\n    private readonly _inCipher: Isaac;\n    private readonly _outCipher: Isaac;\n    private readonly _outgoingPackets: OutboundPacketHandler;\n    private readonly _equipment: ItemContainer;\n    private _rights: Rights;\n    private _loginDate: Date;\n    private _lastAddress: string;\n    private firstTimePlayer: boolean;\n    private _appearance: Appearance;\n    private queuedWidgets: PlayerWidget[];\n    private _carryWeight: number;\n    private _settings: PlayerSettings;\n    private _nearbyChunks: Chunk[];\n    private quadtreeKey: QuadtreeKey | null = null;\n    private privateMessageIndex: number = 1;\n\n    public constructor(\n        socket: Socket,\n        inCipher: Isaac,\n        outCipher: Isaac,\n        clientUuid: number,\n        username: string,\n        password: string,\n        isLowDetail: boolean,\n    ) {\n        super('player');\n\n        this._socket = socket;\n        this._inCipher = inCipher;\n        this._outCipher = outCipher;\n        this.clientUuid = clientUuid;\n        this.username = username;\n        this.passwordHash = password;\n        this._rights = Rights.ADMIN;\n        this.isLowDetail = isLowDetail;\n        this._outgoingPackets = new OutboundPacketHandler(this);\n        this.playerUpdateTask = new PlayerSyncTask(this);\n        this.npcUpdateTask = new NpcSyncTask(this);\n        this.trackedPlayers = [];\n        this.trackedNpcs = [];\n        this.queuedWidgets = [];\n        this._carryWeight = 0;\n        this._equipment = new ItemContainer(14);\n        this.dialogueInteractionEvent = new Subject<number>();\n        this.numericInputEvent = new Subject<number>();\n        this._nearbyChunks = [];\n        this.friendsList = [];\n        this.ignoreList = [];\n\n        this.loadSaveData();\n    }\n\n    public async init(): Promise<void> {\n        super.init();\n\n        this.updateFlags.mapRegionUpdateRequired = true;\n        this.updateFlags.appearanceUpdateRequired = true;\n\n        const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.position);\n        playerChunk.addPlayer(this);\n\n        this.outgoingPackets.updateCurrentMapChunk();\n        this.outgoingPackets.chatboxMessage('Welcome to RuneJS.');\n\n        this.skills.values.forEach((skill, index) => this.outgoingPackets.updateSkill(index, this.skills.getLevel(index), skill.exp));\n\n        this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory);\n        this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipment, this.equipment);\n        for (const item of this.equipment.items) {\n            if (item) {\n                await this.actionPipeline.call('equipment_change', this, item.itemId, 'EQUIP');\n            }\n        }\n\n        if (this.firstTimePlayer) {\n            if (!serverConfig.tutorialEnabled) {\n                this.interfaceState.openWidget(widgets.characterDesign, {\n                    slot: 'screen',\n                    multi: false,\n                });\n            }\n        } else if (serverConfig.showWelcome && (!serverConfig.tutorialEnabled || this.savedMetadata.tutorialComplete)) {\n            const daysSinceLogin = daysSinceLastLogin(this.loginDate);\n            let loginDaysStr = '';\n\n            if (daysSinceLogin <= 0) {\n                loginDaysStr = 'earlier today';\n            } else if (daysSinceLogin === 1) {\n                loginDaysStr = 'yesterday';\n            } else {\n                loginDaysStr = daysSinceLogin + ' days ago';\n            }\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreenChildren.question,\n                1,\n                `Want to help RuneJS improve?\\\\nSend us a pull request over on Github!`,\n            );\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                13,\n                `You last logged in @red@${loginDaysStr}@bla@ from: @red@${this.lastAddress}`,\n            );\n            this.outgoingPackets.updateWidgetString(widgets.welcomeScreen, 16, `You have @yel@0 unread messages\\\\nin your message centre.`);\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                14,\n                `\\\\nYou have not yet set any recovery questions.\\\\nIt is @lre@strongly@yel@ recommended that you do so.\\\\n\\\\nIf you don't you will be @lre@unable to recover your\\\\n@lre@password@yel@ if you forget it, or it is stolen.`,\n            );\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                22,\n                `To change your recovery questions:\\\\n1) Logout and return to the frontpage of this website.\\\\n2) Choose 'Set new recovery questions'.`,\n            );\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                17,\n                `\\\\nYou do not have a Bank PIN.\\\\nPlease visit a bank if you would like one.`,\n            );\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                21,\n                `To start a subscripton:\\\\n1) Logout and return to the frontpage of this website.\\\\n2) Choose 'Start a new subscription'`,\n            );\n            this.outgoingPackets.updateWidgetString(\n                widgets.welcomeScreen,\n                19,\n                `You are not a member.\\\\n\\\\nChoose to subscribe and\\\\nyou'll get loads of extra\\\\nbenefits and features.`,\n            );\n\n            this.interfaceState.openWidget(widgets.welcomeScreen, {\n                slot: 'full',\n                containerId: widgets.welcomeScreenChildren.question,\n                // The welcome screen's main button does not trigger the button packet\n                // @todo read this from elsewhere to mark the welcome screen as closed\n                doNotRegister: true,\n            });\n        }\n\n        for (const playerOption of playerOptions) {\n            this.outgoingPackets.updatePlayerOption(playerOption.option, playerOption.index, playerOption.placement);\n        }\n\n        this.updateBonuses();\n        this.updateCarryWeight(true);\n        this.updateQuestTab();\n        this.updateMusicTab();\n\n        this.inventory.containerUpdated.subscribe(event => this.inventoryUpdated(event));\n        this.playerEvents.on('exp', amt => {\n            logger.info(`Player should have been awarded ${amt} exp if this was hooked up.`);\n        });\n\n        this.actionsCancelled.subscribe(type => {\n            let closeWidget: boolean;\n\n            if (type === 'manual-movement' || type === 'pathing-movement') {\n                closeWidget = true;\n            } else if (type === 'keep-widgets-open' || type === 'button' || type === 'widget') {\n                closeWidget = false;\n            } else {\n                closeWidget = true;\n            }\n\n            if (closeWidget) {\n                this.interfaceState.closeAllSlots();\n            }\n        });\n\n        this._loginDate = new Date();\n        this._lastAddress = (this._socket?.address() as AddressInfo)?.address || '127.0.0.1';\n\n        if (this.rights === Rights.ADMIN) {\n            this.sendCommandList(actionHookMap.player_command as PlayerCommandActionHook[]);\n        }\n        this.outgoingPackets.resetAllClientConfigs();\n\n        await this.actionPipeline.call('player_init', { player: this });\n\n        activeWorld.spawnWorldItems(this);\n\n        if (!this.metadata.customMap) {\n            this.chunkChanged(playerChunk);\n        }\n\n        this.outgoingPackets.flushQueue();\n        logger.info(`${this.username}:${this.worldIndex} has logged in.`);\n    }\n\n    public logout(): void {\n        if (!this.active) {\n            return;\n        }\n\n        if (this.position.level > 3) {\n            this.position.level = 0;\n        }\n\n        if (this.quadtreeKey) {\n            activeWorld.playerTree.remove(this.quadtreeKey);\n        } else {\n            // TODO (Jameskmonger) remove this log if it isn't a problem state\n            logger.warn(`Player ${this.username} has no quadtree key on logout.`);\n        }\n\n        this.save();\n\n        this.destroy();\n\n        this.actionsCancelled.complete();\n        this.walkingQueue.movementEvent.complete();\n        this.walkingQueue.movementQueued.complete();\n        this.actionPipeline.shutdown();\n        this.outgoingPackets.logout();\n        this.instance = null;\n        activeWorld.chunkManager.getChunkForWorldPosition(this.position).removePlayer(this);\n        activeWorld.deregisterPlayer(this);\n\n        logger.info(`${this.username} has logged out.`);\n    }\n\n    public save(): void {\n        savePlayerData(this);\n    }\n\n    public privateMessageReceived(fromPlayer: Player, messageBytes: number[]): void {\n        this.outgoingPackets.sendPrivateMessage(this.privateMessageIndex++, fromPlayer, messageBytes);\n    }\n\n    public addFriend(friendName: string): boolean {\n        if (!playerExists(friendName)) {\n            return false;\n        }\n\n        friendName = friendName.toLowerCase();\n        this.friendsList.push(friendName);\n        return true;\n    }\n\n    public removeFriend(friendName: string): boolean {\n        friendName = friendName.toLowerCase();\n        const index = this.friendsList.findIndex(friend => friend === friendName);\n        if (index === -1) {\n            return false;\n        }\n\n        this.friendsList.splice(index, 1);\n        return true;\n    }\n\n    public addIgnoredPlayer(playerName: string): boolean {\n        if (!playerExists(playerName)) {\n            return false;\n        }\n\n        playerName = playerName.toLowerCase();\n\n        const index = this.ignoreList.findIndex(ignoredPlayer => ignoredPlayer === playerName);\n\n        if (index !== -1) {\n            return false;\n        }\n\n        // @TODO emit event to friend service watcher\n        this.ignoreList.push(playerName);\n        return true;\n    }\n\n    public removeIgnoredPlayer(playerName: string): boolean {\n        playerName = playerName.toLowerCase();\n        const index = this.ignoreList.findIndex(ignoredPlayer => ignoredPlayer === playerName);\n        if (index === -1) {\n            return false;\n        }\n\n        // @TODO emit event to friend service watcher\n        this.ignoreList.splice(index, 1);\n        return true;\n    }\n    public onNpcKill(npc: Npc) {\n        console.log('killed npc');\n    }\n    /**\n     * Should be fired whenever the player's chunk changes. This will fire off chunk updates for all chunks not\n     * already tracked by the player - all the new chunks that are coming into view.\n     * @param chunk The player's new active map chunk.\n     */\n    public chunkChanged(chunk: Chunk): void {\n        const nearbyChunks = activeWorld.chunkManager.getSurroundingChunks(chunk);\n        if (this._nearbyChunks.length === 0) {\n            this.sendChunkUpdates(nearbyChunks);\n        } else {\n            const newChunks = nearbyChunks.filter(c1 => this._nearbyChunks.findIndex(c2 => c1.equals(c2)) === -1);\n            this.sendChunkUpdates(newChunks);\n        }\n        this._nearbyChunks = nearbyChunks;\n    }\n\n    public async tick(): Promise<void> {\n        super.tick();\n\n        return new Promise<void>(resolve => {\n            this.walkingQueue.process();\n\n            if (this.updateFlags.mapRegionUpdateRequired) {\n                if (this.position.x >= 6400) {\n                    // Custom map drawing area is anywhere x >= 6400 on the map\n                    if (this.metadata.customMap) {\n                        this.outgoingPackets.constructMapRegion(this.metadata.customMap);\n                    } else {\n                        logger.warn(`Player ${this.username} is in custom map area but has no custom map set.`);\n                    }\n                } else {\n                    this.outgoingPackets.updateCurrentMapChunk();\n                }\n            }\n\n            resolve();\n        });\n    }\n\n    public async update(): Promise<void> {\n        await Promise.all([this.playerUpdateTask.execute(), this.npcUpdateTask.execute()]);\n    }\n\n    public async reset(): Promise<void> {\n        return new Promise<void>(resolve => {\n            this.updateFlags.reset();\n\n            this.outgoingPackets.flushQueue();\n\n            if (this.metadata.updateChunk) {\n                const { newChunk, oldChunk } = this.metadata.updateChunk;\n                oldChunk.removePlayer(this);\n                newChunk.addPlayer(this);\n                this.chunkChanged(newChunk);\n                this.metadata.updateChunk = undefined;\n            }\n\n            if (this.metadata.teleporting) {\n                this.metadata.teleporting = undefined;\n            }\n\n            resolve();\n        });\n    }\n\n    /**\n     * Fetches the player's number of quest points based off of their completed quests.\n     */\n    public getQuestPoints(): number {\n        let questPoints = 0;\n\n        if (this.quests && this.quests.length !== 0) {\n            this.quests.filter(quest => quest.complete).forEach(quest => (questPoints += questMap[quest.questId]?.points || 0));\n        }\n\n        return questPoints;\n    }\n\n    /**\n     * Fetches a player's quest progression details.\n     * @param questId The ID of the quest to find the player's status on.\n     */\n    public getQuest(questId: string): PlayerQuest {\n        let playerQuest = this.quests.find(quest => quest.questId === questId);\n        if (!playerQuest) {\n            playerQuest = new PlayerQuest(questId);\n            this.quests.push(playerQuest);\n        }\n\n        return playerQuest;\n    }\n\n    /**\n     * Checks if the player has unlocked the required stage of a quest\n     * @param questId The ID of the quest to find the player's status on.\n     * @param minimumStage The minimum quest stage required, defaults to completed\n     * @return boolean if the player has reached the required stage, if the quest does not exist it defaults to true\n     */\n    public hasQuestRequirement(questId: string, minimumStage: QuestKey = 'complete'): boolean {\n        if (!questMap[questId]) {\n            logger.warn(`Quest data not found for ${questId}`);\n            return true;\n        }\n        let playerQuest = this.quests.find(quest => quest.questId === questId);\n        if (!playerQuest) {\n            playerQuest = new PlayerQuest(questId);\n            this.quests.push(playerQuest);\n        }\n        return playerQuest.progress === minimumStage || playerQuest.progress >= minimumStage;\n    }\n\n    /**\n     * Sets a player's quest progress to the specified value.\n     * @param questId The ID of the quest to set the progress of.\n     * @param progress The progress to set the quest to.\n     */\n    public setQuestProgress(questId: string, progress: QuestKey): void {\n        const questData = findQuest(questId);\n\n        if (!questData) {\n            logger.warn(`Quest data not found for ${questId}`);\n            return;\n        }\n\n        let playerQuest = this.quests.find(quest => quest.questId === questId);\n        if (!playerQuest) {\n            playerQuest = new PlayerQuest(questId);\n            this.quests.push(playerQuest);\n        }\n\n        if (playerQuest.progress === 0 && !playerQuest.complete) {\n            playerQuest.progress = progress;\n            this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: colors.yellow });\n        } else if (!playerQuest.complete && progress === 'complete') {\n            playerQuest.complete = true;\n            playerQuest.progress = 'complete';\n            this.outgoingPackets.updateClientConfig(widgetScripts.questPoints, questData.points + this.getQuestPoints());\n            this.modifyWidget(widgets.questReward, { childId: 2, text: `You have completed ${questData.name}!` });\n            this.modifyWidget(widgets.questReward, {\n                childId: 8,\n                text: `${questData.points} Quest Point${questData.points > 1 ? 's' : ''}`,\n            });\n\n            for (let i = 0; i < 5; i++) {\n                if (i >= questData.onComplete.questCompleteWidget.rewardText.length) {\n                    this.modifyWidget(widgets.questReward, { childId: 9 + i, text: '' });\n                } else {\n                    this.modifyWidget(widgets.questReward, {\n                        childId: 9 + i,\n                        text: questData.onComplete.questCompleteWidget.rewardText[i],\n                    });\n                }\n            }\n\n            if (questData.onComplete.questCompleteWidget.itemId) {\n                const cacheItemData = filestore.configStore.itemStore.getItem(questData.onComplete.questCompleteWidget.itemId);\n\n                if (cacheItemData && cacheItemData.model2d.widgetModel) {\n                    this.outgoingPackets.updateWidgetModel1(widgets.questReward, 3, cacheItemData.model2d.widgetModel);\n                }\n            } else if (questData.onComplete.questCompleteWidget.modelId) {\n                this.outgoingPackets.updateWidgetModel1(widgets.questReward, 3, questData.onComplete.questCompleteWidget.modelId);\n            }\n\n            this.outgoingPackets.setWidgetModelRotationAndZoom(\n                widgets.questReward,\n                3,\n                questData.onComplete.questCompleteWidget.modelRotationX || 0,\n                questData.onComplete.questCompleteWidget.modelRotationY || 0,\n                questData.onComplete.questCompleteWidget.modelZoom || 0,\n            );\n\n            this.interfaceState.openWidget(widgets.questReward, {\n                slot: 'screen',\n                multi: false,\n            });\n\n            this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: colors.green });\n\n            if (questData.onComplete.giveRewards) {\n                questData.onComplete.giveRewards(this);\n            }\n        } else {\n            playerQuest.progress = progress;\n        }\n    }\n\n    /**\n     * Modifies the specified widget using the provided options.\n     * @param widgetId The widget id of the widget to modify.\n     * @param options The options with which to modify the widget.\n     */\n    public modifyWidget(widgetId: number, options: { childId?: number; text?: string; hidden?: boolean; textColor?: number }): void {\n        const { childId, text, hidden, textColor } = options;\n\n        if (childId !== undefined) {\n            if (text !== undefined) {\n                this.outgoingPackets.updateWidgetString(widgetId, childId, text);\n            }\n            if (hidden !== undefined) {\n                this.outgoingPackets.toggleWidgetVisibility(widgetId, childId, hidden);\n            }\n            if (textColor !== undefined) {\n                const { r, g, b } = hexToRgb(textColor);\n                this.outgoingPackets.updateWidgetColor(widgetId, childId, rgbTo16Bit(r, g, b));\n            }\n        }\n    }\n\n    /**\n     * Sets the player's specified sidebar widget to the given widget id.\n     * @param sidebarId The sidebar to change.\n     * @param widgetId The widget to insert into the sidebar.\n     */\n    public setSidebarWidget(sidebarId: SidebarTab, widgetId: number | null): void {\n        this.outgoingPackets.sendTabWidget(sidebarId, widgetId || null);\n    }\n\n    /**\n     * Plays the given song for the player.\n     * @param songId The id of the song to play.\n     */\n    public playSong(songId: number): void {\n        const musicTrack = findMusicTrack(songId);\n        if (!musicTrack) {\n            logger.warn(`Music track not found for id ${songId}`);\n            return;\n        }\n\n        this.modifyWidget(widgets.musicPlayerTab, {\n            childId: 177,\n            text: musicTrack.songName,\n            textColor: colors.green,\n        });\n        this.savedMetadata['currentSongIdPlaying'] = songId;\n        this.outgoingPackets.playSong(songId);\n    }\n\n    /**\n     * Plays a sound for this specific player.\n     * @param soundId The id of the sound effect.\n     * @param volume The volume to play the sound at; defaults to 10 (max).\n     * @param delay The delay after which to play the sound; defaults to 0 (no delay).\n     */\n    public playSound(soundId: number, volume: number = 10, delay: number = 0): void {\n        this.outgoingPackets.playSound(soundId, volume, delay);\n    }\n\n    /**\n     * Sends a message to the player via the chat-box.\n     * @param messages The single message or array of lines to send to the player.\n     * @param showDialogue Whether or not to show the message in a \"Click to continue\" dialogue.\n     * @returns A Promise<void> that resolves when the player has clicked the \"click to continue\" button or\n     * after their chat messages have been sent.\n     */\n    public async sendMessage(messages: string | string[], showDialogue?: boolean): Promise<boolean>;\n\n    /**\n     * Sends a message to the player via the chat-box (and the debug console if specified).\n     * @param messages The single message or array of lines to send to the player.\n     * @param options A list of options to provide for sending the message - includes values for `dialogue` and `console`\n     * to enable sending the message as a dialogue message and/or adding the message to the debug console.\n     * @returns A Promise<void> that resolves when the player has clicked the \"click to continue\" button or\n     * after their chat messages have been sent.\n     */\n    public async sendMessage(messages: string | string[], options?: SendMessageOptions): Promise<boolean>;\n\n    public async sendMessage(messages: string | string[], options?: boolean | SendMessageOptions): Promise<boolean> {\n        if (!Array.isArray(messages)) {\n            messages = [messages];\n        }\n\n        let showDialogue = false;\n        let showInConsole = false;\n        if (options) {\n            if (typeof options === 'boolean') {\n                showDialogue = true;\n            } else {\n                showDialogue = options.dialogue || false;\n                showInConsole = options.console || false;\n            }\n        }\n\n        if (!showDialogue) {\n            messages.forEach(message => this.outgoingPackets.chatboxMessage(message));\n        } else {\n            for (let i = 0; i < messages.length; i++) {\n                messages[i] = messages[i]?.trim() || '';\n            }\n\n            return await dialogue([this], [text => (messages as string[]).join(' ')]);\n        }\n\n        if (showInConsole) {\n            messages.forEach(message => this.outgoingPackets.consoleMessage(message));\n        }\n\n        return true;\n    }\n\n    /**\n     * Instantly teleports the player to the specified location.\n     * @param newPosition The player's new position.\n     * @param updateRegion Whether or not to sync the player's map region with their client. Defaults to true.\n     */\n    public teleport(newPosition: Position, updateRegion: boolean = true): void {\n        this.walkingQueue.clear();\n        const originalPosition = this.position.copy();\n        this.metadata.lastPosition = originalPosition;\n        this.position = newPosition;\n        this.metadata.teleporting = true;\n\n        this.updateFlags.mapRegionUpdateRequired = updateRegion;\n        this.lastMapRegionUpdatePosition = newPosition;\n\n        const oldChunk = activeWorld.chunkManager.getChunkForWorldPosition(originalPosition);\n        const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(newPosition);\n\n        if (!oldChunk.equals(newChunk)) {\n            oldChunk.removePlayer(this);\n            newChunk.addPlayer(this);\n            this.metadata.updateChunk = { newChunk, oldChunk };\n\n            if (updateRegion) {\n                this.actionPipeline.call('region_change', regionChangeActionFactory(this, originalPosition, newPosition, true));\n            }\n        }\n    }\n\n    public canMove(): boolean {\n        if (this.metadata?.castingStationarySpell) {\n            return false;\n        }\n        return true;\n    }\n\n    public removeFirstItem(item: number | Item): number {\n        const slot = this.inventory.removeFirst(item);\n\n        if (slot === -1) {\n            return -1;\n        }\n\n        this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, slot, null);\n        return slot;\n    }\n\n    public hasCoins(amount: number): number {\n        return this.inventory.items.findIndex(item => item !== null && item.itemId === itemIds.coins && item.amount >= amount);\n    }\n\n    public removeItem(slot: number): void {\n        this.inventory.remove(slot);\n        this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, slot, null);\n    }\n\n    public giveItem(item: number | Item | string): boolean {\n        const addedItem = this.inventory.add(item);\n        if (addedItem === null) {\n            return false;\n        }\n\n        this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, addedItem.slot, addedItem.item);\n        return true;\n    }\n\n    public hasItemOnPerson(item: number | Item): boolean {\n        return this.hasItemInInventory(item) || this.isItemEquipped(item);\n    }\n\n    /**\n     * Updates the player's carry weight based off of their held items (inventory + equipment).\n     * @param force Whether or not to force send an updated carry weight to the game client.\n     */\n    public updateCarryWeight(force: boolean = false): void {\n        const oldWeight = this._carryWeight;\n        this._carryWeight = Math.round(this.inventory.weight() + this.equipment.weight());\n\n        if (oldWeight !== this._carryWeight || force) {\n            this.outgoingPackets.updateCarryWeight(this._carryWeight);\n        }\n    }\n\n    /**\n     * Updates a player's client settings based off of which setting button they've clicked.\n     * @param buttonId The ID of the setting button.\n     * @TODO refactor to better match the 400+ widget system\n     */\n    public settingChanged(buttonId: number): void {\n        const settingsMappings = {\n            0: { setting: 'runEnabled', value: !this.settings['runEnabled'] },\n            1: { setting: 'chatEffectsEnabled', value: !this.settings['chatEffectsEnabled'] },\n            2: { setting: 'splitPrivateChatEnabled', value: !this.settings['splitPrivateChatEnabled'] },\n            3: { setting: 'twoMouseButtonsEnabled', value: !this.settings['twoMouseButtonsEnabled'] },\n            4: { setting: 'acceptAidEnabled', value: !this.settings['acceptAidEnabled'] },\n            // 5 is house options\n            // 6 is unknown, might not even exist\n            7: { setting: 'screenBrightness', value: 1 },\n            8: { setting: 'screenBrightness', value: 2 },\n            9: { setting: 'screenBrightness', value: 3 },\n            10: { setting: 'screenBrightness', value: 4 },\n            11: { setting: 'musicVolume', value: 4 },\n            12: { setting: 'musicVolume', value: 3 },\n            13: { setting: 'musicVolume', value: 2 },\n            14: { setting: 'musicVolume', value: 1 },\n            15: { setting: 'musicVolume', value: 0 },\n            16: { setting: 'soundEffectVolume', value: 4 },\n            17: { setting: 'soundEffectVolume', value: 3 },\n            18: { setting: 'soundEffectVolume', value: 2 },\n            19: { setting: 'soundEffectVolume', value: 1 },\n            20: { setting: 'soundEffectVolume', value: 0 },\n            29: { setting: 'areaEffectVolume', value: 4 },\n            30: { setting: 'areaEffectVolume', value: 3 },\n            31: { setting: 'areaEffectVolume', value: 2 },\n            32: { setting: 'areaEffectVolume', value: 1 },\n            33: { setting: 'areaEffectVolume', value: 0 },\n            // 150: {setting: 'autoRetaliateEnabled', value: true},\n            // 151: {setting: 'autoRetaliateEnabled', value: false}\n        };\n\n        if (!settingsMappings[buttonId]) {\n            return;\n        }\n\n        const config = settingsMappings[buttonId];\n        this.settings[config.setting] = config.value;\n    }\n\n    /**\n     * Updates the player's combat bonuses based off of their equipped items.\n     */\n    public updateBonuses(): void {\n        this.clearBonuses();\n\n        for (const item of this._equipment.items) {\n            if (item === null) {\n                continue;\n            }\n\n            this.addBonuses(item);\n        }\n    }\n\n    public sendLogMessage(message: string, isConsole: boolean): void {\n        if (isConsole) {\n            this.outgoingPackets.consoleMessage(message);\n        } else {\n            this.outgoingPackets.chatboxMessage(message);\n        }\n    }\n\n    public sendCommandList(commands: PlayerCommandActionHook[]): void {\n        if (!commands || commands.length === 0) {\n            return;\n        }\n\n        for (const command of commands) {\n            let strCmd: string;\n            if (Array.isArray(command.commands)) {\n                strCmd = command.commands.join('|');\n            } else {\n                strCmd = command.commands;\n            }\n            let strHelp: string = '';\n            if (command.args) {\n                for (const arg of command.args) {\n                    if (arg.defaultValue !== undefined) {\n                        strHelp = `${strHelp} \\\\<${arg.name} = ${arg.defaultValue}>`;\n                    } else {\n                        strHelp = `${strHelp} \\\\<${arg.name}>`;\n                    }\n                }\n            }\n            this.outgoingPackets.sendConsoleCommand(strCmd, strHelp);\n        }\n    }\n\n    public isItemEquipped(item: number | Item | string): boolean {\n        if (typeof item === 'string') {\n            item = findItem(item)?.gameId || 0;\n            if (!item) {\n                return false;\n            }\n        }\n        return this._equipment.has(item);\n    }\n\n    public getEquippedItem(equipmentSlot: EquipmentSlot): Item | null {\n        return this.equipment.items[equipmentIndex(equipmentSlot)] || null;\n    }\n\n    /**\n     * Check if a player can equip an item\n     * @param item either an ItemDetails instance or the string id of the item to be checked\n     * @return {equipable: boolean, missingRequirements: string[]} equipable is false if for any reason the item can not\n     * be equipped, if it can not be equipped, a list of reasons are attached as the missingRequirements array\n     *\n     * defaults to equipable=true if the item string id does not exist\n     */\n    public canEquipItem(item: ItemDetails | string): { equipable: boolean; missingRequirements?: string[] } {\n        if (typeof item === 'string') {\n            item = itemMap[item];\n            if (!item) {\n                return { equipable: true };\n            }\n        }\n        const missingRequirements: string[] = [];\n        const requirements = item.equipmentData?.requirements;\n        if (!requirements) return { equipable: true };\n\n        missingRequirements.push(\n            ...Object.entries(requirements.skills || {})\n                .filter(([skill, level]) => !this.skills.hasLevel(skill as SkillName, level))\n                .map(([skill, level]) => `You need to be at least level ${level} ${skill} to equip this item.`),\n            ...Object.entries(requirements.quests || {})\n                .filter(([quest, stage]) => this.hasQuestRequirement(quest, stage))\n                .map(\n                    ([quest]) =>\n                        `You must progress further in the ${quest.replace(/^([a-z]+:)/gm, '').replace(/_/g, ' ')} quest to equip this item.`,\n                ),\n        );\n\n        return { equipable: missingRequirements.length === 0, missingRequirements: missingRequirements };\n    }\n\n    public equipItem(itemId: number, itemSlot: number, slot: EquipmentSlot | number): boolean {\n        const itemToEquip = getItemFromContainer(itemId, itemSlot, this.inventory);\n\n        if (!itemToEquip) {\n            // The specified item was not found in the specified slot.\n            return false;\n        }\n\n        let slotIndex: number;\n        if (typeof slot === 'number') {\n            slotIndex = slot;\n            slot = getEquipmentSlot(slotIndex);\n        } else {\n            slotIndex = equipmentIndex(slot);\n        }\n\n        const itemToUnequip = this.equipment.items[slotIndex];\n        let shouldUnequipOffHand = false;\n        let shouldUnequipMainHand = false;\n        const itemDetails = findItem(itemId);\n\n        if (!itemDetails || !itemDetails.equipmentData || !itemDetails.equipmentData.equipmentSlot) {\n            this.sendMessage(`Unable to equip item ${itemId}: Missing equipment data.`);\n            return false;\n        }\n\n        const equippable = this.canEquipItem(itemDetails);\n        if (!equippable.equipable) {\n            if (equippable.missingRequirements) {\n                equippable.missingRequirements.forEach(async s => this.sendMessage(s));\n            }\n            return false;\n        }\n\n        if (itemDetails && itemDetails.equipmentData) {\n            if (itemDetails.equipmentData.equipmentType === 'two_handed') {\n                shouldUnequipOffHand = true;\n            }\n\n            const mainHandEquipped = this.getEquippedItem('main_hand');\n\n            if (slot === 'off_hand' && mainHandEquipped) {\n                const mainHandItemData = findItem(mainHandEquipped.itemId);\n\n                if (mainHandItemData && mainHandItemData.equipmentData && mainHandItemData.equipmentData.equipmentType === 'two_handed') {\n                    shouldUnequipMainHand = true;\n                }\n            }\n        }\n\n        if (itemToUnequip) {\n            if (shouldUnequipOffHand && !this.unequipItem('off_hand', false)) {\n                return false;\n            }\n\n            if (shouldUnequipMainHand && !this.unequipItem('main_hand', false)) {\n                return false;\n            }\n\n            this.actionPipeline.call('equipment_change', this, itemToUnequip.itemId, 'UNEQUIP', slot);\n\n            this.equipment.remove(slotIndex, false);\n            this.inventory.remove(itemSlot, false);\n\n            this.equipment.set(slotIndex, itemToEquip);\n            this.inventory.set(itemSlot, itemToUnequip);\n        } else {\n            this.equipment.set(slotIndex, itemToEquip);\n            this.inventory.remove(itemSlot);\n\n            if (shouldUnequipOffHand) {\n                this.unequipItem('off_hand');\n            }\n\n            if (shouldUnequipMainHand) {\n                this.unequipItem('main_hand');\n            }\n        }\n\n        this.actionPipeline.call('equipment_change', this, itemId, 'equip', slot);\n        this.equipmentChanged();\n        return true;\n    }\n\n    public equipmentChanged(): void {\n        this.updateBonuses();\n\n        // @TODO change packets to only update modified container slots\n        this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory);\n        this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipment, this.equipment);\n\n        if (this.interfaceState.widgetOpen('screen', widgets.equipmentStats.widgetId)) {\n            this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipmentStats, this.equipment);\n            this.syncBonuses();\n        }\n\n        this.updateFlags.appearanceUpdateRequired = true;\n    }\n\n    public syncBonuses(): void {\n        [\n            { id: 108, text: 'Stab', value: this.bonuses.offensive.stab },\n            { id: 109, text: 'Slash', value: this.bonuses.offensive.slash },\n            { id: 110, text: 'Crush', value: this.bonuses.offensive.crush },\n            { id: 111, text: 'Magic', value: this.bonuses.offensive.magic },\n            { id: 112, text: 'Range', value: this.bonuses.offensive.ranged },\n            { id: 113, text: 'Stab', value: this.bonuses.defensive.stab },\n            { id: 114, text: 'Slash', value: this.bonuses.defensive.slash },\n            { id: 115, text: 'Crush', value: this.bonuses.defensive.crush },\n            { id: 116, text: 'Magic', value: this.bonuses.defensive.magic },\n            { id: 117, text: 'Range', value: this.bonuses.defensive.ranged },\n            { id: 119, text: 'Strength', value: this.bonuses.skill.strength },\n            { id: 120, text: 'Prayer', value: this.bonuses.skill.prayer },\n        ].forEach(bonus =>\n            this.modifyWidget(widgets.equipmentStats.widgetId, {\n                childId: bonus.id,\n                text: `${bonus.text}: ${(bonus.value || 0) > 0 ? `+${bonus.value}` : bonus.value}`,\n            }),\n        );\n    }\n\n    public unequipItem(slot: EquipmentSlot | number, updateRequired: boolean = true): boolean {\n        const inventorySlot = this.inventory.getFirstOpenSlot();\n        if (inventorySlot === -1) {\n            this.sendMessage(`You don't have enough free space to do that.`);\n            return false;\n        }\n\n        let slotIndex: number;\n        if (typeof slot === 'number') {\n            slotIndex = slot;\n            slot = getEquipmentSlot(slotIndex);\n        } else {\n            slotIndex = equipmentIndex(slot);\n        }\n\n        const itemInSlot = this.equipment.items[slotIndex];\n\n        if (!itemInSlot) {\n            return true;\n        }\n\n        this.actionPipeline.call('equipment_change', this, itemInSlot.itemId, 'unequip', slot);\n\n        this.equipment.remove(slotIndex);\n        this.inventory.set(inventorySlot, itemInSlot);\n        if (updateRequired) {\n            this.equipmentChanged();\n        }\n        return true;\n    }\n\n    /**\n     * Transform's the player's appearance into the specified NPC.\n     * @param npc The NPC to copy the appearance of, or null to reset.\n     */\n    public transformInto(npc: NpcDetails | string | number | null): void {\n        if (!npc) {\n            delete this.savedMetadata.npcTransformation;\n            this.updateFlags.appearanceUpdateRequired = true;\n            return;\n        }\n\n        if (typeof npc !== 'number') {\n            if (typeof npc === 'string') {\n                if (npc.indexOf(':') !== -1) {\n                    npc = npcIdMap[npc];\n                } else {\n                    npc = parseInt(npc, 10);\n                }\n            } else {\n                npc = npc.gameId;\n            }\n        }\n\n        if (!npc) {\n            logger.error(`NPC not found.`);\n            return;\n        }\n\n        this.savedMetadata.npcTransformation = npc;\n        this.updateFlags.appearanceUpdateRequired = true;\n    }\n\n    /**\n     * Returns the morphed NPC details for a specific player based on his client settings\n     * @param originalNpc\n     */\n    public getMorphedNpcDetails(originalNpc: Npc) {\n        if (!originalNpc.childrenIds) {\n            return null;\n        }\n\n        let morphIndex: number;\n        if (originalNpc.varbitId !== -1) {\n            morphIndex = getVarbitMorphIndex(originalNpc.varbitId, this.metadata.configs);\n        } else if (originalNpc.settingId !== -1) {\n            morphIndex =\n                this.metadata.configs && this.metadata.configs[originalNpc.settingId] ? this.metadata.configs[originalNpc.settingId] : 0;\n        } else {\n            logger.warn(\n                `Tried to fetch a child NPC index, but but no varbitId or settingId were found in the NPC details. NPC: ${originalNpc.id}, childrenIDs: ${originalNpc.childrenIds}`,\n            );\n            return null;\n        }\n\n        return findNpc(originalNpc.childrenIds[morphIndex]);\n    }\n\n    public equals(player: Player): boolean {\n        return this.worldIndex === player.worldIndex && this.username === player.username && this.clientUuid === player.clientUuid;\n    }\n\n    private inventoryUpdated(event: ContainerUpdateEvent): void {\n        if (event.type === 'CLEAR_ALL') {\n            this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory);\n        } else if (event.type === 'ADD') {\n            if (event.slot !== undefined && event.item !== undefined) {\n                this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, event.slot, event.item);\n            } else {\n                logger.error(`Inventory update event was missing slot or item.`, event);\n            }\n        }\n        this.updateCarryWeight();\n    }\n\n    /**\n     * Sends chunk updates to notify the client of added & removed location objects\n     * @param chunks The chunks to update.\n     */\n    private sendChunkUpdates(chunks: Chunk[]): void {\n        const instance = this.instance;\n\n        if (!instance) {\n            logger.error(`Player ${this.username} tried to send chunk updates without an instance.`);\n            return;\n        }\n\n        chunks.forEach(chunk => {\n            this.outgoingPackets.clearChunk(chunk);\n\n            const chunkUpdateItems: ChunkUpdateItem[] = [];\n\n            const chunkModifications = instance.getInstancedChunk(chunk.position.x, chunk.position.y, chunk.position.level) || null;\n            const personalChunkModifications =\n                this.personalInstance?.getInstancedChunk(chunk.position.x, chunk.position.y, chunk.position.level) || null;\n\n            this.findChunkUpdates(chunkModifications?.mods, chunkUpdateItems);\n            this.findChunkUpdates(personalChunkModifications?.mods, chunkUpdateItems);\n\n            if (chunkUpdateItems.length !== 0) {\n                this.outgoingPackets.updateChunk(chunk, chunkUpdateItems);\n            }\n        });\n    }\n\n    private findChunkUpdates(chunkMods: Map<string, TileModifications>, chunkUpdateItems: ChunkUpdateItem[]): void {\n        if (!chunkMods) {\n            return;\n        }\n\n        Array.from(chunkMods.values()).forEach(worldMods => {\n            worldMods.hiddenObjects?.forEach(object => chunkUpdateItems.push({ object, type: 'REMOVE' }));\n\n            worldMods.spawnedObjects?.forEach(object => chunkUpdateItems.push({ object, type: 'ADD' }));\n\n            worldMods.worldItems?.forEach(worldItem => {\n                if (!worldItem.owner || worldItem.owner.equals(this)) {\n                    chunkUpdateItems.push({ worldItem, type: 'ADD' });\n                }\n            });\n        });\n    }\n\n    /**\n     * Updates the player's quest tab progress.\n     */\n    private updateQuestTab(): void {\n        this.outgoingPackets.updateClientConfig(widgetScripts.questPoints, this.getQuestPoints());\n\n        if (!questMap) {\n            return;\n        }\n        Object.keys(questMap).forEach(questKey => {\n            const questData = questMap[questKey];\n            const playerQuest = this.quests.find(quest => quest.questId === questData.id);\n\n            let color: number;\n\n            if (playerQuest?.complete || playerQuest?.progress === 'complete') {\n                // Quest complete, regardless of progress\n                color = colors.green;\n            } else if ((playerQuest?.progress || 0) > 0) {\n                // Quest in progress, not yet complete but progress is greater than 0\n                color = colors.yellow;\n            } else {\n                // Everything else failed, so quest hasn't been started yet\n                color = colors.red;\n            }\n\n            this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: color });\n        });\n    }\n\n    /**\n     * Updates the player's music tab progress.\n     */\n    private updateMusicTab(): void {\n        if (!this.savedMetadata['currentSongIdPlaying']) {\n            this.savedMetadata['currentSongIdPlaying'] = findSongIdByRegionId(\n                activeWorld.chunkManager.getRegionIdForWorldPosition(this.position),\n            );\n        }\n\n        if (this.settings.musicPlayerMode === MusicPlayerMode.MANUAL) {\n            this.playSong(this.savedMetadata['currentSongIdPlaying']);\n        }\n\n        Object.keys(musicRegions).forEach(key => {\n            const musicData = musicRegions[key];\n            let color = colors.red;\n\n            if (this.musicTracks.includes(musicData.songId)) {\n                color = colors.green;\n            }\n\n            this.modifyWidget(widgets.musicPlayerTab, { childId: musicData.musicTabButtonId, textColor: color });\n        });\n    }\n\n    private addBonuses(item: Item): void {\n        const itemData = findItem(item.itemId);\n\n        if (!itemData || !itemData.equipmentData) {\n            return;\n        }\n\n        const offensiveBonuses = itemData.equipmentData.offensiveBonuses;\n        const defensiveBonuses = itemData.equipmentData.defensiveBonuses;\n        const skillBonuses = itemData.equipmentData.skillBonuses;\n\n        if (offensiveBonuses) {\n            ['speed', 'stab', 'slash', 'crush', 'magic', 'ranged'].forEach(\n                bonus => (this.bonuses.offensive[bonus] += !offensiveBonuses[bonus] ? 0 : offensiveBonuses[bonus]),\n            );\n        }\n\n        if (defensiveBonuses) {\n            ['stab', 'slash', 'crush', 'magic', 'ranged'].forEach(\n                bonus => (this.bonuses.defensive[bonus] += !defensiveBonuses[bonus] ? 0 : defensiveBonuses[bonus]),\n            );\n        }\n\n        if (skillBonuses) {\n            ['strength', 'prayer'].forEach(bonus => (this.bonuses.skill[bonus] += !skillBonuses[bonus] ? 0 : skillBonuses[bonus]));\n        }\n    }\n\n    private loadSaveData(): void {\n        const playerSave = loadPlayerSave(this.username);\n        const firstTimePlayer = playerSave === null;\n        this.firstTimePlayer = firstTimePlayer;\n\n        if (!firstTimePlayer) {\n            if (playerSave.savedMetadata) {\n                this.savedMetadata = playerSave.savedMetadata;\n            }\n\n            // Existing player logging in\n            this.position = new Position(playerSave.position.x, playerSave.position.y, playerSave.position.level);\n            if (playerSave.inventory && playerSave.inventory.length !== 0) {\n                this.inventory.setAll(playerSave.inventory);\n            }\n            if (playerSave.bank && playerSave.bank.length !== 0) {\n                this.bank.setAll(playerSave.bank);\n            }\n            if (playerSave.equipment && playerSave.equipment.length !== 0) {\n                this.equipment.setAll(playerSave.equipment);\n            }\n            if (playerSave.skills && playerSave.skills.length !== 0) {\n                this.skills.values = playerSave.skills;\n            }\n            this._appearance = playerSave.appearance;\n            this._settings = playerSave.settings;\n            this._rights = playerSave.rights || Rights.USER;\n\n            const lastLogin = playerSave.lastLogin?.date;\n            if (!lastLogin) {\n                this._loginDate = new Date();\n            } else {\n                this._loginDate = new Date(lastLogin);\n            }\n\n            if (playerSave.questList) {\n                this.quests = playerSave.questList;\n            }\n            if (playerSave.musicTracks) {\n                this.musicTracks = playerSave.musicTracks;\n            }\n            if (playerSave.achievements) {\n                this.achievements = playerSave.achievements;\n            }\n            if (playerSave.friendsList) {\n                this.friendsList = playerSave.friendsList;\n            }\n            if (playerSave.ignoreList) {\n                this.ignoreList = playerSave.ignoreList;\n            }\n\n            this._lastAddress = playerSave.lastLogin?.address || (this._socket?.address() as AddressInfo)?.address || '127.0.0.1';\n        } else {\n            // Brand new player logging in\n            this.position = new Position(3231, 3239);\n            this._appearance = defaultAppearance();\n            this._rights = Rights.USER;\n            this.savedMetadata = {\n                tutorialProgress: 0,\n                tutorialComplete: false,\n            };\n        }\n\n        if (!this._settings) {\n            this._settings = defaultSettings();\n        }\n    }\n\n    public set position(position: Position) {\n        super.position = position;\n\n        if (this.quadtreeKey !== null) {\n            activeWorld.playerTree.remove(this.quadtreeKey);\n        }\n\n        this.quadtreeKey = { x: position.x, y: position.y, actor: this };\n        activeWorld.playerTree.push(this.quadtreeKey);\n    }\n\n    public get position(): Position {\n        return super.position;\n    }\n\n    public get socket(): Socket {\n        return this._socket;\n    }\n\n    public get inCipher(): Isaac {\n        return this._inCipher;\n    }\n\n    public get outCipher(): Isaac {\n        return this._outCipher;\n    }\n\n    public get outgoingPackets(): OutboundPacketHandler {\n        return this._outgoingPackets;\n    }\n\n    public get loginDate(): Date {\n        return this._loginDate;\n    }\n\n    public get lastAddress(): string {\n        return this._lastAddress;\n    }\n\n    public get rights(): Rights {\n        return this._rights;\n    }\n\n    public get appearance(): Appearance {\n        return this._appearance;\n    }\n\n    public set appearance(value: Appearance) {\n        this._appearance = value;\n    }\n\n    public get equipment(): ItemContainer {\n        return this._equipment;\n    }\n\n    public get carryWeight(): number {\n        return this._carryWeight;\n    }\n\n    public get settings(): PlayerSettings {\n        return this._settings;\n    }\n\n    public get nearbyChunks(): Chunk[] {\n        return this._nearbyChunks;\n    }\n\n    public get instance(): WorldInstance {\n        return super.instance;\n    }\n\n    public set instance(value: WorldInstance | null) {\n        if (this.instance?.instanceId) {\n            this.instance.removePlayer(this);\n        }\n\n        if (value) {\n            value.addPlayer(this);\n        }\n\n        this._instance = value;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/private-messaging.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport enum PrivateChatMode {\n    PUBLIC = 0,\n    FRIENDS = 1,\n    OFF = 2,\n}\n\nexport class PrivateMessaging {\n    public static friendAdded(player: Player, friendName: string): void {\n        friendName = friendName.toLowerCase();\n        const friend = activeWorld?.findPlayer(friendName);\n    }\n\n    public static friendRemoved(player: Player, friendName: string): void {\n        friendName = friendName.toLowerCase();\n        const playerPrivateChatMode = player.settings.privateChatMode;\n        const playerUsername = player.username.toLowerCase();\n        if (playerPrivateChatMode !== PrivateChatMode.PUBLIC) {\n            const friend = activeWorld?.findPlayer(friendName);\n            if (friend && friend.friendsList.indexOf(playerUsername) !== -1) {\n                // Friend being removed is currently online - update their friends list if they have this player added\n                friend.outgoingPackets.updateFriendStatus(player.username, 0);\n            }\n        }\n    }\n\n    /**\n     * Updates a specific player's entire friends list.\n     * @param player The player to update.\n     */\n    public static updateFriendsList(player: Player): void {\n        const friends = player.friendsList;\n        if (friends && friends.length !== 0) {\n            const onlineFriends = activeWorld.playerList.filter(p => p && friends.indexOf(p.username.toLowerCase()) !== -1);\n\n            friends.forEach(friendName => {\n                const friend = onlineFriends.find(p => p.username.toLowerCase() === friendName);\n                if (!friend || friend.settings.privateChatMode === PrivateChatMode.OFF) {\n                    player.outgoingPackets.updateFriendStatus(friendName, 0);\n                } else {\n                    if (friend.settings.privateChatMode === PrivateChatMode.PUBLIC) {\n                        player.outgoingPackets.updateFriendStatus(friendName, 1);\n                    } else {\n                        const otherPlayerFriendsList = friend.friendsList;\n                        player.outgoingPackets.updateFriendStatus(\n                            friendName,\n                            otherPlayerFriendsList.indexOf(player.username.toLowerCase()) !== -1 ? 1 : 0,\n                        );\n                    }\n                }\n            });\n        }\n    }\n\n    /**\n     * Called when the provided player logs in or changes their private chat mode.\n     * @param player The player logging in.\n     * @param updating If the friends list status is being updated or set initially.\n     */\n    public static playerPrivateChatModeChanged(player: Player, updating: boolean = true): void {\n        const playerName = player.username.toLowerCase();\n        const playerPrivateChatMode: PrivateChatMode = player.settings.privateChatMode;\n        const playerFriendsList = player.friendsList || [];\n\n        if (playerPrivateChatMode !== PrivateChatMode.OFF || updating) {\n            const otherPlayers = activeWorld.playerList.filter(p => p && p.friendsList.indexOf(playerName) !== -1);\n            if (otherPlayers && otherPlayers.length !== 0) {\n                otherPlayers.forEach(otherPlayer => {\n                    let worldId = playerPrivateChatMode === PrivateChatMode.OFF ? 0 : 1;\n\n                    if (playerPrivateChatMode === PrivateChatMode.FRIENDS) {\n                        if (playerFriendsList.findIndex(playerName => playerName === otherPlayer.username.toLowerCase()) === -1) {\n                            worldId = 0;\n                        }\n                    }\n\n                    otherPlayer.outgoingPackets.updateFriendStatus(player.username, worldId);\n                });\n            }\n        }\n    }\n\n    public static playerLoggedIn(player: Player): void {\n        PrivateMessaging.playerPrivateChatModeChanged(player, false);\n        PrivateMessaging.updateFriendsList(player);\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/quest.ts",
    "content": "import type { QuestCompletion, QuestJournalHandler } from '@engine/config/quest-config';\n\nexport class Quest {\n    public id: string;\n    public questTabId: number;\n    public name: string;\n    public points: number;\n    public journalHandler: QuestJournalHandler;\n    public onComplete;\n\n    public constructor(options: {\n        id: string;\n        questTabId: number;\n        name: string;\n        points: number;\n        journalHandler: QuestJournalHandler;\n        onComplete: QuestCompletion;\n    }) {\n        this.id = options.id;\n        this.questTabId = options.questTabId;\n        this.name = options.name;\n        this.points = options.points;\n        this.journalHandler = options.journalHandler;\n        this.onComplete = options.onComplete;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/sync/actor-sync.ts",
    "content": "import type { ByteBuffer } from '@runejs/common';\n\nimport type { Packet } from '@engine/net/packet';\nimport { activeWorld } from '@engine/world';\nimport type { Npc } from '@engine/world/actor/npc';\nimport type { Position } from '@engine/world/position';\nimport type { QuadtreeKey } from '@engine/world/world';\nimport type { Actor } from '../../actor';\nimport { isNpc, isPlayer } from '../../util';\nimport type { Player } from '../player';\n\n/**\n * Handles the registration of nearby NPCs or Players for the specified player.\n */\nexport function registerNewActors(\n    packet: Packet,\n    player: Player,\n    trackedActors: Actor[],\n    nearbyActors: QuadtreeKey[],\n    registerActor: (actor: Actor) => void,\n): void {\n    if (trackedActors.length >= 255) {\n        return;\n    }\n\n    // We only want to send about 20 new actors at a time, to help save some memory and computing time\n    // Any remaining players or npcs will be automatically picked up by subsequent updates\n    let newActors: QuadtreeKey[] = nearbyActors.filter(m1 => !trackedActors.includes(m1.actor));\n    if (newActors.length > 50) {\n        // We also sort the list of players or npcs here by how close they are to the current player if there are more than 80, so we can render the nearest first\n        newActors = newActors\n            .sort((a, b) => player.position.distanceBetween(a.actor.position) - player.position.distanceBetween(b.actor.position))\n            .slice(0, 50);\n    }\n\n    for (const newActor of newActors) {\n        const nearbyActor = newActor.actor;\n\n        if (isPlayer(nearbyActor)) {\n            if (player.equals(nearbyActor)) {\n                // Other player is actually this player!\n                continue;\n            }\n\n            if (!activeWorld.playerOnline(nearbyActor)) {\n                // Other player is no longer in the game world\n                continue;\n            }\n        } else if (isNpc(nearbyActor)) {\n            if (!activeWorld.npcExists(nearbyActor)) {\n                // Npc is no longer in the game world\n                continue;\n            }\n        }\n\n        if (trackedActors.findIndex(m => m.equals(nearbyActor)) !== -1) {\n            // Npc or other player is already tracked by this player\n            continue;\n        }\n\n        if (!nearbyActor.position.withinViewDistance(player.position)) {\n            // Player or npc is still too far away to be worth rendering\n            // Also - values greater than 15 and less than -15 are too large, or too small, to be sent via 5 bits (max length of 32)\n            continue;\n        }\n\n        // Only 255 players or npcs are able to be rendered at a time\n        // To help performance, we limit it to 200 here\n        if (trackedActors.length >= 255) {\n            return;\n        }\n\n        registerActor(nearbyActor);\n    }\n}\n\n/**\n * Handles synchronization of nearby NPCs or Players for the specified player.\n */\nexport function syncTrackedActors(\n    packet: Packet,\n    playerPosition: Position,\n    appendUpdateMaskData: (actor: Actor) => void,\n    trackedActors: Actor[],\n    nearbyActors: QuadtreeKey[],\n): Actor[] {\n    packet.putBits(8, trackedActors.length); // Tracked actor count\n\n    if (trackedActors.length === 0) {\n        return [];\n    }\n\n    const existingTrackedActors: Actor[] = [];\n\n    for (let i = 0; i < trackedActors.length; i++) {\n        const trackedActor: Actor = trackedActors[i];\n        let exists = true;\n\n        if (isPlayer(trackedActor)) {\n            if (!activeWorld.playerOnline(trackedActor)) {\n                exists = false;\n            }\n        } else {\n            if (!activeWorld.npcExists(trackedActor as Npc)) {\n                exists = false;\n            }\n        }\n\n        if (\n            exists &&\n            nearbyActors.findIndex(m => m.actor.equals(trackedActor)) !== -1 &&\n            trackedActor.position.withinViewDistance(playerPosition) &&\n            !trackedActor.metadata.teleporting\n        ) {\n            appendMovement(trackedActor, packet);\n            appendUpdateMaskData(trackedActor);\n            existingTrackedActors.push(trackedActor);\n        } else {\n            // De-register the actor if they are no longer nearby\n            packet.putBits(1, 1);\n            packet.putBits(2, 3);\n        }\n    }\n\n    return existingTrackedActors;\n}\n\n/**\n * Appends movement data of a player or NPC to the specified synchronization packet.\n */\nexport function appendMovement(actor: Actor, packet: ByteBuffer): void {\n    if (actor.walkDirection !== -1) {\n        // Actor is walking/running\n        packet.putBits(1, 1); // Update required\n\n        if (actor.runDirection === -1) {\n            // Actor is walking\n            packet.putBits(2, 1); // Actor walking\n            packet.putBits(3, actor.walkDirection);\n        } else {\n            // Actor is running\n            packet.putBits(2, 2); // Actor running\n            packet.putBits(3, actor.walkDirection);\n            packet.putBits(3, actor.runDirection);\n        }\n\n        packet.putBits(1, actor.updateFlags.updateBlockRequired ? 1 : 0); // Whether or not an update flag block follows\n    } else {\n        // Did not move\n        if (actor.updateFlags.updateBlockRequired) {\n            packet.putBits(1, 1); // Update required\n            packet.putBits(2, 0); // Signify the player did not move\n        } else {\n            packet.putBits(1, 0); // No update required\n        }\n    }\n}\n\nexport abstract class SyncTask<T> {\n    public abstract execute(): Promise<T>;\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/sync/npc-sync-task.ts",
    "content": "import { ByteBuffer } from '@runejs/common';\n\nimport { Packet, PacketType } from '@engine/net/packet';\nimport { activeWorld } from '@engine/world';\nimport type { Npc } from '@engine/world/actor/npc';\nimport { isPlayer } from '@engine/world/actor/util';\nimport type { Player } from '../player';\nimport { SyncTask, registerNewActors, syncTrackedActors } from './actor-sync';\n\n/**\n * Handles the chonky npc synchronization packet for a specific player.\n */\nexport class NpcSyncTask extends SyncTask<void> {\n    private readonly player: Player;\n\n    public constructor(player: Player) {\n        super();\n        this.player = player;\n    }\n\n    public async execute(): Promise<void> {\n        return new Promise<void>(resolve => {\n            const npcUpdatePacket: Packet = new Packet(128, PacketType.DYNAMIC_LARGE);\n            npcUpdatePacket.openBitBuffer();\n\n            const updateMaskData = new ByteBuffer(5000);\n\n            const nearbyNpcs = activeWorld.npcTree\n                .colliding({\n                    x: this.player.position.x - 15,\n                    y: this.player.position.y - 15,\n                    width: 32,\n                    height: 32,\n                })\n                .filter(collision => {\n                    const npc = (collision?.actor as Npc) || null;\n                    return npc && npc.initialized && npc.instanceId === this.player.instance.instanceId;\n                });\n\n            this.player.trackedNpcs = syncTrackedActors(\n                npcUpdatePacket,\n                this.player.position,\n                actor => this.appendUpdateMaskData(actor as Npc, updateMaskData),\n                this.player.trackedNpcs,\n                nearbyNpcs,\n            ) as Npc[];\n\n            registerNewActors(npcUpdatePacket, this.player, this.player.trackedNpcs, nearbyNpcs, actor => {\n                const newNpc = actor as Npc;\n                const positionOffsetX = newNpc.position.x - this.player.position.x;\n                const positionOffsetY = newNpc.position.y - this.player.position.y;\n\n                // Add npc to this player's list of tracked npcs\n                this.player.trackedNpcs.push(newNpc);\n\n                // Notify the client of the new npc and their worldIndex\n                npcUpdatePacket.putBits(15, newNpc.worldIndex);\n                npcUpdatePacket.putBits(3, newNpc.faceDirection);\n                npcUpdatePacket.putBits(5, positionOffsetX); // World Position X axis offset relative to the player\n                npcUpdatePacket.putBits(5, positionOffsetY); // World Position Y axis offset relative to the player\n                npcUpdatePacket.putBits(1, newNpc.updateFlags.updateBlockRequired ? 1 : 0); // Update is required\n                npcUpdatePacket.putBits(1, 1); // Discard client walking queues\n                npcUpdatePacket.putBits(13, newNpc.id);\n\n                this.appendUpdateMaskData(newNpc, updateMaskData);\n            });\n\n            if (updateMaskData.writerIndex !== 0) {\n                npcUpdatePacket.putBits(15, 32767);\n                npcUpdatePacket.closeBitBuffer();\n\n                npcUpdatePacket.putBytes(updateMaskData.flipWriter());\n            } else {\n                // No npc updates were appended, so just end the packet here\n                npcUpdatePacket.closeBitBuffer();\n            }\n\n            this.player.outgoingPackets.queue(npcUpdatePacket, true);\n            resolve();\n        });\n    }\n\n    /**\n     * As of 2024-09-03 this has been modified to include an extra `short` if\n     * any updates are required. This extra `short` includes the `worldIndex`\n     * for this NPC, which helps the client figure out which NPC to apply the\n     * updates to.\n     *\n     * For the sake of efficiency, this `short` will only be added once, and it\n     * will always be added after the first updated value's data is processed.\n     *\n     * Make sure to keep your client updated so that it can handle it.\n     */\n    private appendUpdateMaskData(npc: Npc, updateMaskData: ByteBuffer): void {\n        const updateFlags = npc.updateFlags;\n        if (!updateFlags.updateBlockRequired) {\n            return;\n        }\n\n        let mask = 0;\n\n        if (updateFlags.damage !== null) {\n            mask |= 0x1;\n        }\n        if (updateFlags.appearanceUpdateRequired) {\n            mask |= 0x80;\n        }\n        if (updateFlags.faceActor !== null) {\n            mask |= 0x4;\n        }\n        if (updateFlags.chatMessages.length !== 0) {\n            mask |= 0x40;\n        }\n        if (updateFlags.facePosition !== null) {\n            mask |= 0x8;\n        }\n        if (updateFlags.animation) {\n            mask |= 0x10;\n        }\n\n        if (updateFlags.graphics) {\n            mask |= 0x20;\n        }\n\n        updateMaskData.put(mask, 'BYTE');\n\n        let alreadyPutWorldIndex = false;\n        const putWorldIndex = () => {\n            if (alreadyPutWorldIndex) {\n                return;\n            }\n            updateMaskData.put(npc.worldIndex, 'SHORT');\n            alreadyPutWorldIndex = true;\n        };\n\n        if (updateFlags.damage !== null) {\n            const damage = updateFlags.damage;\n            updateMaskData.put(damage.damageDealt);\n            updateMaskData.put(damage.damageType.valueOf());\n            updateMaskData.put(damage.remainingHitpoints);\n            updateMaskData.put(damage.maxHitpoints);\n\n            putWorldIndex();\n        }\n\n        if (updateFlags.faceActor !== null) {\n            const actor = updateFlags.faceActor;\n\n            if (actor === 'CLEAR') {\n                // Reset faced actor\n                updateMaskData.put(65535, 'SHORT');\n            } else {\n                let worldIndex = actor.worldIndex;\n\n                if (isPlayer(actor)) {\n                    // Client checks if index is less than 32768.\n                    // If it is, it looks for an NPC.\n                    // If it isn't, it looks for a player (subtracting 32768 to find the index).\n                    worldIndex += 32768 + 1;\n                }\n\n                updateMaskData.put(worldIndex, 'SHORT');\n            }\n\n            putWorldIndex();\n        }\n\n        if (updateFlags.chatMessages.length !== 0) {\n            const message = updateFlags.chatMessages[0];\n\n            if (message.message) {\n                updateMaskData.putString(message.message);\n            } else {\n                updateMaskData.putString('Undefined Message');\n            }\n\n            putWorldIndex();\n        }\n\n        if (updateFlags.appearanceUpdateRequired) {\n            updateMaskData.put(npc.id, 'SHORT');\n            putWorldIndex();\n        }\n\n        if (updateFlags.facePosition) {\n            const position = updateFlags.facePosition;\n            updateMaskData.put(position.x * 2 + 1, 'SHORT');\n            updateMaskData.put(position.y * 2 + 1, 'SHORT', 'LITTLE_ENDIAN');\n            putWorldIndex();\n        }\n\n        if (updateFlags.animation) {\n            const animation = updateFlags.animation;\n\n            if (animation === null || animation.id === -1) {\n                // Reset animation\n                updateMaskData.put(65535, 'SHORT');\n                updateMaskData.put(0);\n            } else {\n                const delay = updateFlags.animation.delay || 0;\n                updateMaskData.put(animation.id, 'SHORT');\n                updateMaskData.put(delay);\n            }\n            putWorldIndex();\n        }\n\n        if (updateFlags.graphics) {\n            const { id, delay = 0, height } = updateFlags.graphics;\n            updateMaskData.put(id, 'SHORT', 'LITTLE_ENDIAN');\n            updateMaskData.put((height << 16) | (delay & 0xffff), 'INT');\n            putWorldIndex();\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/player/sync/player-sync-task.ts",
    "content": "import { ByteBuffer } from '@runejs/common';\n\nimport { findItem, findNpc } from '@engine/config/config-handler';\nimport type { EquipmentSlot, EquipmentType, ItemDetails } from '@engine/config/item-config';\nimport { Packet, PacketType } from '@engine/net/packet';\nimport { stringToLong } from '@engine/util/strings';\nimport { activeWorld } from '@engine/world';\nimport type { UpdateFlags } from '@engine/world/actor/update-flags';\nimport { isPlayer } from '@engine/world/actor/util';\nimport type { Player } from '../player';\nimport { SyncTask, appendMovement, registerNewActors, syncTrackedActors } from './actor-sync';\n\n/**\n * Handles the chonky player synchronization packet.\n */\nexport class PlayerSyncTask extends SyncTask<void> {\n    private readonly player: Player;\n\n    public constructor(player: Player) {\n        super();\n        this.player = player;\n    }\n\n    public async execute(): Promise<void> {\n        return new Promise<void>(resolve => {\n            const updateFlags: UpdateFlags = this.player.updateFlags;\n            const playerUpdatePacket: Packet = new Packet(92, PacketType.DYNAMIC_LARGE);\n            playerUpdatePacket.openBitBuffer();\n\n            const updateMaskData = new ByteBuffer(5000);\n\n            if (updateFlags.mapRegionUpdateRequired || this.player.metadata.teleporting) {\n                playerUpdatePacket.putBits(1, 1); // Update Required\n                playerUpdatePacket.putBits(2, 3); // Map Region changed (movement type - 0=nomove, 1=walk, 2=run, 3=mapchange\n                playerUpdatePacket.putBits(1, this.player.metadata.teleporting ? 1 : 0); // Whether or not the client should discard the current walking queue (1 if teleporting, 0 if not)\n                playerUpdatePacket.putBits(2, this.player.position.level); // Player Height\n                playerUpdatePacket.putBits(1, updateFlags.updateBlockRequired ? 1 : 0); // Whether or not an update flag block follows\n                playerUpdatePacket.putBits(7, this.player.position.chunkLocalX); // Player Local Chunk X\n                playerUpdatePacket.putBits(7, this.player.position.chunkLocalY); // Player Local Chunk Y\n            } else {\n                appendMovement(this.player, playerUpdatePacket);\n            }\n\n            this.appendUpdateMaskData(this.player, updateMaskData, false);\n\n            let nearbyPlayers = activeWorld.playerTree\n                .colliding({\n                    x: this.player.position.x - 15,\n                    y: this.player.position.y - 15,\n                    width: 32,\n                    height: 32,\n                })\n                .filter(collision => collision?.actor && collision.actor.instance === this.player.instance);\n\n            if (nearbyPlayers.length > 200) {\n                nearbyPlayers = activeWorld.playerTree.colliding({\n                    x: this.player.position.x - 7,\n                    y: this.player.position.y - 7,\n                    width: 16,\n                    height: 16,\n                });\n            }\n\n            this.player.trackedPlayers = syncTrackedActors(\n                playerUpdatePacket,\n                this.player.position,\n                actor => this.appendUpdateMaskData(actor as Player, updateMaskData),\n                this.player.trackedPlayers,\n                nearbyPlayers,\n            ) as Player[];\n\n            registerNewActors(playerUpdatePacket, this.player, this.player.trackedPlayers, nearbyPlayers, actor => {\n                const newPlayer = actor as Player;\n                const positionOffsetX = newPlayer.position.x - this.player.position.x;\n                const positionOffsetY = newPlayer.position.y - this.player.position.y;\n\n                // Add other player to this player's list of tracked players\n                this.player.trackedPlayers.push(newPlayer);\n\n                // Notify the client of the new player and their worldIndex\n                playerUpdatePacket.putBits(11, newPlayer.worldIndex + 1);\n\n                playerUpdatePacket.putBits(5, positionOffsetX); // World Position X axis offset relative to the main player\n                playerUpdatePacket.putBits(5, positionOffsetY); // World Position Y axis offset relative to the main player\n                playerUpdatePacket.putBits(3, newPlayer.faceDirection);\n                playerUpdatePacket.putBits(1, 1); // Update is required\n                playerUpdatePacket.putBits(1, 1); // Discard client walking queues\n\n                this.appendUpdateMaskData(newPlayer, updateMaskData, true);\n            });\n\n            if (updateMaskData.writerIndex !== 0) {\n                playerUpdatePacket.putBits(11, 2047);\n                playerUpdatePacket.closeBitBuffer();\n\n                playerUpdatePacket.putBytes(updateMaskData.flipWriter());\n            } else {\n                // No player updates were appended, so just end the packet here\n                playerUpdatePacket.closeBitBuffer();\n            }\n\n            this.player.outgoingPackets.queue(playerUpdatePacket, true);\n            resolve();\n        });\n    }\n\n    private appendUpdateMaskData(player: Player, updateMaskData: ByteBuffer, forceUpdate?: boolean): void {\n        const updateFlags = player.updateFlags;\n\n        if (!updateFlags.updateBlockRequired && !forceUpdate) {\n            return;\n        }\n\n        let mask: number = 0;\n\n        if (updateFlags.damage !== null) {\n            mask |= 0x100;\n        }\n        if (updateFlags.appearanceUpdateRequired || forceUpdate) {\n            mask |= 0x20;\n        }\n        if (updateFlags.chatMessages.length !== 0) {\n            mask |= 0x8;\n        }\n        if (updateFlags.faceActor !== null) {\n            mask |= 0x4;\n        }\n        if (updateFlags.facePosition) {\n            mask |= 0x10;\n        }\n        if (updateFlags.graphics) {\n            mask |= 0x200;\n        }\n        if (updateFlags.animation !== undefined && updateFlags.animation !== null) {\n            mask |= 0x1;\n        }\n\n        if (mask >= 0x100) {\n            mask |= 0x2;\n            updateMaskData.put(mask & 0xff);\n            updateMaskData.put(mask >> 8);\n        } else {\n            updateMaskData.put(mask);\n        }\n\n        if (updateFlags.damage !== null) {\n            const damage = updateFlags.damage;\n            updateMaskData.put(damage.damageDealt);\n            updateMaskData.put(damage.damageType.valueOf());\n            updateMaskData.put(damage.remainingHitpoints);\n            updateMaskData.put(damage.maxHitpoints);\n        }\n\n        if (updateFlags.facePosition) {\n            const position = updateFlags.facePosition;\n            updateMaskData.put(position.x * 2 + 1, 'SHORT');\n            updateMaskData.put(position.y * 2 + 1, 'SHORT', 'LITTLE_ENDIAN');\n        }\n\n        if (updateFlags.animation !== undefined && updateFlags.animation !== null) {\n            const animation = updateFlags.animation;\n\n            if (animation === null || animation.id === -1) {\n                // Reset animation\n                updateMaskData.put(-1, 'SHORT', 'LITTLE_ENDIAN');\n                updateMaskData.put(0, 'BYTE');\n            } else {\n                const delay = animation.delay || 0;\n                updateMaskData.put(animation.id, 'SHORT', 'LITTLE_ENDIAN');\n                updateMaskData.put(delay, 'BYTE');\n            }\n        }\n\n        if (updateFlags.faceActor !== null) {\n            if (updateFlags.faceActor === 'CLEAR') {\n                // Reset faced actor\n                updateMaskData.put(65535, 'SHORT');\n            } else {\n                const actor = updateFlags.faceActor;\n                let worldIndex = actor.worldIndex;\n\n                if (isPlayer(actor)) {\n                    // Client checks if index is less than 32768.\n                    // If it is, it looks for an NPC.\n                    // If it isn't, it looks for a player (subtracting 32768 to find the index).\n                    worldIndex += 32768 + 1;\n                }\n\n                updateMaskData.put(worldIndex, 'SHORT');\n            }\n        }\n\n        if (updateFlags.chatMessages.length !== 0) {\n            const message = updateFlags.chatMessages[0];\n\n            if (!message.data) {\n                throw new Error('Chat message data is undefined');\n            }\n\n            updateMaskData.put(((message.color || 0 & 0xff) << 8) + (message.effects || 0 & 0xff), 'SHORT');\n            updateMaskData.put(player.rights.valueOf(), 'BYTE');\n            updateMaskData.put(message.data.length, 'BYTE');\n            for (let i = 0; i < message.data.length; i++) {\n                updateMaskData.put(message.data.readInt8(i), 'BYTE');\n            }\n        }\n\n        if (updateFlags.appearanceUpdateRequired || forceUpdate) {\n            const equipment = player.equipment;\n            const appearanceData = new ByteBuffer(500);\n            appearanceData.put(player.appearance.gender); // Gender\n            appearanceData.put(-1); // Skull Icon\n            appearanceData.put(-1); // Prayer Icon\n\n            if (player.savedMetadata.npcTransformation) {\n                appearanceData.put(65535, 'SHORT');\n                appearanceData.put(player.savedMetadata.npcTransformation, 'SHORT');\n            } else {\n                for (let i = 0; i < 4; i++) {\n                    const item = equipment.items[i];\n\n                    if (item) {\n                        appearanceData.put(0x200 + item.itemId, 'SHORT');\n                    } else {\n                        appearanceData.put(0);\n                    }\n                }\n\n                const torsoItem = player.getEquippedItem('torso');\n                let torsoItemData: ItemDetails | null = null;\n                if (torsoItem) {\n                    torsoItemData = findItem(torsoItem.itemId);\n                    appearanceData.put(0x200 + torsoItem.itemId, 'SHORT');\n                } else {\n                    appearanceData.put(0x100 + player.appearance.torso, 'SHORT');\n                }\n\n                const offHandItem = player.getEquippedItem('off_hand');\n                if (offHandItem) {\n                    appearanceData.put(0x200 + offHandItem.itemId, 'SHORT');\n                } else {\n                    appearanceData.put(0);\n                }\n\n                if (\n                    torsoItemData &&\n                    torsoItemData.equipmentData &&\n                    torsoItemData.equipmentData.equipmentType &&\n                    torsoItemData.equipmentData.equipmentType === 'full_top'\n                ) {\n                    appearanceData.put(0);\n                } else {\n                    appearanceData.put(0x100 + player.appearance.arms, 'SHORT');\n                }\n\n                this.appendBasicAppearanceItem(appearanceData, player, player.appearance.legs, 'legs');\n\n                const headItem = player.getEquippedItem('head');\n                let helmetType: EquipmentType | null = null;\n                let fullHelmet = false;\n\n                if (headItem) {\n                    const headItemData = findItem(headItem.itemId);\n\n                    if (headItemData && headItemData.equipmentData && headItemData.equipmentData.equipmentType) {\n                        helmetType = headItemData.equipmentData.equipmentType;\n\n                        if (helmetType === 'helmet') {\n                            fullHelmet = true;\n                        }\n                    }\n                }\n\n                if (!headItem || helmetType === 'hat') {\n                    appearanceData.put(0x100 + player.appearance.head, 'SHORT');\n                } else {\n                    appearanceData.put(0);\n                }\n\n                this.appendBasicAppearanceItem(appearanceData, player, player.appearance.hands, 'hands');\n                this.appendBasicAppearanceItem(appearanceData, player, player.appearance.feet, 'feet');\n\n                if (player.appearance.gender === 1 || fullHelmet) {\n                    appearanceData.put(0);\n                } else {\n                    appearanceData.put(0x100 + player.appearance.facialHair, 'SHORT');\n                }\n            }\n\n            [\n                player.appearance.hairColor,\n                player.appearance.torsoColor,\n                player.appearance.legColor,\n                player.appearance.feetColor,\n                player.appearance.skinColor,\n            ].forEach(color => appearanceData.put(color));\n\n            let animations = [\n                0x328, // stand\n                0x337, // stand turn\n                0x333, // walk\n                0x334, // turn 180\n                0x335, // turn 90\n                0x336, // turn 90 reverse\n                0x338, // run\n            ];\n\n            if (player.savedMetadata.npcTransformation) {\n                const npc = findNpc(player.savedMetadata.npcTransformation);\n                animations = [\n                    npc.animations?.stand || 0x328, // stand\n                    npc.animations?.turnAround || 0x337, // stand turn\n                    npc.animations?.walk || 0x333, // walk\n                    npc.animations?.turnAround || 0x334, // turn 180\n                    npc.animations?.turnRight || 0x335, // turn 90\n                    npc.animations?.turnLeft || 0x336, // turn 90 reverse\n                    npc.animations?.walk || 0x338, // run\n                ];\n            }\n\n            animations.forEach(animationId => appearanceData.put(animationId, 'SHORT'));\n\n            appearanceData.put(stringToLong(player.username), 'LONG'); // Username\n            appearanceData.put(player.skills.getCombatLevel()); // Combat Level\n            appearanceData.put(player.skills.getTotalLevel(), 'SHORT'); // Skill Level (Total Level)\n\n            const appearanceDataSize = appearanceData.writerIndex;\n\n            updateMaskData.put(appearanceDataSize);\n            updateMaskData.putBytes(appearanceData.flipWriter());\n        }\n\n        if (updateFlags.graphics) {\n            const delay = updateFlags.graphics.delay || 0;\n            updateMaskData.put(updateFlags.graphics.id, 'SHORT', 'LITTLE_ENDIAN');\n            updateMaskData.put((updateFlags.graphics.height << 16) | (delay & 0xffff), 'INT');\n        }\n    }\n\n    private appendBasicAppearanceItem(buffer: ByteBuffer, player: Player, appearanceInfo: number, equipmentSlot: EquipmentSlot): void {\n        const item = player.getEquippedItem(equipmentSlot);\n        if (item) {\n            buffer.put(0x200 + item.itemId, 'SHORT');\n        } else {\n            buffer.put(0x100 + appearanceInfo, 'SHORT');\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/prayer.ts",
    "content": "export class Prayer {\n    AnimationId: number;\n    SoundId: number;\n    ButtonId: number;\n}\n"
  },
  {
    "path": "src/engine/world/actor/skills.ts",
    "content": "import { QueueableTask } from '@engine/action/pipe/task/queueable-task';\nimport { startsWithVowel } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { gfxIds } from '@engine/world/config/gfx-ids';\nimport { serverConfig } from '@server/game/game-server';\nimport type { Actor } from './actor';\nimport { isPlayer } from './util';\n\nexport enum Skill {\n    ATTACK,\n    DEFENCE,\n    STRENGTH,\n    HITPOINTS,\n    RANGED,\n    PRAYER,\n    MAGIC,\n    COOKING,\n    WOODCUTTING,\n    FLETCHING,\n    FISHING,\n    FIREMAKING,\n    CRAFTING,\n    SMITHING,\n    MINING,\n    HERBLORE,\n    AGILITY,\n    THIEVING,\n    SLAYER,\n    FARMING,\n    RUNECRAFTING,\n    CONSTRUCTION = 22,\n}\n\nexport type SkillName =\n    | 'attack'\n    | 'defence'\n    | 'strength'\n    | 'hitpoints'\n    | 'ranged'\n    | 'prayer'\n    | 'magic'\n    | 'cooking'\n    | 'woodcutting'\n    | 'fletching'\n    | 'fishing'\n    | 'firemaking'\n    | 'crafting'\n    | 'smithing'\n    | 'mining'\n    | 'herblore'\n    | 'agility'\n    | 'thieving'\n    | 'slayer'\n    | 'farming'\n    | 'runecrafting'\n    | 'construction';\n\nexport interface SkillDetail {\n    readonly name: string;\n    readonly advancementWidgetId?: number;\n}\n\nexport const skillDetails: SkillDetail[] = [\n    { name: 'Attack', advancementWidgetId: 158 },\n    { name: 'Defence', advancementWidgetId: 161 },\n    { name: 'Strength', advancementWidgetId: 175 },\n    { name: 'Hitpoints', advancementWidgetId: 167 },\n    { name: 'Ranged', advancementWidgetId: 171 },\n    { name: 'Prayer', advancementWidgetId: 170 },\n    { name: 'Magic', advancementWidgetId: 168 },\n    { name: 'Cooking', advancementWidgetId: 159 },\n    { name: 'Woodcutting', advancementWidgetId: 177 },\n    { name: 'Fletching', advancementWidgetId: 165 },\n    { name: 'Fishing', advancementWidgetId: 164 },\n    { name: 'Firemaking', advancementWidgetId: 163 },\n    { name: 'Crafting', advancementWidgetId: 160 },\n    { name: 'Smithing', advancementWidgetId: 174 },\n    { name: 'Mining', advancementWidgetId: 169 },\n    { name: 'Herblore', advancementWidgetId: 166 },\n    { name: 'Agility', advancementWidgetId: 157 },\n    { name: 'Thieving', advancementWidgetId: 176 },\n    { name: 'Slayer', advancementWidgetId: 173 },\n    { name: 'Farming', advancementWidgetId: 162 },\n    { name: 'Runecrafting', advancementWidgetId: 172 },\n    null as unknown as SkillDetail, // (Jameskmonger) this is a placeholder\n    { name: 'Construction' },\n];\n\nexport interface SkillValue {\n    exp: number;\n    level: number;\n    modifiedLevel?: number;\n}\n\nexport class SkillShortcut {\n    public constructor(\n        private skills: Skills,\n        private skillName: SkillName,\n    ) {}\n\n    public addExp(exp: number): void {\n        this.skills.addExp(this.skillName, exp);\n    }\n\n    public set level(value: number) {\n        this.skills.setLevel(this.skillName, value);\n    }\n\n    public get level(): number {\n        return this.skills.getLevel(this.skillName);\n    }\n\n    public set exp(value: number) {\n        this.skills.setExp(this.skillName, value);\n    }\n\n    public get exp(): number {\n        return this.skills.get(this.skillName).exp;\n    }\n\n    public get levelForExp(): number {\n        return this.skills.getLevelForExp(this.exp);\n    }\n}\n\ntype SkillShortcutMap = {\n    [skillName in SkillName]: SkillShortcut;\n};\n\nclass SkillShortcuts implements SkillShortcutMap {\n    agility: SkillShortcut;\n    attack: SkillShortcut;\n    construction: SkillShortcut;\n    cooking: SkillShortcut;\n    crafting: SkillShortcut;\n    defence: SkillShortcut;\n    farming: SkillShortcut;\n    firemaking: SkillShortcut;\n    fishing: SkillShortcut;\n    fletching: SkillShortcut;\n    herblore: SkillShortcut;\n    hitpoints: SkillShortcut;\n    magic: SkillShortcut;\n    mining: SkillShortcut;\n    prayer: SkillShortcut;\n    ranged: SkillShortcut;\n    runecrafting: SkillShortcut;\n    slayer: SkillShortcut;\n    smithing: SkillShortcut;\n    strength: SkillShortcut;\n    thieving: SkillShortcut;\n    woodcutting: SkillShortcut;\n}\n\nexport class Skills extends SkillShortcuts {\n    private static EXPERIENCE_LOOKUP_TABLE: number[] = [\n        0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, 1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, 5018, 5624, 6291, 7028,\n        7842, 8740, 9730, 10824, 12031, 13363, 14833, 16456, 18247, 20224, 22406, 24815, 27473, 30408, 33648, 37224, 41171, 45529, 50339,\n        55649, 61512, 67983, 75127, 83014, 91721, 101333, 111945, 123660, 136594, 150872, 166636, 184040, 203254, 224466, 247886, 273742,\n        302288, 333804, 368599, 407015, 449428, 496254, 547953, 605032, 668051, 737627, 814445, 899257, 992895, 1096278, 1210421, 1336443,\n        1475581, 1629200, 1798808, 1986068, 2192818, 2421087, 2673114, 2951373, 3258594, 3597792, 3972294, 4385776, 4842295, 5346332,\n        5902831, 6517253, 7195629, 7944614, 8771558, 9684577, 10692629, 11805606, 13034431,\n    ];\n\n    private static MAXIMUM_EXPERIENCE: number = 200000000;\n    private static MINIMUM_LEVEL: number = 0;\n    private static MAXIMUM_LEVEL: number = 99;\n    private static MAXIMUM_INDEX: number = Skills.EXPERIENCE_LOOKUP_TABLE.length - 1;\n\n    private _values: SkillValue[];\n\n    public constructor(\n        private actor: Actor,\n        values?: SkillValue[],\n    ) {\n        super();\n\n        Object.keys(Skill)\n            .map(skillName => skillName.toLowerCase())\n            .forEach(skillName => (this[skillName] = new SkillShortcut(this, skillName as SkillName)));\n\n        if (values) {\n            this._values = values;\n        } else {\n            this._values = this.defaultValues();\n        }\n    }\n\n    private static confine(value: number, min: number, max: number): number {\n        return Math.max(min, Math.min(value, max));\n    }\n\n    public setHitpoints(hitpoints: number): void {\n        this.setLevel(Skill.HITPOINTS, hitpoints);\n    }\n\n    public getTotalLevel(): number {\n        return this._values.map(skillValue => skillValue.level).reduce((accumulator, currentValue) => accumulator + currentValue);\n    }\n\n    public getCombatLevel(): number {\n        const combatLevel = (this.defence.level + this.hitpoints.level + Math.floor(this.prayer.level / 2)) * 0.25;\n        const melee = (this.attack.level + this.strength.level) * 0.325;\n        const ranger = this.ranged.level * 0.4875;\n        const mage = this.magic.level * 0.4875;\n        return combatLevel + Math.max(melee, Math.max(ranger, mage));\n    }\n\n    public getLevel(skill: number | SkillName, ignoreLevelModifications: boolean = false): number {\n        const s = this.get(skill);\n        return s.modifiedLevel !== undefined && !ignoreLevelModifications ? s.modifiedLevel : s.level;\n    }\n\n    public hasLevel(skill: number | SkillName, level: number, ignoreLevelModifications: boolean = false): boolean {\n        return this.getLevel(skill, ignoreLevelModifications) >= level;\n    }\n\n    public getLevelForExp(exp: number, index: number | undefined = undefined): number {\n        const start = Skills.confine(index || Skills.MAXIMUM_INDEX, Skills.MINIMUM_LEVEL, Skills.MAXIMUM_INDEX);\n        for (let level = start; level >= 1; level--) {\n            const requirement = Skills.EXPERIENCE_LOOKUP_TABLE[level];\n            if (exp >= requirement) {\n                return level + 1;\n            }\n        }\n        return 1;\n    }\n\n    public getExpForLevel(level: number): number {\n        const index = Skills.confine(level - 1, Skills.MINIMUM_LEVEL, Skills.MAXIMUM_INDEX);\n        return Skills.EXPERIENCE_LOOKUP_TABLE[index];\n    }\n\n    public addExp(skill: number | SkillName, exp: number): void {\n        const currentExp = this.get(skill).exp;\n        const currentLevel = this.getLevelForExp(currentExp);\n        let finalExp = currentExp + exp * serverConfig.expRate;\n        if (finalExp > Skills.MAXIMUM_EXPERIENCE) {\n            finalExp = Skills.MAXIMUM_EXPERIENCE;\n        }\n\n        const finalLevel = this.getLevelForExp(finalExp);\n\n        this.setExp(skill, finalExp);\n\n        if (isPlayer(this.actor)) {\n            this.actor.outgoingPackets.updateSkill(this.getSkillId(skill), finalLevel, finalExp);\n        }\n\n        if (currentLevel !== finalLevel) {\n            this.setLevel(skill, finalLevel);\n\n            if (isPlayer(this.actor)) {\n                const achievementDetails = skillDetails[this.getSkillId(skill)];\n                if (!achievementDetails) {\n                    return;\n                }\n\n                /**\n                 * Note: For skills like casting magic teleports, if the\n                 * level-up dialogue is shown too quickly, it interrupts the\n                 * task processing queue - the dialogue gets shown but the\n                 * teleport doesn't always finish.\n                 *\n                 * Queueing the level-up dialog 1 tick later will result in the\n                 * dialogue being shown after other events get processed on the\n                 * tick that the xp drop occurred.\n                 */\n                this.actor.enqueueBaseTask(\n                    new QueueableTask(\n                        [],\n                        this.actor,\n                        () => {\n                            (this.actor as Player).sendMessage(\n                                `Congratulations, you just advanced a ` + `${achievementDetails.name.toLowerCase()} level.`,\n                            );\n                            this.showLevelUpDialogue(skill, finalLevel);\n                            return {\n                                callbackResult: false,\n                                shouldContinueLooping: false,\n                            };\n                        },\n                        null,\n                        null,\n                    ),\n                );\n            }\n        }\n    }\n\n    public showLevelUpDialogue(skill: number | SkillName, level: number): void {\n        if (!isPlayer(this.actor)) {\n            return;\n        }\n\n        const player = this.actor;\n        const achievementDetails = skillDetails[this.getSkillId(skill)];\n        const widgetId = achievementDetails.advancementWidgetId;\n\n        if (!widgetId) {\n            return;\n        }\n\n        const skillName = achievementDetails.name.toLowerCase();\n\n        player.modifyWidget(widgetId, {\n            childId: 0,\n            text:\n                `<col=000080>Congratulations, you just advanced ${startsWithVowel(skillName) ? 'an' : 'a'} ` + `${skillName} level.</col>`,\n        });\n        player.modifyWidget(widgetId, {\n            childId: 1,\n            text: `Your ${skillName} level is now ${level}.`,\n        });\n\n        player.interfaceState.openWidget(widgetId, {\n            slot: 'chatbox',\n            multi: true,\n        });\n\n        player.playGraphics({ id: gfxIds.levelUpFireworks, delay: 0, height: 125 });\n        // @TODO sounds\n    }\n\n    public getSkillId(skill: number | SkillName): number {\n        if (typeof skill === 'number') {\n            return skill;\n        } else {\n            const skillName = skill.toString().toUpperCase();\n            return Skill[skillName].valueOf();\n        }\n    }\n\n    public get(skill: number | SkillName): SkillValue {\n        if (typeof skill === 'number') {\n            return this._values[skill];\n        } else {\n            const skillName = skill.toString().toUpperCase();\n            return this._values[Skill[skillName].valueOf()];\n        }\n    }\n\n    public setExp(skill: number | SkillName, exp: number): void {\n        const skillId = this.getSkillId(skill);\n        this._values[skillId].exp = exp;\n    }\n\n    public setLevel(skill: number | SkillName, level: number): void {\n        const skillId = this.getSkillId(skill);\n        this._values[skillId].level = level;\n    }\n\n    private defaultValues(): SkillValue[] {\n        const values: SkillValue[] = [];\n        skillDetails.forEach(() => values.push({ exp: 0, level: 1 }));\n        values[Skill.HITPOINTS] = { exp: 1154, level: 10 };\n        return values;\n    }\n\n    public get values(): SkillValue[] {\n        return this._values;\n    }\n\n    public set values(value: SkillValue[]) {\n        this._values = value;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/update-flags.ts",
    "content": "import type { Position } from '../position';\nimport type { Actor } from './actor';\n\n/**\n * A specific chat message.\n */\nexport interface ChatMessage {\n    color?: number;\n    effects?: number;\n    data?: Buffer;\n    message?: string;\n}\n\n/**\n * A graphic.\n */\nexport interface Graphic {\n    id: number;\n    height: number;\n    delay?: number;\n}\n\n/**\n * An animation.\n */\nexport interface Animation {\n    id: number;\n    delay?: number;\n}\n\nexport enum DamageType {\n    NO_DAMAGE = 0,\n    DAMAGE = 1,\n    POISON = 2,\n}\n\n/**\n * An instance of damage.\n */\nexport interface Damage {\n    damageDealt: number;\n    damageType: DamageType;\n    remainingHitpoints: number;\n    maxHitpoints: number;\n}\n\n/**\n * Various actor updating flags.\n */\nexport class UpdateFlags {\n    private _mapRegionUpdateRequired: boolean;\n    private _appearanceUpdateRequired: boolean;\n    private _chatMessages: ChatMessage[];\n    private _facePosition: Position | null;\n    private _faceActor: Actor | 'CLEAR' | null;\n    private _graphics: Graphic | null;\n    private _animation: Animation | null;\n    private _damage: Damage | null;\n\n    public constructor() {\n        this._chatMessages = [];\n        this.reset();\n    }\n\n    public reset(): void {\n        this._mapRegionUpdateRequired = false;\n        this._appearanceUpdateRequired = false;\n        this._facePosition = null;\n        this._faceActor = null;\n        this._graphics = null;\n        this._animation = null;\n        this._damage = null;\n\n        if (this._chatMessages.length !== 0) {\n            this._chatMessages.shift();\n        }\n    }\n\n    public addDamage(amount: number, type: DamageType, remainingHitpoints: number, maxHitpoints: number): void {\n        this.damage = {\n            damageDealt: amount,\n            damageType: type,\n            remainingHitpoints,\n            maxHitpoints,\n        };\n    }\n\n    public addChatMessage(chatMessage: ChatMessage): void {\n        if (this._chatMessages.length > 4) {\n            return;\n        }\n\n        this._chatMessages.push(chatMessage);\n    }\n\n    /**\n     * Determines if any update is required. When false will short circuit\n     * the entire update process.\n     */\n    public get updateBlockRequired(): boolean {\n        return (\n            this._appearanceUpdateRequired ||\n            this._chatMessages.length !== 0 ||\n            this._facePosition !== null ||\n            this._graphics !== null ||\n            this._animation !== null ||\n            this._faceActor !== null ||\n            this._damage !== null\n        );\n    }\n\n    public get mapRegionUpdateRequired(): boolean {\n        return this._mapRegionUpdateRequired;\n    }\n\n    public set mapRegionUpdateRequired(value: boolean) {\n        this._mapRegionUpdateRequired = value;\n    }\n\n    public get appearanceUpdateRequired(): boolean {\n        return this._appearanceUpdateRequired;\n    }\n\n    public set appearanceUpdateRequired(value: boolean) {\n        this._appearanceUpdateRequired = value;\n    }\n\n    public get chatMessages(): ChatMessage[] {\n        return this._chatMessages;\n    }\n\n    public set chatMessages(value: ChatMessage[]) {\n        this._chatMessages = value;\n    }\n\n    public get facePosition(): Position | null {\n        return this._facePosition;\n    }\n\n    public set facePosition(value: Position | null) {\n        this._facePosition = value;\n    }\n\n    public get faceActor(): Actor | 'CLEAR' | null {\n        return this._faceActor;\n    }\n\n    public set faceActor(value: Actor | 'CLEAR' | null) {\n        this._faceActor = value;\n    }\n\n    public get graphics(): Graphic | null {\n        return this._graphics;\n    }\n\n    public set graphics(value: Graphic | null) {\n        this._graphics = value;\n    }\n\n    public get animation(): Animation | null {\n        return this._animation;\n    }\n\n    public set animation(value: Animation | null) {\n        this._animation = value;\n    }\n\n    public get damage(): Damage | null {\n        return this._damage;\n    }\n\n    public set damage(value: Damage | null) {\n        this._damage = value;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/actor/util.ts",
    "content": "import type { Actor } from './actor';\nimport type { Npc } from './npc';\nimport type { Player } from './player/player';\n\nexport const isPlayer = (actor: Actor): actor is Player => actor.type === 'player';\nexport const isNpc = (actor: Actor): actor is Npc => actor.type === 'npc';\n"
  },
  {
    "path": "src/engine/world/actor/walking-queue.ts",
    "content": "import { regionChangeActionFactory } from '@engine/action/pipe/region-change.action';\nimport { activeWorld } from '@engine/world';\nimport { isNpc, isPlayer } from '@engine/world/actor/util';\nimport { Subject } from 'rxjs';\nimport { Position } from '../position';\nimport type { Actor } from './actor';\n\n/**\n * Controls an actor's movement.\n */\nexport class WalkingQueue {\n    public readonly movementQueued = new Subject<Position>();\n    public readonly movementEvent = new Subject<Position>();\n    public readonly movementQueued$ = this.movementQueued.asObservable();\n    public readonly movementEvent$ = this.movementEvent.asObservable();\n\n    private queue: Position[];\n    private _valid: boolean;\n\n    public constructor(private readonly actor: Actor) {\n        this.queue = [];\n        this._valid = false;\n    }\n\n    public moving(): boolean {\n        return this.queue.length !== 0;\n    }\n\n    public clear(): void {\n        this.queue = [];\n    }\n\n    public getLastPosition(): Position {\n        if (this.queue.length === 0) {\n            return this.actor.position;\n        } else {\n            return this.queue[this.queue.length - 1];\n        }\n    }\n\n    public add(x: number, y: number, positionMetadata?: { [key: string]: any }): void {\n        let lastPosition = this.getLastPosition();\n\n        let lastX = lastPosition.x;\n        let lastY = lastPosition.y;\n        let diffX = x - lastX;\n        let diffY = y - lastY;\n\n        const stepsBetween = Math.max(Math.abs(diffX), Math.abs(diffY));\n\n        for (let i = 0; i < stepsBetween; i++) {\n            if (diffX !== 0) {\n                diffX += diffX < 0 ? 1 : -1;\n            }\n\n            if (diffY !== 0) {\n                diffY += diffY < 0 ? 1 : -1;\n            }\n\n            lastX = x - diffX;\n            lastY = y - diffY;\n\n            const newPosition = new Position(lastX, lastY, this.actor.position.level);\n\n            if (this.actor.pathfinding.canMoveTo(lastPosition, newPosition)) {\n                lastPosition = newPosition;\n                newPosition.metadata = {\n                    ...newPosition.metadata,\n                    ...positionMetadata,\n                };\n                this.queue.push(newPosition);\n                this.movementQueued.next(newPosition);\n            } else {\n                this.valid = false;\n                break;\n            }\n        }\n\n        if (lastX !== x || (lastY !== y && this.valid)) {\n            const newPosition = new Position(x, y, this.actor.position.level);\n\n            if (this.actor.pathfinding.canMoveTo(lastPosition, newPosition)) {\n                newPosition.metadata = {\n                    ...newPosition.metadata,\n                    ...positionMetadata,\n                };\n                this.queue.push(newPosition);\n                this.movementQueued.next(newPosition);\n            } else {\n                this.valid = false;\n            }\n        }\n    }\n\n    public moveIfAble(xDiff: number, yDiff: number): boolean {\n        const position = this.actor.position;\n        const newPosition = new Position(position.x + xDiff, position.y + yDiff, position.level);\n\n        if (this.actor.pathfinding.canMoveTo(position, newPosition)) {\n            this.clear();\n            this.valid = true;\n            this.add(newPosition.x, newPosition.y, { ignoreWidgets: true });\n            return true;\n        }\n\n        return false;\n    }\n\n    public resetDirections(): void {\n        this.actor.walkDirection = -1;\n        this.actor.runDirection = -1;\n    }\n\n    public calculateDirection(diffX: number, diffY: number): number {\n        if (diffX < 0) {\n            if (diffY < 0) {\n                return 5;\n            } else if (diffY > 0) {\n                return 0;\n            } else {\n                return 3;\n            }\n        } else if (diffX > 0) {\n            if (diffY < 0) {\n                return 7;\n            } else if (diffY > 0) {\n                return 2;\n            } else {\n                return 4;\n            }\n        } else {\n            if (diffY < 0) {\n                return 6;\n            } else if (diffY > 0) {\n                return 1;\n            } else {\n                return -1;\n            }\n        }\n    }\n\n    public process(): void {\n        if (this.actor.busy || this.queue.length === 0 || !this.valid) {\n            this.resetDirections();\n            return;\n        }\n\n        const walkPosition = this.queue.shift();\n\n        if (!walkPosition) {\n            return;\n        }\n\n        if (this.actor.metadata.faceActorClearedByWalking === undefined || this.actor.metadata.faceActorClearedByWalking) {\n            this.actor.clearFaceActor();\n        }\n\n        const originalPosition = this.actor.position;\n\n        if (this.actor.pathfinding.canMoveTo(originalPosition, walkPosition)) {\n            const oldChunk = activeWorld.chunkManager.getChunkForWorldPosition(originalPosition);\n            const lastMapRegionUpdatePosition = this.actor.lastMapRegionUpdatePosition;\n\n            const walkDiffX = walkPosition.x - originalPosition.x;\n            const walkDiffY = walkPosition.y - originalPosition.y;\n            const walkDir = this.calculateDirection(walkDiffX, walkDiffY);\n\n            if (walkDir === -1) {\n                this.resetDirections();\n                return;\n            }\n\n            this.actor.lastMovementPosition = this.actor.position;\n            this.actor.position = walkPosition;\n\n            let runDir = -1;\n\n            // @TODO npc running\n            if (isPlayer(this.actor)) {\n                if (this.actor.settings.runEnabled && this.queue.length !== 0) {\n                    const runPosition = this.queue.shift();\n\n                    if (!runPosition) {\n                        return;\n                    }\n\n                    if (this.actor.pathfinding.canMoveTo(walkPosition, runPosition)) {\n                        const runDiffX = runPosition.x - walkPosition.x;\n                        const runDiffY = runPosition.y - walkPosition.y;\n                        runDir = this.calculateDirection(runDiffX, runDiffY);\n\n                        if (runDir != -1) {\n                            this.actor.lastMovementPosition = this.actor.position;\n                            this.actor.position = runPosition;\n                        }\n                    } else {\n                        this.resetDirections();\n                        this.clear();\n                    }\n                }\n            }\n\n            this.actor.walkDirection = walkDir;\n            this.actor.runDirection = runDir;\n\n            if (runDir !== -1) {\n                this.actor.faceDirection = runDir;\n            } else {\n                this.actor.faceDirection = walkDir;\n            }\n\n            const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.actor.position);\n\n            this.movementEvent.next(this.actor.position);\n\n            if (isPlayer(this.actor)) {\n                const mapDiffX = this.actor.position.x - lastMapRegionUpdatePosition.chunkX * 8;\n                const mapDiffY = this.actor.position.y - lastMapRegionUpdatePosition.chunkY * 8;\n                if (mapDiffX < 16 || mapDiffX > 87 || mapDiffY < 16 || mapDiffY > 87) {\n                    this.actor.updateFlags.mapRegionUpdateRequired = true;\n                    this.actor.lastMapRegionUpdatePosition = this.actor.position;\n                }\n            }\n\n            if (!oldChunk.equals(newChunk)) {\n                if (isPlayer(this.actor)) {\n                    this.actor.metadata.updateChunk = { newChunk, oldChunk };\n\n                    this.actor.actionPipeline.call(\n                        'region_change',\n                        regionChangeActionFactory(this.actor, originalPosition, this.actor.position),\n                    );\n                } else if (isNpc(this.actor)) {\n                    oldChunk.removeNpc(this.actor);\n                    newChunk.addNpc(this.actor);\n                }\n            }\n        } else {\n            this.resetDirections();\n            this.clear();\n        }\n    }\n\n    get valid(): boolean {\n        return this._valid;\n    }\n\n    set valid(value: boolean) {\n        this._valid = value;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/config/animation-ids.ts",
    "content": "export const animationIds = {\n    reset: -1,\n    walk: 819,\n    idle: 808,\n    run: 824,\n    milkCow: 2305,\n    lightingFire: 733,\n    homeTeleportDraw: 4847,\n    homeTeleportSit: 4850,\n    homeTeleportPullOutAndReadBook: 4853,\n    homeTeleportReadBookAndGlowCircle: 4855,\n    homeTeleport: 4857,\n    fillContainerWithWater: 832,\n    shearSheep: 893,\n    spinSpinningWheel: 894,\n    cry: 860,\n    climbLadder: 828,\n    smelting: 899,\n    death: 2304,\n    eat: 829,\n    buryBones: 827,\n    herblore: {\n        make_potion: 363,\n        pestle_and_mortar: 364,\n    },\n    combat: {\n        punch: 422,\n        kick: 423,\n        stab: 412,\n        slash: 451,\n        armBlock: 424,\n    },\n    fadeOut: 3541,\n    fadeIn: 2115,\n    transparent: 15,\n    teleport: 714,\n};\n"
  },
  {
    "path": "src/engine/world/config/examine-data.ts",
    "content": "import { readFileSync } from 'fs';\nimport { logger } from '@runejs/common';\nimport { JSON_SCHEMA, load } from 'js-yaml';\n\ninterface Examine {\n    id: number;\n    examine: string;\n}\n\nexport class ExamineCache {\n    private readonly items: Map<number, Examine>;\n    private readonly npcs: Map<number, Examine>;\n    private readonly objects: Map<number, Examine>;\n\n    public constructor() {\n        logger.info('Parsing examine data...');\n        this.items = parseData('data/config/examine-item-data.yaml');\n        this.npcs = new Map<number, Examine>();\n        this.objects = new Map<number, Examine>();\n    }\n\n    public getItem(id: number): string | null {\n        const examine = this.items.get(id);\n        return examine ? examine.examine : null;\n    }\n\n    public getNpc(id: number): string | null {\n        const examine = this.npcs.get(id);\n        return examine ? examine.examine : null;\n    }\n\n    public getObject(id: number): string | null {\n        const examine = this.objects.get(id);\n        return examine ? examine.examine : null;\n    }\n}\n\nfunction parseData(fileName: string): Map<number, Examine> {\n    const examineMap: Map<number, Examine> = new Map<number, Examine>();\n    try {\n        const examineItems = load(readFileSync(fileName, 'utf8'), { schema: JSON_SCHEMA }) as Examine[];\n\n        if (!examineItems || examineItems.length === 0) {\n            throw new Error('Unable to read examine data.');\n        }\n\n        for (const item of examineItems) {\n            examineMap.set(item.id, item);\n        }\n    } catch (error) {\n        logger.error('Error parsing examine data: ' + error);\n    }\n\n    return examineMap;\n}\n"
  },
  {
    "path": "src/engine/world/config/gfx-ids.ts",
    "content": "export const gfxIds = {\n    homeTeleportDraw: 800,\n    homeTeleportFullDrawnCircle: 801,\n    homeTeleportPullOutBook: 802,\n    homeTeleportCircleGlow: 803,\n    homeTeleport: 804,\n    levelUpFireworks: 199,\n    teleport: 111,\n};\n"
  },
  {
    "path": "src/engine/world/config/harvest-tool.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\n\nexport interface HarvestTool {\n    itemId: number;\n    level: number;\n    animation: number;\n}\n\nexport enum Pickaxe {\n    BRONZE,\n    IRON,\n    STEEL,\n    MITHRIL,\n    ADAMANT,\n    RUNE,\n}\n\nexport enum Axe {\n    BRONZE,\n    IRON,\n    STEEL,\n    MITHRIL,\n    ADAMANT,\n    RUNE,\n    DRAGON,\n}\n\nconst Pickaxes: HarvestTool[] = [\n    { itemId: 1265, level: 1, animation: 625 },\n    { itemId: 1267, level: 1, animation: 626 },\n    { itemId: 1269, level: 6, animation: 627 },\n    { itemId: 1273, level: 21, animation: 629 },\n    { itemId: 1271, level: 31, animation: 628 },\n    { itemId: 1275, level: 41, animation: 624 },\n];\n\nconst Axes: HarvestTool[] = [\n    { itemId: 1351, level: 1, animation: 879 },\n    { itemId: 1349, level: 1, animation: 877 },\n    { itemId: 1353, level: 6, animation: 875 },\n    { itemId: 1355, level: 21, animation: 871 },\n    { itemId: 1357, level: 31, animation: 869 },\n    { itemId: 1359, level: 41, animation: 867 },\n    { itemId: 6739, level: 61, animation: 2846 },\n];\n\n/**\n * Checks the players inventory and equipment for pickaxe\n * @param player\n * @return the highest level pickage the player can use, or null if theres none found\n */\nexport function getBestPickaxe(player: Player): HarvestTool | null {\n    for (let i = Pickaxes.length - 1; i >= 0; i--) {\n        if (player.skills.hasLevel(Skill.MINING, Pickaxes[i].level)) {\n            if (player.hasItemOnPerson(Pickaxes[i].itemId)) {\n                return Pickaxes[i];\n            }\n        }\n    }\n    return null;\n}\n/**\n * Checks the players inventory and equipment for axe\n * @param player\n * @return the highest level axe the player can use, or null if theres none found\n */\nexport function getBestAxe(player: Player): HarvestTool | null {\n    for (let i = Axes.length - 1; i >= 0; i--) {\n        if (player.skills.hasLevel(Skill.WOODCUTTING, Axes[i].level)) {\n            if (player.hasItemOnPerson(Axes[i].itemId)) {\n                return Axes[i];\n            }\n        }\n    }\n    return null;\n}\n\nexport function getPickaxe(pickaxe: Pickaxe): HarvestTool {\n    return Pickaxes[pickaxe];\n}\n\nexport function getAxe(axe: Axe): HarvestTool {\n    return Axes[axe];\n}\n"
  },
  {
    "path": "src/engine/world/config/harvestable-object.ts",
    "content": "import { randomBetween } from '@engine/util/num';\nimport { objectIds } from '@engine/world/config/object-ids';\n\ninterface WeightedItem {\n    itemConfigId: string;\n    weight: number;\n}\n\nexport interface IHarvestable {\n    objects: Map<number, number>;\n    items: string | WeightedItem[];\n    level: number;\n    experience: number;\n    respawnLow: number;\n    respawnHigh: number;\n    baseChance: number;\n    break: number;\n}\n\n// Object maps work with key is mineable object, value is empty ore\nconst CLAY_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.default.clay.map(tree => [tree.default, tree.empty])] as [\n    number,\n    number,\n][]);\n\nconst COPPER_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.default.copper.map(tree => [tree.default, tree.empty]),\n] as [number, number][]);\n\nconst TIN_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.default.tin.map(tree => [tree.default, tree.empty])] as [\n    number,\n    number,\n][]);\n\nconst IRON_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.default.iron.map(tree => [tree.default, tree.empty])] as [\n    number,\n    number,\n][]);\n\nconst COAL_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.default.coal.map(tree => [tree.default, tree.empty])] as [\n    number,\n    number,\n][]);\n\nconst SILVER_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.default.silver.map(tree => [tree.default, tree.empty]),\n] as [number, number][]);\n\nconst GOLD_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.default.gold.map(tree => [tree.default, tree.empty])] as [\n    number,\n    number,\n][]);\n\nconst MITHRIL_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.default.mithril.map(tree => [tree.default, tree.empty]),\n] as [number, number][]);\n\nconst ADAMANT_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.default.adamant.map(tree => [tree.default, tree.empty]),\n] as [number, number][]);\n\nconst RUNITE_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.default.runite.map(tree => [tree.default, tree.empty]),\n] as [number, number][]);\n\nconst NORMAL_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.tree.normal.map(tree => [tree.default, tree.stump]),\n    ...objectIds.tree.dead.map(tree => [tree.default, tree.stump]),\n] as [number, number][]);\n\nconst ACHEY_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.archey.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst OAK_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.oak.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst WILLOW_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.willow.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst TEAK_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.teak.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst DRAMEN_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.dramen.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst MAPLE_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.maple.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst HOLLOW_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.hollow.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst MAHOGANY_OBJECTS: Map<number, number> = new Map<number, number>([\n    ...objectIds.tree.mahogany.map(tree => [tree.default, tree.stump]),\n] as [number, number][]);\n\nconst YEW_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.yew.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nconst MAGIC_OBJECTS: Map<number, number> = new Map<number, number>([...objectIds.tree.magic.map(tree => [tree.default, tree.stump])] as [\n    number,\n    number,\n][]);\n\nexport enum Ore {\n    CLAY,\n    COPPER,\n    TIN,\n    IRON,\n    COAL,\n    SILVER,\n    GOLD,\n    MITHIL,\n    ADAMANT,\n    RUNITE,\n    RUNE_ESS,\n    GEM,\n}\n\nexport enum Tree {\n    NORMAL,\n    ACHEY,\n    OAK,\n    WILLOW,\n    TEAK,\n    MAPLE,\n    MAHOGANY,\n    YEW,\n    MAGIC,\n    HOLLOW,\n    DRAMEN,\n}\n\nexport function selectWeightedItem(items: WeightedItem[]): string {\n    const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);\n    let random = randomBetween(1, totalWeight);\n\n    for (const item of items) {\n        random -= item.weight;\n        if (random <= 0) {\n            return item.itemConfigId;\n        }\n    }\n\n    return items[0].itemConfigId; // Fallback to first item\n}\n\nconst Ores: IHarvestable[] = [\n    {\n        objects: CLAY_OBJECTS,\n        items: 'rs:clay',\n        level: 1,\n        experience: 5.0,\n        respawnLow: 5,\n        respawnHigh: 10,\n        baseChance: 70,\n        break: 100,\n    },\n    {\n        objects: COPPER_OBJECTS,\n        items: 'rs:copper_ore',\n        level: 1,\n        experience: 17.5,\n        respawnLow: 10,\n        respawnHigh: 20,\n        baseChance: 70,\n        break: 100,\n    },\n    {\n        objects: TIN_OBJECTS,\n        items: 'rs:tin_ore',\n        level: 1,\n        experience: 17.5,\n        respawnLow: 10,\n        respawnHigh: 20,\n        baseChance: 70,\n        break: 100,\n    },\n    {\n        objects: IRON_OBJECTS,\n        items: 'rs:iron_ore',\n        level: 15,\n        experience: 35.0,\n        respawnLow: 9,\n        respawnHigh: 9,\n        baseChance: 0.0085,\n        break: 100,\n    },\n    {\n        objects: COAL_OBJECTS,\n        items: 'rs:coal',\n        level: 30,\n        experience: 50.0,\n        respawnLow: 20,\n        respawnHigh: 30,\n        baseChance: 50,\n        break: 100,\n    },\n    {\n        objects: SILVER_OBJECTS,\n        items: 'rs:silver_ore',\n        level: 20,\n        experience: 40.0,\n        respawnLow: 30,\n        respawnHigh: 40,\n        baseChance: 40,\n        break: 100,\n    },\n    {\n        objects: GOLD_OBJECTS,\n        items: 'rs:gold_ore',\n        level: 40,\n        experience: 65.0,\n        respawnLow: 50,\n        respawnHigh: 70,\n        baseChance: 30,\n        break: 100,\n    },\n    {\n        objects: MITHRIL_OBJECTS,\n        items: 'rs:mithril_ore',\n        level: 55,\n        experience: 65.0,\n        respawnLow: 90,\n        respawnHigh: 120,\n        baseChance: 20,\n        break: 100,\n    },\n    {\n        objects: ADAMANT_OBJECTS,\n        items: 'rs:adamantite_ore',\n        level: 70,\n        experience: 95.0,\n        respawnLow: 200,\n        respawnHigh: 400,\n        baseChance: 0,\n        break: 100,\n    },\n    {\n        objects: RUNITE_OBJECTS,\n        items: 'rs:runite_ore',\n        level: 85,\n        experience: 125.0,\n        respawnLow: 1200,\n        respawnHigh: 1200,\n        baseChance: -10,\n        break: 100,\n    },\n    {\n        objects: new Map<number, number>([[2111, 450]]),\n        items: [\n            { itemConfigId: 'rs:uncut_opal', weight: 60 }, // 60/128\n            { itemConfigId: 'rs:uncut_jade', weight: 30 }, // 30/128\n            { itemConfigId: 'rs:uncut_red_topaz', weight: 15 }, // 15/128\n            { itemConfigId: 'rs:uncut_sapphire', weight: 9 }, // 9/128\n            { itemConfigId: 'rs:uncut_emerald', weight: 5 }, // 5/128\n            { itemConfigId: 'rs:uncut_ruby', weight: 5 }, // 5/128\n            { itemConfigId: 'rs:uncut_diamond', weight: 4 }, // 4/128\n        ],\n        level: 40,\n        experience: 65.0,\n        respawnLow: 200,\n        respawnHigh: 400,\n        baseChance: 28, // Base success chance at level 40\n        break: 100, // Always depletes after successful mining\n    },\n];\n\nconst Trees: IHarvestable[] = [\n    {\n        objects: NORMAL_OBJECTS,\n        items: 'rs:logs',\n        level: 1,\n        experience: 25,\n        respawnLow: 59,\n        respawnHigh: 98,\n        baseChance: 70,\n        break: 100,\n    },\n    {\n        objects: ACHEY_OBJECTS,\n        items: 'rs:achey_logs',\n        level: 1,\n        experience: 25,\n        respawnLow: 59,\n        respawnHigh: 98,\n        baseChance: 70,\n        break: 100,\n    },\n    {\n        objects: OAK_OBJECTS,\n        items: 'rs:oak_logs',\n        level: 15,\n        experience: 37.5,\n        respawnLow: 14,\n        respawnHigh: 14,\n        baseChance: 50,\n        break: 100 / 8,\n    },\n    {\n        objects: WILLOW_OBJECTS,\n        items: 'rs:willow_logs',\n        level: 30,\n        experience: 67.5,\n        respawnLow: 14,\n        respawnHigh: 14,\n        baseChance: 30,\n        break: 100 / 8,\n    },\n    {\n        objects: TEAK_OBJECTS,\n        items: 'rs:teak_logs',\n        level: 35,\n        experience: 85,\n        respawnLow: 15,\n        respawnHigh: 15,\n        baseChance: 0,\n        break: 100 / 8,\n    },\n    {\n        objects: DRAMEN_OBJECTS,\n        items: 'rs:dramen_branch', // You'll need to add this to logs.json\n        level: 36,\n        experience: 0,\n        respawnLow: 0,\n        respawnHigh: 0,\n        baseChance: 100,\n        break: 0,\n    },\n    {\n        objects: MAPLE_OBJECTS,\n        items: 'rs:maple_logs',\n        level: 45,\n        experience: 100,\n        respawnLow: 59,\n        respawnHigh: 59,\n        baseChance: 0,\n        break: 100 / 8,\n    },\n    {\n        objects: HOLLOW_OBJECTS,\n        items: 'rs:bark', // You'll need to add this to logs.json\n        level: 45,\n        experience: 82.5,\n        respawnLow: 43,\n        respawnHigh: 44,\n        baseChance: 0,\n        break: 100 / 8,\n    },\n    {\n        objects: MAHOGANY_OBJECTS,\n        items: 'rs:mahogany_logs',\n        level: 50,\n        experience: 125,\n        respawnLow: 14,\n        respawnHigh: 14,\n        baseChance: -5,\n        break: 100 / 8,\n    },\n    {\n        objects: YEW_OBJECTS,\n        items: 'rs:yew_logs',\n        level: 60,\n        experience: 175,\n        respawnLow: 99,\n        respawnHigh: 99,\n        baseChance: -15,\n        break: 100 / 8,\n    },\n    {\n        objects: MAGIC_OBJECTS,\n        items: 'rs:magic_logs',\n        level: 75,\n        experience: 250,\n        respawnLow: 199,\n        respawnHigh: 199,\n        baseChance: -25,\n        break: 100 / 8,\n    },\n    {\n        objects: DRAMEN_OBJECTS,\n        items: 'rs:dramen_branch',\n        level: 36,\n        experience: 0,\n        respawnLow: 0,\n        respawnHigh: 0,\n        baseChance: 100,\n        break: 0,\n    },\n    {\n        objects: HOLLOW_OBJECTS,\n        items: 'rs:bark',\n        level: 45,\n        experience: 82.5,\n        respawnLow: 43,\n        respawnHigh: 44,\n        baseChance: 0,\n        break: 100 / 8,\n    },\n];\n\nexport function getOre(ore: Ore): IHarvestable {\n    return Ores[ore];\n}\n\nexport function getOreFromRock(id: number): IHarvestable {\n    return Ores.find(ore => ore.objects.has(id)) as IHarvestable;\n}\n\nexport function getTreeFromHealthy(id: number): IHarvestable {\n    return Trees.find(tree => tree.objects.has(id)) as IHarvestable;\n}\n\nexport function getOreFromDepletedRock(id: number): IHarvestable {\n    return Ores.find(ore => {\n        for (const [rock, expired] of ore.objects) {\n            if (expired === id) {\n                return true;\n            }\n        }\n        return false;\n    }) as IHarvestable;\n}\n\nexport function getAllOreIds(): number[] {\n    const oreIds: number[] = [];\n    for (const ore of Ores) {\n        for (const [rock, expired] of ore.objects) {\n            oreIds.push(rock);\n            oreIds.push(expired);\n        }\n    }\n    return oreIds;\n}\n\nexport function getTreeIds(): number[] {\n    const treeIds: number[] = [];\n    for (const tree of Trees) {\n        for (const [healthy, expired] of tree.objects) {\n            treeIds.push(healthy);\n        }\n    }\n    return treeIds;\n}\n"
  },
  {
    "path": "src/engine/world/config/item-ids.ts",
    "content": "export const itemIds = {\n    witchesPotion: {\n        ratsTail: 300,\n        burntMeat: 2146,\n        eyeOfNewt: 221,\n        onion: 1957,\n    },\n    coins: 995,\n    bucket: 1925,\n    bucketOfMilk: 1927,\n    bucketOfWater: 1929,\n    banana: 1963,\n    ashes: 592,\n    tinderbox: 590,\n    jug: 1935,\n    jugOfWater: 1937,\n    pot: 1931,\n    potOfFlour: 1933,\n    egg: 1944,\n    grain: 1947,\n    wool: 1737,\n    ballOfWool: 1759,\n    cabbage: 1965,\n    flax: 1779,\n    bowstring: 1777,\n    potato: 1942,\n    onion: 1957,\n    shears: 1735,\n    magicString: 6038,\n    crossbowString: 9438,\n    sinew: 9436,\n    knife: 946,\n    feather: 314,\n    recruitmentDrive: {\n        shears: 5603,\n    },\n    kebab: 1971,\n    beer: 1917,\n    hammer: 2347,\n    arrows: {\n        shaft: 52,\n        headless: 53,\n        broken: 687,\n        bronze: 882,\n        bronze_p: 883,\n        bronze_pp: 5616,\n        bronze_s: 5622,\n        bronze_fire_u: 598,\n        bronze_fire_l: 942,\n        iron: 884,\n        iron_p: 885,\n        iron_pp: 5617,\n        iron_s: 5623,\n        iron_fire_u: 2532,\n        iron_fire_l: 2533,\n        steel: 886,\n        steel_p: 887,\n        steel_pp: 5618,\n        steel_s: 5624,\n        steel_fire_u: 2534,\n        steel_fire_l: 2535,\n        mithril: 888,\n        mithril_p: 889,\n        mithril_pp: 5619,\n        mithril_s: 5625,\n        mithril_fire_u: 2536,\n        mithril_fire_l: 2537,\n        adamant: 890,\n        adamant_p: 891,\n        adamant_pp: 5620,\n        adamant_s: 5626,\n        adamant_fire_u: 2538,\n        adamant_fire_l: 2539,\n        rune: 892,\n        rune_p: 893,\n        rune_pp: 5621,\n        rune_s: 5627,\n        rune_fire_u: 2540,\n        rune_fire_l: 2541,\n    },\n    essence: {\n        pure: 7936,\n        rune: 1436,\n    },\n    talismans: {\n        air: 1438,\n        earth: 1440,\n        fire: 1442,\n        water: 1444,\n        body: 1446,\n        mind: 1448,\n        blood: 1450,\n        chaos: 1452,\n        cosmic: 1454,\n        death: 1456,\n        law: 1458,\n        soul: 1460,\n        nature: 1462,\n        elemental: 5516,\n    },\n    tiaras: {\n        blank: 5525,\n        air: 5527,\n        mind: 5529,\n        water: 5531,\n        body: 5533,\n        earth: 5535,\n        fire: 5537,\n        cosmic: 5539,\n        nature: 5541,\n        chaos: 5543,\n        law: 5545,\n        death: 5547,\n        blood: 5549,\n        soul: 5551,\n    },\n    runes: {\n        air: 556,\n        mind: 558,\n        water: 555,\n        earth: 557,\n        fire: 554,\n        body: 559,\n        cosmic: 564,\n        chaos: 562,\n        nature: 561,\n        law: 563,\n        death: 560,\n    },\n    bars: {\n        bronze: 2349,\n        blurite: 9467,\n        iron: 2351,\n        silver: 2355,\n        steel: 2353,\n        gold: 2357,\n        mithril: 2359,\n        adamantite: 2361,\n        runite: 2363,\n    },\n    ores: {\n        clay: 434,\n        coal: 453,\n        tin: 438,\n        copper: 436,\n        blurite: 668,\n        iron: 440,\n        silver: 442,\n        gold: 444,\n        mithril: 447,\n        adamantite: 449,\n        runite: 451,\n    },\n    daggers: {\n        bronze: 1205,\n        iron: 1203,\n        steel: 1207,\n        mithril: 1209,\n        adamant: 1211,\n        rune: 1213,\n    },\n    swords: {\n        bronze: 1277,\n        iron: 1279,\n        steel: 1281,\n        mithril: 1285,\n        adamantite: 1287,\n        runite: 1289,\n    },\n    scimitars: {\n        bronze: 1321,\n        iron: 1323,\n        steel: 1325,\n        mithril: 1329,\n        adamantite: 1331,\n        runite: 1333,\n    },\n    spears: {\n        bronze: 1237,\n        iron: 1239,\n        steel: 1241,\n        mithril: 1243,\n        adamantite: 1245,\n        runite: 1247,\n    },\n    longswords: {\n        bronze: 1291,\n        iron: 1293,\n        steel: 1295,\n        mithril: 1299,\n        adamantite: 1301,\n        runite: 1303,\n    },\n    twoHandSwords: {\n        bronze: 1307,\n        iron: 1309,\n        steel: 1311,\n        mithril: 1315,\n        adamantite: 1317,\n        runite: 1319,\n    },\n    axes: {\n        bronze: 1351,\n        iron: 1363,\n        steel: 1365,\n        mithril: 1355,\n        adamantite: 1357,\n        runite: 1359,\n    },\n    maces: {\n        bronze: 1422,\n        iron: 1420,\n        steel: 1424,\n        mithril: 1428,\n        adamantite: 1430,\n        runite: 1432,\n    },\n    warhammers: {\n        bronze: 1337,\n        iron: 1335,\n        steel: 1339,\n        mithril: 1343,\n        adamantite: 1345,\n        runite: 1347,\n    },\n    battleAxes: {\n        bronze: 1375,\n        iron: 1363,\n        steel: 1365,\n        mithril: 1369,\n        adamantite: 1371,\n        runite: 1373,\n    },\n    claws: {\n        bronze: 3095,\n        iron: 3096,\n        steel: 3097,\n        mithril: 3099,\n        adamantite: 3100,\n        runite: 3101,\n    },\n    chainbodies: {\n        bronze: 1103,\n        iron: 1101,\n        steel: 1105,\n        mithril: 1109,\n        adamantite: 1111,\n        runite: 1113,\n    },\n    platelegs: {\n        bronze: 1075,\n        iron: 1067,\n        steel: 1069,\n        mithril: 1071,\n        adamantite: 1073,\n        runite: 1079,\n    },\n    plateskirts: {\n        bronze: 1087,\n        iron: 1081,\n        steel: 1083,\n        mithril: 1085,\n        adamantite: 1091,\n        runite: 1093,\n    },\n    platebodys: {\n        bronze: 1117,\n        iron: 1115,\n        steel: 1119,\n        mithril: 1121,\n        adamantite: 1123,\n        runite: 1127,\n    },\n    mediumHelmets: {\n        bronze: 1139,\n        iron: 1137,\n        steel: 1141,\n        mithril: 1143,\n        adamantite: 1145,\n        runite: 1147,\n    },\n    fullHelmets: {\n        bronze: 1155,\n        iron: 1153,\n        steel: 1157,\n        mithril: 1159,\n        adamantite: 1161,\n        runite: 1163,\n    },\n    squareShields: {\n        bronze: 1173,\n        iron: 1175,\n        steel: 1177,\n        mithril: 1181,\n        adamantite: 1183,\n        runite: 1185,\n    },\n    kiteshields: {\n        bronze: 1189,\n        iron: 1191,\n        steel: 1193,\n        mithril: 1197,\n        adamantite: 1199,\n        runite: 1201,\n    },\n    nails: {\n        bronze: 4819,\n        iron: 4820,\n        steel: 1539,\n        mithril: 4822,\n        adamantite: 4823,\n        runite: 4824,\n    },\n    dartTips: {\n        bronze: 819,\n        iron: 820,\n        steel: 821,\n        mithril: 822,\n        adamantite: 823,\n        runite: 824,\n    },\n    arrowTips: {\n        bronze: 39,\n        iron: 40,\n        steel: 41,\n        mithril: 42,\n        adamantite: 43,\n        runite: 44,\n    },\n    throwingKnives: {\n        bronze: 864,\n        iron: 863,\n        steel: 865,\n        mithril: 866,\n        adamantite: 867,\n        runite: 868,\n    },\n    bolts: {\n        bronze: 877,\n        iron: 9140,\n        steel: 9141,\n        mithril: 9142,\n        adamantite: 9143,\n        runite: 9144,\n    },\n    limbs: {\n        bronze: 9420,\n        iron: 9423,\n        steel: 9425,\n        mithril: 9427,\n        adamantite: 9429,\n        runite: 9431,\n    },\n    oilLanternFrames: {\n        steel: 4540,\n    },\n    studs: {\n        steel: 2370,\n    },\n    grappleTips: {\n        mithril: 9415,\n    },\n    roots: {\n        oak: 6043,\n        willow: 6047,\n        maple: 6047,\n        yew: 6049,\n        magic: 6051,\n    },\n    logs: {\n        normal: 1511,\n        achey: 2862,\n        oak: 1521,\n        willow: 1519,\n        teak: 6333,\n        dramenbranch: 771,\n        maple: 1517,\n        bark: 3239,\n        mahogany: 6332,\n        yew: 1515,\n        magic: 1513,\n    },\n    bowunstrung: {\n        woodshort: 50,\n        woodlong: 48,\n        oakshort: 54,\n        oaklong: 56,\n        compogre: 4825,\n        willowshort: 60,\n        willowlong: 58,\n        mapleshort: 64,\n        maplelong: 62,\n        yewshort: 68,\n        yewlong: 66,\n        magicshort: 72,\n        magiclong: 70,\n    },\n    bowstrung: {\n        woodshort: 841,\n        woodlong: 839,\n        oakshort: 843,\n        oaklong: 845,\n        compogre: 4827,\n        willowshort: 849,\n        willowlong: 847,\n        mapleshort: 853,\n        maplelong: 851,\n        yewshort: 857,\n        yewlong: 855,\n        magicshort: 861,\n        magiclong: 859,\n    },\n    skillCapes: {\n        attack: {\n            untrimmed: 9747,\n            trimmed: 9748,\n        },\n        strength: {\n            untrimmed: 9750,\n            trimmed: 9751,\n        },\n        defence: {\n            untrimmed: 9753,\n            trimmed: 9754,\n        },\n        ranged: {\n            untrimmed: 9756,\n            trimmed: 9757,\n        },\n        prayer: {\n            untrimmed: 9759,\n            trimmed: 9760,\n        },\n        magic: {\n            untrimmed: 9762,\n            trimmed: 9763,\n        },\n        runecrafting: {\n            untrimmed: 9765,\n            trimmed: 9766,\n        },\n        constitution: {\n            untrimmed: 9768,\n            trimmed: 9769,\n        },\n        agility: {\n            untrimmed: 9771,\n            trimmed: 9772,\n        },\n        herblore: {\n            untrimmed: 9774,\n            trimmed: 9775,\n        },\n        thieving: {\n            untrimmed: 9777,\n            trimmed: 9775,\n        },\n        crafting: {\n            untrimmed: 9780,\n            trimmed: 9781,\n        },\n        fletching: {\n            untrimmed: 9783,\n            trimmed: 9784,\n        },\n        slayer: {\n            untrimmed: 9786,\n            trimmed: 9786,\n        },\n        construction: {\n            untrimmed: 9789,\n            trimmed: 9790,\n        },\n        mining: {\n            untrimmed: 9792,\n            trimmed: 9793,\n        },\n        smithing: {\n            untrimmed: 9795,\n            trimmed: 9796,\n        },\n        fishing: {\n            untrimmed: 9798,\n            trimmed: 9799,\n        },\n        cooking: {\n            untrimmed: 9801,\n            trimmed: 9802,\n        },\n        firemaking: {\n            untrimmed: 9804,\n            trimmed: 9805,\n        },\n        woodcutting: {\n            untrimmed: 9807,\n            trimmed: 9808,\n        },\n        farming: {\n            untrimmed: 9810,\n            trimmed: 9811,\n        },\n        questpoint: {\n            untrimmed: 9813,\n        },\n    },\n    staffs: {\n        air: 1381,\n        fire: 1387,\n        water: 1383,\n        earth: 1385,\n    },\n};\n"
  },
  {
    "path": "src/engine/world/config/object-ids.ts",
    "content": "export const objectIds = {\n    furnace: 2781,\n    milkableCow: 8689,\n    fire: 2732,\n    spinningWheel: 2644,\n    bankBooth: [2213,18491],\n    depositBox: 9398,\n    shortCuts: {\n        stile: 12982,\n        fenceNearKharidCows: 9300,\n    },\n    ladders: {\n        taverlyDungeonOverworld: 1759,\n        taverlyDungeonUnderground: 1755,\n    },\n    brokenCart: 306,\n    brokenCartWheel: 327,\n    burntChest: 6420,\n    barricade: 4421,\n    crushedSkullBarricade: 6881,\n    fancyBarrel: 10293,\n    brokenDoor: 5439,\n    spearWall: 849,\n    crate: 357,\n    skeletonLayingFlat: 5358,\n    skeletonLayingAgainstWall: 5359,\n    lumbridgeAxeInLogs: 5581,\n    tree: {\n        normal: [\n            { default: 1276, stump: 1342 },\n            { default: 1277, stump: 1343 },\n            { default: 1278, stump: 1342 },\n            { default: 1279, stump: 1345 },\n            { default: 1280, stump: 1343 },\n            { default: 1315, stump: 1342 },\n            { default: 1316, stump: 1355 },\n            { default: 1318, stump: 1355 },\n            { default: 1319, stump: 1355 },\n            { default: 1330, stump: 1357 },\n            { default: 1331, stump: 1357 },\n            { default: 1332, stump: 1357 },\n            { default: 3033, stump: 1345 },\n            { default: 3034, stump: 1345 },\n            { default: 3879, stump: 3880 },\n            { default: 3881, stump: 3880 },\n            { default: 3882, stump: 3880 },\n            // { default: 3883, stump: 3884 }, // sigex: no matching stump\n            { default: 14308, stump: 1342 }, // Sigex questionable object, high value\n            { default: 14309, stump: 1342 }, // Sigex questionable object, high value\n        ],\n        dead: [\n            // Sigex: OSRS has an extra root on the stump too\n            { default: 1282, stump: 1347 },\n            { default: 1283, stump: 1347 },\n            { default: 1284, stump: 1348 },\n            { default: 1285, stump: 1349 },\n            { default: 1286, stump: 1351 },\n            { default: 1289, stump: 1353 },\n            { default: 1290, stump: 1354 },\n            { default: 1291, stump: 1351 },\n            { default: 1365, stump: 1352 },\n            { default: 1383, stump: 1358 },\n            { default: 1384, stump: 1359 },\n            { default: 3035, stump: 1347 },\n            // { default: 3036, stump: 1351 },// Sigex: no suitable stump offset looks wrong\n            { default: 5902, stump: 1347 },\n            { default: 5903, stump: 1351 },\n            { default: 5904, stump: 1351 },\n        ],\n        archey: [{ default: 2023, stump: 3371 }],\n        oak: [\n            { default: 1281, stump: 1356 },\n            // { default: 3037, stump: 1342 }, // Sigex: dark Oak tutorial island no stump\n            { default: 8467, stump: 0 }, // Farming\n        ],\n        willow: [\n            { default: 1308, stump: 7399 },\n            { default: 5551, stump: 5554 },\n            { default: 5552, stump: 5554 }, // Sigex: offset is wrong\n            { default: 5553, stump: 5554 }, // Sigex: offset is wrong\n            { default: 8487, stump: 1324 }, // Farming\n            // { default: 8488, stump: 1324 }, // Farming\n        ],\n        teak: [\n            { default: 9036, stump: 9037 },\n            { default: 15062, stump: 9037 }, // Sigex: questionable object, high value\n        ],\n        dramen: [{ default: 1292, stump: -1 }],\n        maple: [\n            { default: 1307, stump: 7400 },\n            { default: 4674, stump: 7400 },\n            // { default: 8444, stump: 0 }, // Farming\n        ],\n        hollow: [\n            { default: 2289, stump: 2310 },\n            { default: 4060, stump: 4061 },\n        ],\n        mahogany: [{ default: 9034, stump: 9035 }],\n        yew: [\n            { default: 1309, stump: 7402 },\n            // { default: 8513, stump: 0 }, // Farming\n        ],\n        magic: [\n            { default: 1306, stump: 7401 },\n            // { default: 8409, stump: 0 }, // Farming\n        ],\n    },\n    default: {\n        clay: [\n            { default: 2108, empty: 450 },\n            { default: 2109, empty: 451 },\n            { default: 14904, empty: 14896 },\n            { default: 14905, empty: 14897 },\n        ],\n        copper: [\n            { default: 11960, empty: 11555 },\n            { default: 11961, empty: 11556 },\n            { default: 11962, empty: 11557 },\n            { default: 11936, empty: 11552 },\n            { default: 11937, empty: 11553 },\n            { default: 11938, empty: 11554 },\n            { default: 2090, empty: 450 },\n            { default: 2091, empty: 451 },\n            { default: 14906, empty: 14898 },\n            { default: 14907, empty: 14899 },\n            { default: 14856, empty: 14832 },\n            { default: 14857, empty: 14833 },\n            { default: 14858, empty: 14834 },\n        ],\n        tin: [\n            { default: 11597, empty: 11555 },\n            { default: 11958, empty: 11556 },\n            { default: 11959, empty: 11557 },\n            { default: 11933, empty: 11552 },\n            { default: 11934, empty: 11553 },\n            { default: 11935, empty: 11554 },\n            { default: 2094, empty: 450 },\n            { default: 2095, empty: 451 },\n            { default: 14092, empty: 14894 },\n            { default: 14903, empty: 14895 },\n        ],\n        iron: [\n            { default: 11954, empty: 11555 },\n            { default: 11955, empty: 11556 },\n            { default: 11956, empty: 11557 },\n            { default: 2092, empty: 450 },\n            { default: 2093, empty: 451 },\n            { default: 14900, empty: 14892 },\n            { default: 14901, empty: 14893 },\n            { default: 14913, empty: 14915 },\n            { default: 14914, empty: 14916 },\n        ],\n        coal: [\n            { default: 11963, empty: 11555 },\n            { default: 11964, empty: 11556 },\n            { default: 11965, empty: 11557 },\n            { default: 11930, empty: 11552 },\n            { default: 11931, empty: 11553 },\n            { default: 11932, empty: 11554 },\n            { default: 2096, empty: 450 },\n            { default: 2097, empty: 451 },\n            { default: 14850, empty: 14832 },\n            { default: 14851, empty: 14833 },\n            { default: 14852, empty: 14834 },\n        ],\n        silver: [\n            { default: 11948, empty: 11555 },\n            { default: 11949, empty: 11556 },\n            { default: 11950, empty: 11557 },\n            { default: 2100, empty: 450 },\n            { default: 2101, empty: 451 },\n        ],\n        gold: [\n            { default: 11951, empty: 11555 },\n            { default: 11952, empty: 11556 },\n            { default: 11953, empty: 11557 },\n            { default: 2098, empty: 450 },\n            { default: 2099, empty: 451 },\n        ],\n        mithril: [\n            { default: 11945, empty: 11555 },\n            { default: 11946, empty: 11556 },\n            { default: 11947, empty: 11557 },\n            { default: 11942, empty: 11552 },\n            { default: 11943, empty: 11553 },\n            { default: 11944, empty: 11554 },\n            { default: 2102, empty: 450 },\n            { default: 2103, empty: 451 },\n            { default: 14853, empty: 14832 },\n            { default: 14854, empty: 14833 },\n            { default: 14855, empty: 14834 },\n        ],\n        adamant: [\n            { default: 11939, empty: 11552 },\n            { default: 11940, empty: 11553 },\n            { default: 11941, empty: 11554 },\n            { default: 2104, empty: 450 },\n            { default: 2105, empty: 451 },\n            { default: 14862, empty: 14832 },\n            { default: 14863, empty: 14833 },\n            { default: 14864, empty: 14834 },\n        ],\n        runite: [\n            { default: 2106, empty: 450 },\n            { default: 2107, empty: 451 },\n            { default: 14859, empty: 14832 },\n            { default: 14860, empty: 14833 },\n            { default: 14861, empty: 14834 },\n        ],\n    },\n};\n"
  },
  {
    "path": "src/engine/world/config/scenery-spawns.ts",
    "content": "import { readFileSync } from 'fs';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { JSON_SCHEMA, load } from 'js-yaml';\n\nexport function parseScenerySpawns(): LandscapeObject[] {\n    try {\n        logger.info('Parsing scenery spawns...');\n\n        const scenerySpawns = load(readFileSync('data/config/scenery-spawns.yaml', 'utf8'), { schema: JSON_SCHEMA }) as LandscapeObject[];\n\n        if (!scenerySpawns || scenerySpawns.length === 0) {\n            throw new Error('Unable to read scenery spawns.');\n        }\n\n        logger.info(`${scenerySpawns.length} scenery spawns found.`);\n\n        return scenerySpawns;\n    } catch (error) {\n        logger.error('Error parsing scenery spawns: ' + error);\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/engine/world/config/songs.ts",
    "content": "export const songs = new Map<string, number>([\n    ['scape main', 0],\n    ['iban', 1],\n    ['autumn voyage', 2],\n    ['unknown land', 3],\n    ['hells bells', 4],\n    ['sad meadow', 5],\n    ['jollyr', 6],\n    ['overture', 7],\n    ['wildwood', 8],\n    ['kingdom', 9],\n    ['moody', 10],\n    ['spooky2', 11],\n    ['long way home', 12],\n    ['mage arena', 13],\n    ['witching', 14],\n    ['workshop', 15],\n    ['scape main default', 16],\n    ['escape', 17],\n    ['horizon', 18],\n    ['arabique', 19],\n    ['lullaby', 20],\n    ['monarch waltz', 21],\n    ['gnome king', 22],\n    ['gnome', 23],\n    ['attack1', 24],\n    ['attack2', 25],\n    ['attack3', 26],\n    ['attack4', 27],\n    ['attack5', 28],\n    ['attack6', 29],\n    ['voodoo cult', 30],\n    ['voyage', 32],\n    ['gnome village', 33],\n    ['wonder', 34],\n    ['sea shanty2', 35],\n    ['arabian', 36],\n    ['deep wildy', 37],\n    ['trawler', 38],\n    ['church music 1', 39],\n    ['church music 2', 40],\n    ['expecting', 41],\n    ['wilderness2', 42],\n    ['wilderness3', 43],\n    ['right on track', 44],\n    ['venture2', 45],\n    ['harmony2', 46],\n    ['duel arena', 47],\n    ['morytania', 48],\n    ['wander', 49],\n    ['al kharid', 50],\n    ['trawler minor', 51],\n    ['serene', 52],\n    ['royale', 53],\n    ['scape soft', 54],\n    ['high seas', 55],\n    ['doorways', 56],\n    ['rune essence', 57],\n    ['nomad', 58],\n    ['cursed', 59],\n    ['lasting', 60],\n    ['village', 61],\n    ['newbie melody', 62],\n    ['chain of command', 63],\n    ['book of spells', 64],\n    ['miracle dance', 65],\n    ['legion', 66],\n    ['close quarters', 67],\n    ['cavern', 68],\n    ['egypt', 69],\n    ['upcoming', 70],\n    ['chompy hunt', 71],\n    ['fanfare', 72],\n    ['alls fairy in love n war', 73],\n    ['lightwalk', 74],\n    ['venture', 75],\n    ['harmony', 76],\n    ['splendour', 77],\n    ['reggae', 78],\n    ['the desert', 79],\n    ['soundscape', 80],\n    ['wonderous', 81],\n    ['waterfall', 82],\n    ['big chords', 83],\n    ['dead quiet', 84],\n    ['vision', 85],\n    ['dimension x', 86],\n    ['ice melody', 87],\n    ['twilight', 88],\n    ['reggae2', 89],\n    ['ambient jungle', 90],\n    ['riverside', 91],\n    ['sea shanty', 92],\n    ['parade', 93],\n    ['tribal2', 94],\n    ['intrepid', 95],\n    ['inspiration', 96],\n    ['hermit', 97],\n    ['forever', 98],\n    ['baroque', 99],\n    ['beyond', 100],\n    ['gnome village2', 101],\n    ['alone', 102],\n    ['oriental', 103],\n    ['camelot', 104],\n    ['tomorrow', 105],\n    ['expanse', 106],\n    ['miles away', 107],\n    ['starlight', 108],\n    ['theme', 109],\n    ['serenade', 110],\n    ['still night', 111],\n    ['gnomeball', 112],\n    ['lightness', 113],\n    ['jungly1', 114],\n    ['jungly2', 115],\n    ['greatness', 116],\n    ['jungly3', 117],\n    ['faerie', 118],\n    ['fishing', 119],\n    ['shining', 120],\n    ['forbidden', 121],\n    ['shine', 122],\n    ['arabian2', 123],\n    ['arabian3', 124],\n    ['garden', 125],\n    ['we are the fairies', 126],\n    ['nightfall', 127],\n    ['grumpy', 128],\n    ['spookyjungle', 129],\n    ['tree spirits', 130],\n    ['understanding', 131],\n    ['breeze', 132],\n    ['the tower', 133],\n    ['la mort', 134],\n    ['emperor', 138],\n    ['talking forest', 140],\n    ['barbarianism', 141],\n    ['complication', 142],\n    ['down to earth', 143],\n    ['scape cave', 144],\n    ['yesteryear', 145],\n    ['zealot', 146],\n    ['silence', 147],\n    ['emotion', 148],\n    ['principality', 149],\n    ['gnome theme', 150],\n    ['start', 151],\n    ['ballad of enchantment', 152],\n    ['expedition', 153],\n    ['bone dance', 154],\n    ['neverland', 155],\n    ['mausoleum', 156],\n    ['medieval', 157],\n    ['quest', 158],\n    ['gaol', 159],\n    ['army of darkness', 160],\n    ['long ago', 161],\n    ['tribal background', 162],\n    ['flute salad', 163],\n    ['landlubber', 164],\n    ['tribal', 165],\n    ['fanfare2', 166],\n    ['fanfare3', 167],\n    ['lonesome', 168],\n    ['crystal sword', 169],\n    ['the shadow', 170],\n    ['null', 171],\n    ['jungle island', 172],\n    ['dunjun', 173],\n    ['desert voyage', 174],\n    ['spirit', 175],\n    ['undercurrent', 176],\n    ['adventure', 177],\n    ['courage', 178],\n    ['underground', 179],\n    ['attention', 180],\n    ['crystal cave', 181],\n    ['dangerous', 182],\n    ['troubled', 183],\n    ['magical journey', 184],\n    ['magic dance', 185],\n    ['arrival', 186],\n    ['tremble', 187],\n    ['in the manor', 188],\n    ['wolf mountain', 189],\n    ['heart and mind', 190],\n    ['knightly', 191],\n    ['trinity', 192],\n    ['mellow', 193],\n    [\"brimstail's scales\", 194],\n    ['delrith summoning', 195],\n    ['wally cutscene', 196],\n    ['lament of meiyerditch', 197],\n    ['dagannoth dawn', 198],\n    ['the mollusc menace', 200],\n    ['slug a bug ball', 201],\n    ['prime time', 202],\n    ['my arms journey', 203],\n    ['roc and roll', 204],\n    ['high spirits', 205],\n    ['stagnant', 241],\n    ['time out', 242],\n    ['stratosphere', 243],\n    ['waterlogged', 244],\n    ['natural', 245],\n    ['grotto', 246],\n    ['artistry', 247],\n    ['aztec', 248],\n    ['forest', 251],\n    ['elven mist', 252],\n    ['lost soul', 253],\n    ['meridian', 254],\n    ['woodland', 255],\n    ['overpass', 256],\n    ['sojourn', 257],\n    ['contest', 258],\n    ['crystal castle', 259],\n    ['insect queen', 260],\n    ['marzipan', 261],\n    ['righteousness', 262],\n    ['bandit camp', 263],\n    ['mad eadgar', 264],\n    ['superstition', 265],\n    ['bone dry', 266],\n    ['sunburn', 267],\n    ['everywhere', 268],\n    ['competition', 269],\n    ['exposed', 270],\n    ['well of voyage', 271],\n    ['haunted mine', 277],\n    ['deep down', 278],\n    ['chamber', 282],\n    ['miscellania', 284],\n    ['etcetera', 285],\n    ['shadowland', 286],\n    ['lair', 287],\n    ['deadlands', 288],\n    ['rellekka', 289],\n    ['saga', 290],\n    ['borderland', 291],\n    ['stranded', 292],\n    ['legend', 293],\n    ['frostbite', 294],\n    ['warrior', 295],\n    ['technology', 296],\n    ['etcetera theme', 297],\n    ['monkey madness', 303],\n    ['marooned', 304],\n    ['anywhere', 305],\n    ['island life', 306],\n    ['temple', 307],\n    ['suspicious', 308],\n    ['showdown', 311],\n    ['find my way', 312],\n    ['castlewars', 314],\n    ['the navigator', 316],\n    ['melodrama', 317],\n    ['ready for battle', 318],\n    ['stillness', 319],\n    ['lighthouse', 320],\n    ['scape scared', 321],\n    ['out of the deep', 322],\n    ['upass', 323],\n    ['background', 324],\n    ['cave background', 325],\n    ['dark', 326],\n    ['dream', 327],\n    ['march', 328],\n    ['regal', 329],\n    ['cellar song', 330],\n    ['scape sad', 331],\n    ['scape wild', 332],\n    ['spooky', 333],\n    ['pirates of peril', 334],\n    ['romancing the crone', 335],\n    ['dangerous road', 336],\n    ['faithless', 337],\n    ['tiptoe', 338],\n    ['the terrible tower', 339],\n    ['masquerade', 340],\n    ['the slayer', 341],\n    ['body parts', 342],\n    ['monster melee', 343],\n    [\"fenkenstrain's refrain\", 344],\n    ['barking mad', 345],\n    ['goblin game', 346],\n    ['fruits de mer', 347],\n    [\"narnode's theme\", 348],\n    ['dynasty', 351],\n    ['scarab', 352],\n    ['shipwrecked', 353],\n    ['phasmatys', 354],\n    ['the other side', 355],\n    ['settlement', 356],\n    ['cave of beasts', 357],\n    ['dragontooth island', 358],\n    ['sarcophagus', 359],\n    ['down below', 361],\n    ['karamja jam', 362],\n    ['7th realm', 363],\n    ['pathways', 364],\n    ['eagle peak', 366],\n    ['time to mine', 369],\n    ['in between', 370],\n    ['far away', 372],\n    ['claustrophobia', 373],\n    ['fight or flight', 375],\n    ['temple of light', 376],\n    ['the golem', 377],\n    ['forgotten', 378],\n    ['throne of the demon', 379],\n    ['dance of the undead', 380],\n    ['dangerous way', 381],\n    ['city of the dead', 383],\n    ['hypnotized', 384],\n    ['sphinx', 387],\n    ['mirage', 388],\n    ['cave of the goblins', 389],\n    ['romper chomper', 390],\n    ['zogre dance', 392],\n    ['path of peril', 393],\n    ['wayward', 394],\n    ['tale of keldagrim', 395],\n    ['land of the dwarves', 396],\n    ['tears of guthix', 397],\n    ['the power of tears', 398],\n    ['scape original', 400],\n    ['the adventurer', 401],\n    ['the rogues den', 402],\n    ['the far side', 403],\n    ['sailing journey2', 406],\n    ['the lost melody', 407],\n    ['frogland', 409],\n    ['lost tribe cutscene', 410],\n    ['evil bobs island', 411],\n    ['into the abyss', 412],\n    ['the quizmaster', 413],\n    ['athletes foot', 415],\n    ['corporal punishment', 418],\n    ['pheasant peasant', 419],\n    ['the lost tribe', 420],\n    ['the chosen', 425],\n    ['have a blast', 434],\n    ['wilderness', 435],\n    ['forgettable melody', 436],\n    ['drunken dwarf', 438],\n    ['over to nardah', 447],\n    ['the monsters below', 448],\n    ['jungle hunt', 453],\n    ['home sweet home', 454],\n    ['joy of the hunt', 460],\n    ['the desolate isle', 461],\n    ['spirits of elid', 462],\n    ['fire and brimstone', 463],\n    ['the genie', 464],\n    ['desert heat', 465],\n    ['ground scape', 466],\n    ['in the pits', 469],\n    ['strange place', 470],\n    ['brew hoo hoo', 471],\n    ['tzhaar', 473],\n    ['wild side', 475],\n    ['dead can dance', 476],\n    ['the cellar dwellers', 478],\n    ['jungle troubles', 479],\n    ['mined out', 480],\n    ['catch me if you can', 481],\n    ['rat a tat tat', 482],\n    ['the noble rodent', 485],\n    ['bubble and squeak', 489],\n    [\"sarim's vermin\", 490],\n    ['rat hunt', 491],\n    ['the trade parade', 496],\n    ['aye car rum ba', 497],\n    ['blistering barnacles', 498],\n    ['distant land', 501],\n    ['fangs for the memory', 504],\n    [\"pharoah's tomb\", 505],\n    ['land down under', 506],\n    ['meddling kids', 508],\n    ['corridors of power', 509],\n    ['slither and thither', 510],\n    ['in the clink', 511],\n    ['mudskipper melody', 515],\n    ['subterranea', 517],\n    ['incantation', 519],\n    ['grip of the talon', 520],\n    ['xenophobe', 524],\n    ['title fight', 525],\n    ['victory is mine', 528],\n    ['woe of the wyvern', 529],\n    ['in the brine', 530],\n    [\"diango's little helpers\", 532],\n    ['roll the bones', 533],\n    ['mind over matter', 534],\n    ['golden touch', 535],\n    ['dogs of war', 537],\n    ['autumn in bridgelum', 538],\n    ['the enchanter', 541],\n    ['lament', 542],\n    ['making waves', 544],\n    ['cabin fever', 545],\n    ['last stand', 546],\n    ['scape santa', 547],\n    ['poles apart', 548],\n    ['food for thought', 558],\n    ['malady', 559],\n    ['dance of death', 560],\n    ['wrath and ruin', 565],\n    ['storm brew', 568],\n    ['the mad mole', 573],\n    ['fairy dragon cutscene', 574],\n    ['chickened out', 575],\n    ['davy jones locker', 576],\n    ['mastermindless', 577],\n    ['too many cooks', 582],\n    ['chef surprize', 583],\n    ['everlasting fire', 586],\n    ['null and void', 587],\n    ['pest control', 588],\n    ['tomb raider', 591],\n    ['no way out', 594],\n    ['method of madness', 600],\n    ['fear and loathing', 602],\n    ['funny bunnies', 603],\n    ['assault and battery', 604],\n    ['the depths', 606],\n    ['distillery hilarity', 610],\n    ['trouble brewing', 611],\n    ['head to head', 612],\n    ['pinball wizard', 614],\n    ['beetle juice', 615],\n    ['back to life', 616],\n    ['spy games', 619],\n    ['where eagles lair', 620],\n    ['homescape', 621],\n    ['waking dream', 622],\n    ['dreamstate', 623],\n    ['the lunar isle', 625],\n    ['way of the enchanter', 626],\n    ['isle of everywhere', 627],\n    ['the galleon', 630],\n    [\"life's a beach!\", 631],\n    ['little cave of horrors', 632],\n    ['on the wing', 633],\n    ['warriors guild', 634],\n    ['ham fisted', 638],\n    ['sigmunds showdown', 640],\n    ['the last shanty', 643],\n    ['night of the vampyre', 646],\n]);\n"
  },
  {
    "path": "src/engine/world/config/sound-ids.ts",
    "content": "export const soundIds = {\n    dropItem: 2739,\n    pickupItem: 2582,\n    milkCow: 372,\n    lightingFire: 2599,\n    fireLit: 2594,\n    openDoor: 62,\n    closeDoor: 60,\n    openGate: 67,\n    closeGate: 66,\n    homeTeleportDraw: 193,\n    homeTeleportSit: 196,\n    homeTeleportPullOutBook: 194,\n    homeTeleportCircleGlowAndTeleport: 195,\n    teleport: 200,\n    emptyBucket: 2401,\n    pinBeep: 1041,\n    potContentModified: 2584,\n    fillContainerWithWater: 2609,\n    sheepBaa: 2053,\n    shearSheep: 761,\n    spinWool: 2590,\n    inventoryFull: 2277,\n    buryBones: 2738,\n    oreDepeleted: 3600,\n    pickaxeSwing: 3220,\n    axeSwing: [88, 89, 90],\n    oreEmpty: 2661,\n    smelting: 2725,\n    eat: 2393,\n    prayer: {\n        deactivated: 2663,\n        thick_skin: 2690,\n        burst_of_strength: 2688,\n        clarity_of_thought: 2664,\n        sharp_eye: 2685,\n        mystic_will: 2670,\n        rock_skin: 2684,\n        superhuman_strength: 2689,\n        improved_reflexes: 2662,\n        rapid_restore: 2679,\n        rapid_heal: 2678,\n        prot_item: 1982,\n        hawk_eye: 2666,\n        mystic_lore: 2668,\n        steel_skin: 2687,\n        ultimate_strength: 2691,\n        incred_reflexes: 2667,\n        prot_from_mage: 2675,\n        prot_from_ranged: 2677,\n        prot_from_melee: 2676,\n        eagle_eye: 2665,\n        mystic_might: 2669,\n        retribution: 2682,\n        redemption: 2680,\n        smite: 2686,\n        preserve: 2679,\n        chivalry: 3826,\n        piety: 3825,\n    },\n    herblore: {\n        clean_herb: 3920,\n        make_potion: 2608, // both unfinished and finished\n    },\n    npc: {\n        human: {\n            maleDeath: 512,\n            maleDefence: 513,\n            femaleDeath: 505,\n            femaleDefence: 506,\n            playerDefence: 516,\n            noArmorHitPlayer: 519,\n            noArmorHit: 511,\n        },\n    },\n};\n"
  },
  {
    "path": "src/engine/world/config/travel-locations.ts",
    "content": "import { readFileSync } from 'fs';\nimport { Position } from '@engine/world/position';\nimport { JSON_SCHEMA, load } from 'js-yaml';\n\ninterface RawTravelLocation {\n    name: string;\n    x: number;\n    y: number;\n}\n\nexport interface TravelLocation {\n    name: string;\n    key: string;\n    position: Position;\n}\n\nconst readLocations = (): TravelLocation[] => {\n    const locationData = load(readFileSync('data/config/travel-locations-data.yaml', 'utf8'), {\n        schema: JSON_SCHEMA,\n    }) as RawTravelLocation[];\n    return locationData.map(location => {\n        return {\n            name: location.name,\n            key: location.name.toLowerCase(),\n            position: new Position(location.x, location.y, 0),\n        };\n    }) as TravelLocation[];\n};\n\nexport class TravelLocations {\n    public readonly locations: TravelLocation[];\n\n    public constructor() {\n        this.locations = readLocations();\n    }\n\n    public find(search: string): TravelLocation | null {\n        search = search.toLowerCase().trim().replace(/_/g, ' ');\n        for (const location of this.locations) {\n            if (location.key.toLowerCase() === search) {\n                return location;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/config/widget.ts",
    "content": "import type { Subject } from 'rxjs';\n\nexport const widgetScripts = {\n    musicPlayerAutoManual: 18,\n    musicPlayerLoop: 19,\n    attackStyle: 43,\n    brightness: 166,\n    unknown: 167, // ????\n    musicVolume: 168,\n    soundEffectVolume: 169,\n    mouseButtons: 170,\n    chatEffects: 171,\n    autoRetaliate: 172,\n    runMode: 173,\n    splitPrivateChat: 287,\n    bankInsertMode: 304,\n    bankWithdrawNoteMode: 115,\n    acceptAid: 427,\n    areaEffectVolume: 872,\n    questPoints: 101,\n};\n\nexport interface PlayerWidget {\n    widgetId: number;\n    secondaryWidgetId?: number;\n    type: 'SCREEN' | 'CHAT' | 'FULLSCREEN' | 'SCREEN_AND_TAB';\n    disablePlayerMovement?: boolean;\n    closeOnWalk?: boolean;\n    permanent?: boolean;\n    forceClosed?: () => void;\n    beforeOpened?: () => void;\n    afterOpened?: () => void;\n    closed?: Subject<void>;\n}\n"
  },
  {
    "path": "src/engine/world/direction.ts",
    "content": "export interface DirectionData {\n    index: number;\n    deltaX: number;\n    deltaY: number;\n    rotation: number;\n}\n\n/**\n * A direction within the world.\n */\nexport type Direction = 'NORTH' | 'SOUTH' | 'EAST' | 'WEST' | 'NORTHEAST' | 'NORTHWEST' | 'SOUTHEAST' | 'SOUTHWEST';\nexport const directionData: { [key: string]: DirectionData } = {\n    NORTH: {\n        index: 1,\n        deltaX: 0,\n        deltaY: 1,\n        rotation: 1,\n    },\n    SOUTH: {\n        index: 6,\n        deltaX: 0,\n        deltaY: -1,\n        rotation: 3,\n    },\n    EAST: {\n        index: 4,\n        deltaX: 1,\n        deltaY: 0,\n        rotation: 2,\n    },\n    WEST: {\n        index: 3,\n        deltaX: -1,\n        deltaY: 0,\n        rotation: 0,\n    },\n    NORTHEAST: {\n        index: 2,\n        deltaX: 1,\n        deltaY: 1,\n        rotation: 1,\n    },\n    NORTHWEST: {\n        index: 0,\n        deltaX: -1,\n        deltaY: 1,\n        rotation: 0,\n    },\n    SOUTHEAST: {\n        index: 7,\n        deltaX: 1,\n        deltaY: -1,\n        rotation: 2,\n    },\n    SOUTHWEST: {\n        index: 5,\n        deltaX: -1,\n        deltaY: -1,\n        rotation: 3,\n    },\n};\nexport const WNES: Direction[] = ['WEST', 'NORTH', 'EAST', 'SOUTH'];\n\nexport const directionFromIndex = (index: number): DirectionData | null => {\n    const keys = Object.keys(directionData);\n    for (const key of keys) {\n        if (directionData[key].index === index) {\n            return directionData[key];\n        }\n    }\n\n    return null;\n};\n\nexport const oppositeDirectionIndex = (index: number): number => {\n    return 7 - index;\n};\n"
  },
  {
    "path": "src/engine/world/index.ts",
    "content": "import { World } from '@engine/world/world';\n\n/**\n * The singleton instance of this game world.\n */\nexport let activeWorld: World;\n\n/**\n * Creates a new instance of the game world and assigns it to the singleton world variable.\n */\nexport const activateGameWorld = async (): Promise<World> => {\n    activeWorld = new World();\n    await activeWorld.startup();\n    return activeWorld;\n};\n"
  },
  {
    "path": "src/engine/world/instances.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport { activeWorld } from '@engine/world/index';\nimport type { Item } from '@engine/world/items/item';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport { CollisionMap } from '@engine/world/map/collision-map';\nimport { Position } from '@engine/world/position';\nimport { schedule } from '@engine/world/task';\nimport { World } from '@engine/world/world';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\n\n/**\n * Additional configuration info for an item being spawned in an instance.\n */\ninterface ItemSpawnConfig {\n    /**\n     * optional] The original owner of the spawned item.\n     */\n    owner?: Player;\n\n    /**\n     * optional] When the spawned item should expire and de-spawn.\n     */\n    expires?: number;\n\n    /**\n     * [optional] When the item should re-spawn after being picked up.\n     */\n    respawns?: number;\n}\n\n/**\n * A game world chunk that is tied to a specific instance.\n */\nexport interface InstancedChunk {\n    /**\n     * A specific instanced game chunk's collision map.\n     */\n    collisionMap: CollisionMap;\n\n    /**\n     * Tile modifications made to this instanced chunk.\n     */\n    mods: Map<string, TileModifications>;\n}\n\n/**\n * Modifications made to a single game tile within an instance.\n */\nexport class TileModifications {\n    /**\n     * New game objects that have been introduced to an instance.\n     */\n    public readonly spawnedObjects: LandscapeObject[] = [];\n\n    /**\n     * Cache/standard game objects that have been hidden from an instance.\n     */\n    public readonly hiddenObjects: LandscapeObject[] = [];\n\n    /**\n     * World items spawned onto this tile within an instance.\n     */\n    public readonly worldItems: WorldItem[] = [];\n\n    /**\n     * Checks if this tile is devoid of any modifications.\n     */\n    public get empty(): boolean {\n        return this.spawnedObjects.length === 0 && this.hiddenObjects.length === 0 && this.worldItems.length === 0;\n    }\n}\n\n/**\n * A player or group instance within the world.\n */\nexport class WorldInstance {\n    /**\n     * A list of game world chunks that have modifications made to them in this instance.\n     */\n    public readonly chunkModifications = new Map<string, InstancedChunk>();\n\n    /**\n     * A list of players currently in this instance.\n     */\n    public readonly players: Map<string, Player> = new Map<string, Player>();\n\n    /**\n     * Creates a new game world instance.\n     * @param instanceId The instanceId to apply to this new world instance.\n     */\n    public constructor(public readonly instanceId: string) {}\n\n    /**\n     * Spawns a new world item in this instance.\n     * @param item The item to spawn into the game world.\n     * @param position The position to spawn the item at.\n     * @param config Additional item spawn config.\n     * If not provided, the item will stay within the instance indefinitely.\n     */\n    public spawnWorldItem(item: Item | number, position: Position, config?: ItemSpawnConfig): WorldItem {\n        const { owner, respawns, expires } = config || {};\n\n        if (typeof item === 'number') {\n            item = { itemId: item, amount: 1 };\n        }\n        const worldItem: WorldItem = {\n            itemId: item.itemId,\n            amount: item.amount,\n            position,\n            owner,\n            expires,\n            respawns,\n            instance: this,\n        };\n\n        const { chunk: instancedChunk, mods } = this.getTileModifications(position);\n\n        if (owner) {\n            // If this world item is only visible to one player initially, we setup a timeout to spawn it for all other\n            // players after 100 game cycles.\n            try {\n                owner.outgoingPackets.setWorldItem(worldItem, worldItem.position);\n            } catch (error) {\n                logger.error(`Error spawning world item ${worldItem?.itemId} at ${worldItem?.position?.key}`, error);\n                throw error;\n            }\n        }\n\n        mods.worldItems.push(worldItem);\n        instancedChunk.mods.set(position.key, mods);\n\n        if (owner) {\n            setTimeout(() => {\n                if (worldItem.removed) {\n                    return;\n                }\n\n                this.worldItemAdded(worldItem, owner);\n                worldItem.owner = undefined;\n            }, 100 * World.TICK_LENGTH);\n        } else {\n            this.worldItemAdded(worldItem);\n        }\n\n        if (expires) {\n            // If the world item is set to expire, set up a timeout to remove it from the game world after the\n            // specified number of game cycles.\n            setTimeout(() => {\n                if (worldItem.removed) {\n                    return;\n                }\n\n                this.despawnWorldItem(worldItem);\n            }, expires * World.TICK_LENGTH);\n        }\n\n        return worldItem;\n    }\n\n    /**\n     * De-spawns a world item from this instance.\n     * @param worldItem The world item to de-spawn.\n     */\n    public despawnWorldItem(worldItem: WorldItem): void {\n        const chunkMap = this.getInstancedChunk(worldItem.position);\n\n        const chunkMod = chunkMap.mods.get(worldItem.position.key);\n        if (!chunkMod) {\n            // Object no longer exists\n            return;\n        }\n\n        if (chunkMod.worldItems && chunkMod.worldItems.length !== 0) {\n            const idx = chunkMod.worldItems.findIndex(i => i.itemId === worldItem.itemId && i.amount === worldItem.amount);\n            if (idx !== -1) {\n                chunkMod.worldItems.splice(idx, 1);\n            }\n        }\n\n        if (chunkMod.worldItems.length === 0) {\n            this.clearTileIfEmpty(worldItem.position);\n        }\n\n        worldItem.removed = true;\n        this.worldItemRemoved(worldItem);\n\n        if (worldItem.respawns !== undefined) {\n            this.respawnItem(worldItem);\n        }\n    }\n\n    /**\n     * Re-spawns a previously de-spawned world item after a specified amount of time.\n     * @param worldItem The item to re-spawn.\n     */\n    public async respawnItem(worldItem: WorldItem): Promise<void> {\n        if (worldItem.respawns === undefined) {\n            logger.warn(`Attempting to respawn item ${worldItem.itemId} at ${worldItem.position.key} that does not have a respawn time.`);\n            return;\n        }\n\n        await schedule(worldItem.respawns);\n\n        this.spawnWorldItem(\n            {\n                itemId: worldItem.itemId,\n                amount: worldItem.amount,\n            },\n            worldItem.position,\n            {\n                respawns: worldItem.respawns,\n                owner: worldItem.owner,\n                expires: worldItem.expires,\n            },\n        );\n    }\n\n    /**\n     * Adds a world item to the view of any nearby players in this instance.\n     * @param worldItem The world item that was added.\n     * @param excludePlayer [optional] A specific player to not show this world item update to.\n     * Usually this is used when a player drops the item and it should appear for them immediately, but have a delay\n     * before being shown to other players in the instance.\n     */\n    public worldItemAdded(worldItem: WorldItem, excludePlayer?: Player): void {\n        const nearbyPlayers = activeWorld.findNearbyPlayers(worldItem.position, 16, this.instanceId) || [];\n\n        nearbyPlayers.forEach(player => {\n            if (excludePlayer && excludePlayer.equals(player)) {\n                return;\n            }\n\n            player.outgoingPackets.setWorldItem(worldItem, worldItem.position);\n        });\n    }\n\n    /**\n     * Removes a world item from the view of any nearby players in this instance.\n     * @param worldItem The world item that was removed.\n     */\n    public worldItemRemoved(worldItem: WorldItem): void {\n        const nearbyPlayers = activeWorld.findNearbyPlayers(worldItem.position, 16, this.instanceId) || [];\n\n        nearbyPlayers.forEach(player => player.outgoingPackets.removeWorldItem(worldItem, worldItem.position));\n    }\n\n    /**\n     * Temporarily hides a game object from the game world.\n     * @param object The game object to temporarily hide from view.\n     * @param hideTicks The number of game cycles/ticks before the object will be shown again.\n     */\n    public async hideGameObjectTemporarily(object: LandscapeObject, hideTicks: number): Promise<void> {\n        this.hideGameObject(object);\n        await schedule(hideTicks);\n        this.showGameObject(object);\n    }\n\n    /**\n     * Spawns a temporary game object within the game world.\n     * @param object The game object to spawn.\n     * @param position The position to spawn the object at.\n     * @param despawnTicks The number of game cycles/ticks before the object will de-spawn.\n     */\n    public async spawnTemporaryGameObject(object: LandscapeObject, position: Position, despawnTicks: number): Promise<void> {\n        this.spawnGameObject(object);\n        await schedule(despawnTicks);\n        this.despawnGameObject(object);\n    }\n\n    /**\n     * Removes one game object and adds another to the game world. The new object may be completely different from\n     * the one being removed, and in different positions. NOT to be confused with `replaceObject`, which will replace\n     * and existing object with another object of the same type, orientation, and position.\n     * @param newObject The game object being spawned.\n     * @param oldObject The game object being removed.\n     * @param newObjectInCache Whether or not the object being added is the original game-cache object.\n     */\n    public toggleGameObjects(newObject: LandscapeObject, oldObject: LandscapeObject, newObjectInCache: boolean): void {\n        if (newObjectInCache) {\n            this.showGameObject(newObject);\n            this.despawnGameObject(oldObject);\n        } else {\n            this.hideGameObject(oldObject);\n            this.spawnGameObject(newObject);\n        }\n    }\n\n    /**\n     * Replaces a game object within the instance with a different object of the same object type, orientation, and position.\n     * NOT to be confused with `toggleGameObjects`, which removes one object and adds a different one that may have a differing\n     * type, orientation, or position (such as a door being opened).\n     * @param newObject The new game object to spawn, or the id of the location object to spawn.\n     * @param oldObject The game object being replaced. Usually a game-cache-stored object.\n     * @param respawnTicks [optional] How many ticks it will take before the original location object respawns.\n     * If not provided, the original game object will never re-spawn and the new location object will forever\n     * remain in it's place (in this instance).\n     */\n    public async replaceGameObject(newObject: LandscapeObject | number, oldObject: LandscapeObject, respawnTicks?: number): Promise<void> {\n        if (typeof newObject === 'number') {\n            newObject = {\n                objectId: newObject,\n                x: oldObject.x,\n                y: oldObject.y,\n                level: oldObject.level,\n                type: oldObject.type,\n                orientation: oldObject.orientation,\n            } as LandscapeObject;\n        }\n\n        this.hideGameObject(oldObject);\n        this.spawnGameObject(newObject);\n\n        if (respawnTicks !== undefined) {\n            await schedule(respawnTicks);\n            this.despawnGameObject(newObject as LandscapeObject);\n            this.showGameObject(oldObject);\n        }\n    }\n\n    /**\n     * Spawn a new game object into the instance.\n     * @param object The game object to spawn.\n     * @param reference Whether or not the object being spawned is a reference to an existing object or if it should\n     * be sent to the game client for forced rendering. Defaults to false for forced rendering.\n     */\n    public spawnGameObject(object: LandscapeObject, reference: boolean = false): void {\n        const position = new Position(object.x, object.y, object.level);\n\n        const { chunk: instancedChunk, mods } = this.getTileModifications(position);\n\n        if (mods.spawnedObjects.find(o => o.x === object.x && o.y === object.y && o.level === object.level && o.type === object.type)) {\n            return;\n        }\n\n        mods.spawnedObjects.push(object);\n        instancedChunk.mods.set(position.key, mods);\n\n        instancedChunk.collisionMap.markGameObject(object, true);\n\n        const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || [];\n        nearbyPlayers.forEach(player => player.outgoingPackets.setLocationObject(object, position));\n    }\n\n    /**\n     * Remove a previously spawned game object from the instance.\n     * @param object The game object to de-spawn.\n     */\n    public despawnGameObject(object: LandscapeObject): void {\n        const position = new Position(object.x, object.y, object.level);\n        const instancedChunk = this.getInstancedChunk(position);\n\n        const tileModifications = instancedChunk.mods.get(position.key);\n        if (!tileModifications) {\n            // Object no longer exists\n            return;\n        }\n\n        if (tileModifications.spawnedObjects && tileModifications.spawnedObjects.length !== 0) {\n            const idx = tileModifications.spawnedObjects.findIndex(\n                o => o.objectId === object.objectId && o.type === object.type && o.orientation === object.orientation,\n            );\n            if (idx !== -1) {\n                tileModifications.spawnedObjects.splice(idx, 1);\n            }\n        }\n\n        if (tileModifications.spawnedObjects.length === 0) {\n            this.clearTileIfEmpty(position);\n        }\n\n        instancedChunk.collisionMap.markGameObject(object, false);\n\n        const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || [];\n        nearbyPlayers.forEach(player => player.outgoingPackets.removeLocationObject(object, position));\n    }\n\n    /**\n     * Hides a static game object from an instance.\n     * @param object The cache game object to hide from the instance.\n     */\n    public hideGameObject(object: LandscapeObject): void {\n        const position = new Position(object.x, object.y, object.level);\n\n        const { chunk: instancedChunk, mods } = this.getTileModifications(position);\n\n        mods.hiddenObjects.push(object);\n        instancedChunk.mods.set(position.key, mods);\n\n        instancedChunk.collisionMap.markGameObject(object, false);\n\n        const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || [];\n        nearbyPlayers.forEach(player => player.outgoingPackets.removeLocationObject(object, position));\n    }\n\n    /**\n     * Shows a previously hidden static game object.\n     * @param object The cache game object to stop hiding from view.\n     */\n    public showGameObject(object: LandscapeObject): void {\n        const position = new Position(object.x, object.y, object.level);\n        const instancedChunk = this.getInstancedChunk(position);\n\n        const tileModifications = instancedChunk.mods.get(position.key);\n\n        if (!tileModifications) {\n            // Object no longer exists\n            return;\n        }\n\n        if (tileModifications.hiddenObjects && tileModifications.hiddenObjects.length !== 0) {\n            const idx = tileModifications.hiddenObjects.findIndex(\n                o => o.objectId === object.objectId && o.type === object.type && o.orientation === object.orientation,\n            );\n            if (idx !== -1) {\n                tileModifications.hiddenObjects.splice(idx, 1);\n            }\n        }\n\n        if (tileModifications.hiddenObjects.length === 0) {\n            this.clearTileIfEmpty(position);\n        }\n\n        instancedChunk.collisionMap.markGameObject(object, true);\n\n        const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || [];\n        nearbyPlayers.forEach(player => player.outgoingPackets.setLocationObject(object, position));\n    }\n\n    /**\n     * Fetch a list of world modifications from this instance.\n     * @param worldPosition The game world position to find the chunk for.\n     */\n    public getInstancedChunk(worldPosition: Position): InstancedChunk;\n\n    /**\n     * Fetch a list of world modifications from this instance.\n     * @param x The X coordinate to find the chunk of.\n     * @param y The Y coordinate to find the chunk of.\n     * @param level The height level of the chunk.\n     */\n    public getInstancedChunk(x: number, y: number, level: number): InstancedChunk;\n\n    public getInstancedChunk(worldPositionOrX: Position | number, y?: number, level?: number): InstancedChunk {\n        let chunkPosition: Position | null = null;\n\n        if (typeof worldPositionOrX === 'number') {\n            const chunk = activeWorld.chunkManager.getChunk({\n                x: worldPositionOrX,\n\n                // using ! here because we know that if the first parameter is a number, the other two will be too\n                y: y!,\n                level: level!,\n            });\n\n            if (chunk) {\n                chunkPosition = chunk.position;\n            }\n        } else {\n            chunkPosition = activeWorld.chunkManager.getChunkForWorldPosition(worldPositionOrX)?.position || null;\n        }\n\n        if (!chunkPosition) {\n            // Chunk not found - fail gracefully\n            logger.error('Failed to find chunk for world position', worldPositionOrX, y, level);\n            logger.error('Something has likely gone horribly wrong!');\n\n            return {\n                collisionMap: new CollisionMap(0, 0, 0, { instance: this }),\n                mods: new Map<string, TileModifications>(),\n            };\n        }\n\n        if (!this.chunkModifications.has(chunkPosition.key)) {\n            this.chunkModifications.set(chunkPosition.key, {\n                collisionMap: new CollisionMap(chunkPosition.x, chunkPosition.y, chunkPosition.level, { instance: this }),\n                mods: new Map<string, TileModifications>(),\n            });\n        }\n\n        // using ! here because we know the chunk exists, as we've just created it if it didn't\n        return this.chunkModifications.get(chunkPosition.key)!;\n    }\n\n    /**\n     * Fetches the list of tile modifications for a specific game tile in this instance.\n     * @param worldPosition The world position to find the modifications for.\n     */\n    public getTileModifications(worldPosition: Position): { chunk: InstancedChunk; mods: TileModifications } {\n        const instancedChunk = this.getInstancedChunk(worldPosition);\n        if (instancedChunk.mods.has(worldPosition.key)) {\n            return {\n                chunk: instancedChunk,\n                // using ! here because we know it exists\n                mods: instancedChunk.mods.get(worldPosition.key)!,\n            };\n        }\n\n        return { chunk: instancedChunk, mods: new TileModifications() };\n    }\n\n    /**\n     * Adds a new player to this instance.\n     * @param player The player to allow in.\n     */\n    public addPlayer(player: Player): void {\n        this.players.set(player.username, player);\n    }\n\n    /**\n     * Removes a player from this instance.\n     * If the instance is not the global instance and\n     * there are no more players within it, then this will gracefully close the instance.\n     * @param player The player remove from the instance.\n     */\n    public removePlayer(player: Player): void {\n        this.players.delete(player.username);\n\n        if (this.instanceId !== activeWorld.globalInstance.instanceId && this.players.size === 0) {\n            this.chunkModifications.clear();\n            const instancedNpcs = activeWorld.findNpcsByInstance(this.instanceId);\n            instancedNpcs?.forEach(npc => activeWorld.deregisterNpc(npc));\n        }\n    }\n\n    /**\n     * Checks to see if the specified world tile is devoid of modifications for this instance.\n     * If it is, this method will delete the `TileModifications` entry to free up memory.\n     * @param worldPosition The position of the game world tile to check.\n     * @private\n     */\n    private clearTileIfEmpty(worldPosition: Position): void {\n        const instancedChunk = this.getInstancedChunk(worldPosition);\n\n        const mods = instancedChunk.mods.get(worldPosition.key);\n\n        if (!mods) {\n            return;\n        }\n\n        if (mods.empty) {\n            instancedChunk.mods.delete(worldPosition.key);\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/world/items/item-container.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport { hasValueNotNull } from '@engine/util/data';\nimport { fromNote } from '@engine/world/items/item';\nimport { logger } from '@runejs/common';\nimport { filestore } from '@server/game/game-server';\nimport { Subject } from 'rxjs';\nimport type { Item } from './item';\n\nexport interface ContainerUpdateEvent {\n    slot?: number;\n    item?: Item | null;\n    type: 'ADD' | 'REMOVE' | 'SWAP' | 'SET' | 'SET_ALL' | 'UPDATE_AMOUNT' | 'CLEAR_ALL';\n}\n\nexport const getItemFromContainer = (itemId: number, slot: number, container: ItemContainer): Item | null => {\n    if (slot < 0 || slot > container.items.length - 1) {\n        return null;\n    }\n\n    const item = container.items[slot];\n    if (!item || item.itemId !== itemId) {\n        return null;\n    }\n\n    return item;\n};\n\n/**\n * This class represents a container of items.\n *\n * TODO (jameskmonger) We should use a Map instead of an array for the items.\n */\ntype InventoryMapType = (Item | null)[];\n\nexport class ItemContainer {\n    private readonly _size: number;\n    private readonly _items: InventoryMapType;\n    private readonly _containerUpdated: Subject<ContainerUpdateEvent>;\n\n    public constructor(size: number) {\n        this._size = size;\n        this._items = new Array(size);\n        this._containerUpdated = new Subject<ContainerUpdateEvent>();\n\n        for (let i = 0; i < size; i++) {\n            this._items[i] = null;\n        }\n    }\n\n    public clear(fireEvent: boolean = true): void {\n        this._items.forEach((item, index) => (this._items[index] = null));\n\n        if (fireEvent) {\n            this._containerUpdated.next({ type: 'CLEAR_ALL' });\n        }\n    }\n\n    public has(item: number | Item): boolean {\n        return this.findIndex(item) !== -1;\n    }\n\n    public amount(item: number | Item): number {\n        const itemId = typeof item === 'number' ? item : item.itemId;\n        return this._items\n            .map(item => (item && item.itemId === itemId ? item.amount || 0 : 0))\n            .reduce((accumulator, currentValue) => accumulator + currentValue);\n    }\n\n    /**\n     * Finds all slots within the container that contain the specified items.\n     * @param search The item id or Item object to search for.\n     * @returns An array of slot numbers.\n     */\n    public findAll(search: number | Item): number[] {\n        if (typeof search !== 'number') {\n            search = search.itemId;\n        }\n\n        const searchItem = findItem(search);\n\n        if (!searchItem) {\n            logger.error(`Could not find item '${search}' when searching for items in container.`);\n            return [];\n        }\n\n        const stackable = searchItem.stackable;\n\n        if (stackable) {\n            const index = this.findIndex(search);\n\n            if (!hasValueNotNull(index) || index === -1) {\n                return [];\n            } else {\n                return [index];\n            }\n        } else {\n            const slots: number[] = [];\n\n            for (let i = 0; i < this.size; i++) {\n                const item = this.items[i];\n\n                if (item?.itemId === search) {\n                    slots.push(i);\n                }\n            }\n\n            return slots;\n        }\n    }\n\n    public findIndex(item: number | Item): number {\n        const itemId = typeof item === 'number' ? item : item.itemId;\n        return this._items.findIndex(i => i?.itemId === itemId);\n    }\n\n    public setAll(items: (Item | null)[], fireEvent: boolean = true): void {\n        for (let i = 0; i < this._size; i++) {\n            this._items[i] = items[i];\n        }\n\n        if (fireEvent) {\n            this._containerUpdated.next({ type: 'SET_ALL' });\n        }\n    }\n\n    public set(slot: number, item: Item | null, fireEvent: boolean = true): void {\n        this._items[slot] = item;\n        if (fireEvent) {\n            this._containerUpdated.next({ type: 'SET', slot, item });\n        }\n    }\n\n    public findItemIndex(item: Item): number {\n        for (let i = 0; i < this._size; i++) {\n            const inventoryItem = this._items[i];\n\n            if (inventoryItem === null) {\n                continue;\n            }\n\n            if (inventoryItem.itemId === item.itemId && inventoryItem.amount >= item.amount) {\n                return i;\n            }\n        }\n\n        return -1;\n    }\n\n    public add(item: number | string | Item, fireEvent: boolean = true): { item: Item; slot: number } | null {\n        if (typeof item === 'number') {\n            item = { itemId: item, amount: 1 } as Item;\n        } else if (typeof item === 'string') {\n            const itemDetails = findItem(item);\n            if (!itemDetails) {\n                logger.warn(`Item ${item} not configured on the server.`);\n                return null;\n            }\n\n            item = { itemId: itemDetails.gameId, amount: 1 };\n        }\n\n        const existingItemIndex = this.findItemIndex({ itemId: item.itemId, amount: 1 });\n        const cacheItem = findItem(item.itemId);\n\n        if (!cacheItem) {\n            logger.error(`Could not find item '${item.itemId}' in cache when adding item to container.`);\n            return null;\n        }\n\n        if (existingItemIndex !== -1 && (cacheItem.stackable || cacheItem.bankNoteId != null)) {\n            const newItem = {\n                itemId: item.itemId,\n                // using ! here because we know the item exists in the inventory\n                amount: (this._items[existingItemIndex]!.amount += item.amount),\n            } as Item;\n\n            this.set(existingItemIndex, newItem, false);\n\n            if (fireEvent) {\n                this._containerUpdated.next({ type: 'UPDATE_AMOUNT', slot: existingItemIndex, item });\n            }\n\n            // Item already in inventory and is stackable\n            return { item: newItem, slot: existingItemIndex };\n        } else {\n            const newItemIndex = this.getFirstOpenSlot();\n            if (newItemIndex === -1 || item.amount === 0) {\n                // Not enough container space, or the amount of item being added is 0.\n                return null;\n            }\n\n            this._items[newItemIndex] = item;\n\n            if (fireEvent) {\n                this._containerUpdated.next({ type: 'ADD', slot: newItemIndex, item });\n            }\n\n            // Item added to inventory\n            return { item, slot: newItemIndex };\n        }\n    }\n\n    public addStacking(item: number | Item, fireEvent: boolean = true): { item: Item; slot: number } | null {\n        if (typeof item === 'number') {\n            item = { itemId: item, amount: 1 } as Item;\n        }\n\n        const existingItemIndex = this.findItemIndex({ itemId: item.itemId, amount: 1 });\n\n        if (existingItemIndex !== -1) {\n            const newItem = {\n                itemId: item.itemId,\n                // using ! here because we know the item exists in the inventory\n                amount: (this._items[existingItemIndex]!.amount += item.amount),\n            } as Item;\n\n            this.set(existingItemIndex, newItem, false);\n\n            if (fireEvent) {\n                this._containerUpdated.next({ type: 'UPDATE_AMOUNT', slot: existingItemIndex, item });\n            }\n\n            // Item already in inventory and is stackable\n            return { item: newItem, slot: existingItemIndex };\n        } else {\n            const newItemIndex = this.getFirstOpenSlot();\n            if (newItemIndex === -1) {\n                // Not enough container space\n                return null;\n            }\n\n            this._items[newItemIndex] = item;\n\n            if (fireEvent) {\n                this._containerUpdated.next({ type: 'ADD', slot: newItemIndex, item });\n            }\n\n            // Item added to inventory\n            return { item, slot: newItemIndex };\n        }\n    }\n\n    public amountInStack(slot: number): number {\n        return this._items[slot]?.amount || 0;\n    }\n\n    public removeFirst(item: number | Item, fireEvent: boolean = true): number {\n        const slot = this.findIndex(item);\n        if (slot === -1) {\n            return -1;\n        }\n\n        this._items[slot] = null;\n\n        if (fireEvent) {\n            this._containerUpdated.next({ type: 'REMOVE', slot });\n        }\n\n        return slot;\n    }\n\n    public remove(slot: number, fireEvent: boolean = true): Item | null {\n        const item = this._items[slot];\n        this._items[slot] = null;\n\n        if (fireEvent) {\n            this._containerUpdated.next({ type: 'REMOVE', slot });\n        }\n        return item;\n    }\n\n    public getFirstOpenSlot(): number {\n        return this._items.findIndex(item => !hasValueNotNull(item));\n    }\n\n    public hasSpace(): boolean {\n        return this.getFirstOpenSlot() !== -1;\n    }\n\n    public getOpenSlotCount(): number {\n        let count = 0;\n        for (let i = 0; i < this._size; i++) {\n            if (!hasValueNotNull(this._items[i])) {\n                count++;\n            }\n        }\n\n        return count;\n    }\n\n    public getOpenSlots(): number[] {\n        const slots: number[] = [];\n\n        for (let i = 0; i < this._size; i++) {\n            if (!hasValueNotNull(this._items[i])) {\n                slots.push(i);\n            }\n        }\n\n        return slots;\n    }\n\n    public swap(fromSlot: number, toSlot: number): void {\n        const fromItem = this._items[fromSlot];\n        const toItem = this._items[toSlot];\n\n        this._items[toSlot] = fromItem;\n        this._items[fromSlot] = toItem;\n    }\n\n    public weight(): number {\n        let weight = 0;\n\n        for (const item of this._items) {\n            if (!item) {\n                continue;\n            }\n\n            const itemData = findItem(item.itemId);\n\n            if (!itemData?.weight) {\n                continue;\n            }\n\n            weight += itemData.weight;\n        }\n\n        return weight;\n    }\n\n    public canFit(item: Item, everythingStacks: boolean = false): boolean {\n        const itemDefinition = filestore.configStore.itemStore.getItem(item.itemId);\n        if (!itemDefinition) {\n            throw new Error(`Item ID ${item.itemId} not found!`);\n        }\n        if (itemDefinition.stackable || everythingStacks || fromNote(item) > -1) {\n            if (this.has(item.itemId)) {\n                // using ! here because we know that we have the item in the inventory\n                const invItem = this.items[this.findIndex(item.itemId)]!;\n                return invItem.amount + item.amount <= 2147483647;\n            }\n            return this.hasSpace();\n        } else {\n            return this.getOpenSlotCount() >= item.amount;\n        }\n    }\n\n    public get size(): number {\n        return this._size;\n    }\n\n    public get items(): (Item | null)[] {\n        return this._items;\n    }\n\n    public get containerUpdated(): Subject<ContainerUpdateEvent> {\n        return this._containerUpdated;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/items/item.ts",
    "content": "import { findItem, itemMap, widgets } from '@engine/config/config-handler';\nimport type { ParentWidget, StaticItemWidget, WidgetBase } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\n\nexport interface Item {\n    itemId: number;\n    amount: number;\n}\n\nfunction itemInventoryOptions(itemId: number): string[] {\n    const itemDefinition = filestore.configStore.itemStore.getItem(itemId);\n    if (!itemDefinition) {\n        return [];\n    }\n\n    return itemDefinition.widgetOptions || [];\n}\n// TODO: Move to the filestore\nfunction IsParentWidget(widget: WidgetBase): widget is ParentWidget {\n    return 'children' in widget;\n}\n\n// TODO: Move to the filestore\nfunction IsStaticItemWidget(widget: WidgetBase): widget is StaticItemWidget {\n    return 'items' in widget;\n}\n\nexport const getItemOptions = (itemId: number, widget: { widgetId: number; containerId: number }): string[] => {\n    const widgetDefinition = filestore.widgetStore.decodeWidget(widget.widgetId) as WidgetBase;\n\n    if (widget.widgetId === widgets.inventory.widgetId) {\n        return itemInventoryOptions(itemId);\n    }\n\n    let optionsWidget: StaticItemWidget | null = null;\n    if (IsStaticItemWidget(widgetDefinition) && widgetDefinition.options && !widget.containerId) {\n        optionsWidget = widgetDefinition;\n    }\n\n    if (IsParentWidget(widgetDefinition)) {\n        const widgetChild = widgetDefinition.children[widget.containerId];\n        if (IsStaticItemWidget(widgetChild)) {\n            optionsWidget = widgetChild;\n        }\n    }\n\n    if (!optionsWidget || !optionsWidget.items || !optionsWidget.options) {\n        return itemInventoryOptions(itemId);\n    }\n\n    return optionsWidget.options;\n};\n\nexport const getItemOption = (itemId: number, optionNumber: number, widget: { widgetId: number; containerId: number }): string => {\n    const optionIndex = optionNumber - 1;\n    const options = getItemOptions(itemId, widget);\n    let option = 'option-' + optionNumber;\n    if (options && options.length >= optionNumber) {\n        if (options[optionIndex] !== null && options[optionIndex].toLowerCase() !== 'hidden') {\n            option = options[optionIndex].toLowerCase();\n        }\n    }\n\n    option = option.replace(/ /g, '-');\n    if (['wield', 'wear', 'equip'].find(s => s === option)) {\n        option = 'equip';\n    }\n    return option;\n};\n\nexport function parseItemId(item: number | Item): number {\n    return typeof item !== 'number' ? item.itemId : item;\n}\n\nexport function toNote(item: number | Item): number {\n    item = parseItemId(item);\n    let notedItem = Object.values(itemMap).find(i => i.bankNoteId === item);\n\n    if (!notedItem) {\n        const fallbackNote = findItem(item + 1);\n        if (fallbackNote?.bankNoteId === item) {\n            notedItem = fallbackNote;\n        }\n    }\n\n    return !notedItem ? -1 : notedItem.gameId;\n}\n\nexport function fromNote(item: number | Item): number {\n    item = parseItemId(item);\n    const notedItem = findItem(item);\n    return !notedItem ? -1 : notedItem.bankNoteId;\n}\n"
  },
  {
    "path": "src/engine/world/items/world-item.ts",
    "content": "import type { Player } from '@engine/world/actor/player/player';\nimport type { WorldInstance } from '@engine/world/instances';\nimport type { Position } from '@engine/world/position';\n\nexport type WorldItem = {\n    itemId: number;\n    amount: number;\n    position: Position;\n    owner?: Player;\n    expires?: number;\n    respawns?: number;\n    removed?: boolean;\n    instance: WorldInstance;\n};\n"
  },
  {
    "path": "src/engine/world/map/chunk-manager.ts",
    "content": "import { logger } from '@runejs/common';\nimport type { LandscapeFile, LandscapeObject, MapFile } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\nimport { Position } from '../position';\nimport { Chunk } from './chunk';\n\nexport class Tile {\n    public settings: number = 0;\n    public blocked: boolean = false;\n    public bridge: boolean = false;\n\n    public constructor(\n        public x: number,\n        public y: number,\n        public level: number,\n        settings?: number,\n    ) {\n        if (settings) {\n            this.setSettings(settings);\n        }\n    }\n\n    public setSettings(settings: number): void {\n        this.settings = settings;\n        this.blocked = (this.settings & 0x1) === 1;\n        this.bridge = (this.settings & 0x2) === 2;\n    }\n}\n\nexport interface MapRegion {\n    objects: LandscapeObject[];\n    mapFile: MapFile;\n}\n\n/**\n * Controls all of the game world's map chunks.\n */\nexport class ChunkManager {\n    public readonly regionMap: Map<string, MapRegion> = new Map<string, MapRegion>();\n    private readonly chunkMap: Map<string, Chunk>;\n\n    public constructor() {\n        this.chunkMap = new Map<string, Chunk>();\n    }\n\n    public getTile(position: Position): Tile {\n        const chunkX = position.chunkX + 6;\n        const chunkY = position.chunkY + 6;\n        const mapRegionX = Math.floor(chunkX / 8);\n        const mapRegionY = Math.floor(chunkY / 8);\n        const mapWorldPositionX = (mapRegionX & 0xff) * 64;\n        const mapWorldPositionY = mapRegionY * 64;\n        const regionSettings = this.regionMap.get(`${mapRegionX},${mapRegionY}`)?.mapFile?.tileSettings;\n\n        this.registerMapRegion(mapRegionX, mapRegionY);\n\n        if (!regionSettings) {\n            return new Tile(position.x, position.y, position.level);\n        }\n\n        const tileX = position.x - mapWorldPositionX;\n        const tileY = position.y - mapWorldPositionY;\n        const tileLevel = position.level;\n        let tileSettings = regionSettings[tileLevel][tileX][tileY];\n\n        if (tileLevel < 3) {\n            // Check for a bridge tile above the active tile\n            const tileAboveSettings = regionSettings[tileLevel + 1][tileX][tileY];\n            if ((tileAboveSettings & 0x2) === 2) {\n                // Set this tile as walkable if the tile above is a bridge -\n                // This is because the maps are stored with bridges being one level\n                // above where their collision maps need to be\n                tileSettings = 0;\n            }\n        }\n\n        return new Tile(position.x, position.y, tileLevel, tileSettings);\n    }\n\n    public registerMapRegion(mapRegionX: number, mapRegionY: number): void {\n        const key = `${mapRegionX},${mapRegionY}`;\n\n        if (this.regionMap.has(key)) {\n            // Map region already registered\n            return;\n        }\n        this.regionMap.delete(key);\n\n        let mapFile: MapFile | null = null;\n        let landscapeFile: LandscapeFile | null = null;\n\n        try {\n            mapFile = filestore.regionStore.getMapFile(mapRegionX, mapRegionY);\n        } catch (error) {\n            logger.error(`Error decoding map file ${mapRegionX},${mapRegionY}`);\n        }\n        try {\n            landscapeFile = filestore.regionStore.getLandscapeFile(mapRegionX, mapRegionY);\n        } catch (error) {\n            logger.error(`Error decoding landscape file ${mapRegionX},${mapRegionY}`);\n        }\n\n        if (!mapFile) {\n            logger.error(`No decoded map file ${mapRegionX},${mapRegionY}`);\n            return;\n        }\n\n        if (!landscapeFile) {\n            logger.error(`No decoded landscape file ${mapRegionX},${mapRegionY}`);\n            return;\n        }\n\n        const region: MapRegion = { mapFile, objects: landscapeFile?.landscapeObjects || [] };\n\n        this.regionMap.set(key, region);\n        this.registerObjects(region.objects, mapFile);\n    }\n\n    public registerObjects(objects: LandscapeObject[], mapFile: MapFile): void {\n        if (!objects || objects.length === 0) {\n            return;\n        }\n\n        const mapWorldPositionX = (mapFile.regionX & 0xff) * 64;\n        const mapWorldPositionY = mapFile.regionY * 64;\n\n        for (const object of objects) {\n            const position = new Position(object.x, object.y, object.level);\n            const localX = object.x - mapWorldPositionX;\n            const localY = object.y - mapWorldPositionY;\n\n            for (let level = 3; level >= 0; level--) {\n                if ((mapFile.tileSettings[level][localX][localY] & 0x2) === 2) {\n                    // Object is on or underneath a bridge tile and needs to move down one level\n                    position.move(object.x, object.y, object.level - 1);\n                }\n            }\n\n            this.getChunkForWorldPosition(position).setFilestoreLandscapeObject(object);\n        }\n    }\n\n    public getSurroundingChunks(chunk: Chunk): Chunk[] {\n        const chunks: Chunk[] = [];\n\n        const mainX = chunk.position.x;\n        const mainY = chunk.position.y;\n        const level = chunk.position.level;\n\n        for (let x = mainX - 2; x <= mainX + 2; x++) {\n            for (let y = mainY - 2; y <= mainY + 2; y++) {\n                chunks.push(this.getChunk({ x, y, level }));\n            }\n        }\n\n        return chunks;\n    }\n\n    /**\n     * Given a world Position, return the region ID that it is contained within.\n     * @param position The position to use\n     */\n    public getRegionIdForWorldPosition(position: Position): number {\n        return ((position.x >> 6) << 8) + (position.y >> 6);\n    }\n\n    public getChunkForWorldPosition(position: Position): Chunk {\n        return this.getChunk({ x: position.chunkX, y: position.chunkY, level: position.level });\n    }\n\n    public getChunk(position: Position | { x: number; y: number; level: number }): Chunk {\n        if (!(position instanceof Position)) {\n            position = new Position(position.x, position.y, position.level);\n        }\n\n        const pos = position as Position;\n        if (this.chunkMap.has(pos.key)) {\n            // using ! here because we know it exists\n            return this.chunkMap.get(pos.key)!;\n        } else {\n            const chunk = new Chunk(pos);\n            this.chunkMap.set(pos.key, chunk);\n            chunk.registerMapRegion();\n            return chunk;\n        }\n    }\n}\n"
  },
  {
    "path": "src/engine/world/map/chunk.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport type { Npc } from '../actor/npc';\nimport type { Player } from '../actor/player/player';\nimport type { Position } from '../position';\nimport { CollisionMap } from './collision-map';\n\ninterface CustomLandscapeObject {\n    reference?: boolean;\n}\n\nexport interface ChunkUpdateItem {\n    object?: LandscapeObject & CustomLandscapeObject;\n    worldItem?: WorldItem;\n    type: 'ADD' | 'REMOVE';\n}\n\n/**\n * A single map chunk within the game world that keeps track of the entities within it.\n */\nexport class Chunk {\n    private readonly _position: Position;\n    private readonly _players: Player[];\n    private readonly _npcs: Npc[];\n    private readonly _collisionMap: CollisionMap;\n    private readonly _filestoreLandscapeObjects: Map<string, LandscapeObject>;\n\n    public constructor(position: Position) {\n        this._position = position;\n        this._players = [];\n        this._npcs = [];\n        this._collisionMap = new CollisionMap(position.x, position.y, position.level, { chunk: this });\n        this._filestoreLandscapeObjects = new Map<string, LandscapeObject>();\n    }\n\n    public registerMapRegion(): void {\n        const mapRegionX = Math.floor((this.position.x + 6) / 8);\n        const mapRegionY = Math.floor((this.position.y + 6) / 8);\n        activeWorld.chunkManager.registerMapRegion(mapRegionX, mapRegionY);\n    }\n\n    public setFilestoreLandscapeObject(landscapeObject: LandscapeObject): void {\n        this._filestoreLandscapeObjects.set(`${landscapeObject.x},${landscapeObject.y},${landscapeObject.objectId}`, landscapeObject);\n        this._collisionMap.markGameObject(landscapeObject, true);\n    }\n\n    public addPlayer(player: Player): void {\n        if (this._players.findIndex(p => p.equals(player)) === -1) {\n            this._players.push(player);\n        }\n    }\n\n    public removePlayer(player: Player): void {\n        const index = this._players.findIndex(p => p.equals(player));\n        if (index !== -1) {\n            this._players.splice(index, 1);\n        }\n    }\n\n    public addNpc(npc: Npc): void {\n        if (this._npcs.findIndex(n => n.equals(npc)) === -1) {\n            this._npcs.push(npc);\n        }\n    }\n\n    public removeNpc(npc: Npc): void {\n        const index = this._npcs.findIndex(n => n.equals(npc));\n        if (index !== -1) {\n            this._npcs.splice(index, 1);\n        }\n    }\n\n    public getFilestoreLandscapeObject(objectId: number, position: Position): LandscapeObject | null {\n        return this.filestoreLandscapeObjects.get(`${position.x},${position.y},${objectId}`) || null;\n    }\n\n    public equals(chunk: Chunk): boolean {\n        return this.position.x === chunk.position.x && this.position.y === chunk.position.y && this.position.level === chunk.position.level;\n    }\n\n    public get position(): Position {\n        return this._position;\n    }\n\n    public get players(): Player[] {\n        return this._players;\n    }\n\n    public get npcs(): Npc[] {\n        return this._npcs;\n    }\n\n    public get collisionMap(): CollisionMap {\n        return this._collisionMap;\n    }\n\n    public get filestoreLandscapeObjects(): Map<string, LandscapeObject> {\n        return this._filestoreLandscapeObjects;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/map/collision-map.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { WorldInstance } from '@engine/world/instances';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\nimport type { Chunk } from './chunk';\n\n/**\n * A map of collision masks for a chunk within the game world.\n */\nexport class CollisionMap {\n    private heightLevel: number;\n    private x: number;\n    private y: number;\n    private sizeX: number;\n    private sizeY: number;\n    private _insetX: number;\n    private _insetY: number;\n    private _adjacency: (number | null)[][];\n    private chunk: Chunk | undefined;\n    private instance: WorldInstance | undefined;\n\n    public constructor(x: number, y: number, heightLevel: number, options?: { chunk?: Chunk; instance?: WorldInstance }) {\n        this.heightLevel = heightLevel;\n        this.x = x;\n        this.y = y;\n        this.sizeX = 8;\n        this.sizeY = 8;\n        this._insetX = (x + 6) * 8;\n        this._insetY = (y + 6) * 8;\n        this.chunk = options?.chunk;\n        this.instance = options?.instance;\n        this._adjacency = new Array(this.sizeX);\n        for (let i = 0; i < this.sizeX; i++) {\n            this._adjacency[i] = new Array(this.sizeY);\n        }\n        this.reset();\n    }\n\n    public markGameObject(landscapeObject: LandscapeObject, mark: boolean): void {\n        const x: number = landscapeObject.x;\n        const y: number = landscapeObject.y;\n        const objectType = landscapeObject.type;\n        const objectOrientation = landscapeObject.orientation;\n        const objectDetails = filestore.configStore.objectStore.getObject(landscapeObject.objectId);\n\n        if (!objectDetails) {\n            logger.error(`Could not find object details for object id: ${landscapeObject.objectId} when marking collision map.`);\n            return;\n        }\n\n        if (objectDetails.solid) {\n            if (objectType === 22) {\n                if (objectDetails.hasOptions) {\n                    this.markBlocked(x, y, mark);\n                }\n            } else if (objectType >= 9) {\n                this.markSolidOccupant(\n                    x,\n                    y,\n                    objectDetails.rendering.sizeX,\n                    objectDetails.rendering.sizeY,\n                    objectOrientation,\n                    objectDetails.nonWalkable,\n                    mark,\n                );\n            } else if (objectType >= 0 && objectType <= 3) {\n                if (mark) {\n                    this.markWall(x, y, objectType, objectOrientation, objectDetails.nonWalkable);\n                } else {\n                    this.unmarkWall(x, y, objectType, objectOrientation, objectDetails.nonWalkable);\n                }\n            }\n        }\n    }\n\n    public reset(): void {\n        for (let x = 0; x < this.sizeX; x++) {\n            for (let y = 0; y < this.sizeY; y++) {\n                this._adjacency[x][y] = this.chunk ? 0 : null;\n            }\n        }\n    }\n\n    public markWall(x: number, y: number, type: number, rotation: number, walkable: boolean): void {\n        x -= this._insetX;\n        y -= this._insetY;\n\n        if (type == 0) {\n            if (rotation == 0) {\n                this.set(x, y, 128);\n                this.set(x - 1, y, 8);\n            }\n            if (rotation == 1) {\n                this.set(x, y, 2);\n                this.set(x, y + 1, 32);\n            }\n            if (rotation == 2) {\n                this.set(x, y, 8);\n                this.set(x + 1, y, 128);\n            }\n            if (rotation == 3) {\n                this.set(x, y, 32);\n                this.set(x, y - 1, 2);\n            }\n        }\n        if (type == 1 || type == 3) {\n            if (rotation == 0) {\n                this.set(x, y, 1);\n                this.set(x - 1, y + 1, 16);\n            }\n            if (rotation == 1) {\n                this.set(x, y, 4);\n                this.set(x + 1, y + 1, 64);\n            }\n            if (rotation == 2) {\n                this.set(x, y, 16);\n                this.set(x + 1, y - 1, 1);\n            }\n            if (rotation == 3) {\n                this.set(x, y, 64);\n                this.set(x - 1, y - 1, 4);\n            }\n        }\n        if (type == 2) {\n            if (rotation == 0) {\n                this.set(x, y, 130);\n                this.set(x - 1, y, 8);\n                this.set(x, y + 1, 32);\n            }\n            if (rotation == 1) {\n                this.set(x, y, 10);\n                this.set(x, y + 1, 32);\n                this.set(x + 1, y, 128);\n            }\n            if (rotation == 2) {\n                this.set(x, y, 40);\n                this.set(x + 1, y, 128);\n                this.set(x, y - 1, 2);\n            }\n            if (rotation == 3) {\n                this.set(x, y, 160);\n                this.set(x, y - 1, 2);\n                this.set(x - 1, y, 8);\n            }\n        }\n        if (walkable) {\n            if (type == 0) {\n                if (rotation == 0) {\n                    this.set(x, y, 0x10000);\n                    this.set(x - 1, y, 4096);\n                }\n                if (rotation == 1) {\n                    this.set(x, y, 1024);\n                    this.set(x, y + 1, 16384);\n                }\n                if (rotation == 2) {\n                    this.set(x, y, 4096);\n                    this.set(x + 1, y, 0x10000);\n                }\n                if (rotation == 3) {\n                    this.set(x, y, 16384);\n                    this.set(x, y - 1, 1024);\n                }\n            }\n            if (type == 1 || type == 3) {\n                if (rotation == 0) {\n                    this.set(x, y, 512);\n                    this.set(x - 1, y + 1, 8192);\n                }\n                if (rotation == 1) {\n                    this.set(x, y, 2048);\n                    this.set(x + 1, y + 1, 32768);\n                }\n                if (rotation == 2) {\n                    this.set(x, y, 8192);\n                    this.set(x + 1, y - 1, 512);\n                }\n                if (rotation == 3) {\n                    this.set(x, y, 32768);\n                    this.set(x - 1, y - 1, 2048);\n                }\n            }\n            if (type == 2) {\n                if (rotation == 0) {\n                    this.set(x, y, 0x10400);\n                    this.set(x - 1, y, 4096);\n                    this.set(x, y + 1, 16384);\n                }\n                if (rotation == 1) {\n                    this.set(x, y, 5120);\n                    this.set(x, y + 1, 16384);\n                    this.set(x + 1, y, 0x10000);\n                }\n                if (rotation == 2) {\n                    this.set(x, y, 20480);\n                    this.set(x + 1, y, 0x10000);\n                    this.set(x, y - 1, 1024);\n                }\n                if (rotation == 3) {\n                    this.set(x, y, 0x14000);\n                    this.set(x, y - 1, 1024);\n                    this.set(x - 1, y, 4096);\n                }\n            }\n        }\n    }\n\n    public unmarkWall(x: number, y: number, position: number, rotation: number, impenetrable: boolean): void {\n        x -= this._insetX;\n        y -= this._insetY;\n        if (position == 0) {\n            if (rotation == 0) {\n                this.unset(x, y, 128);\n                this.unset(x - 1, y, 8);\n            }\n            if (rotation == 1) {\n                this.unset(x, y, 2);\n                this.unset(x, y + 1, 32);\n            }\n            if (rotation == 2) {\n                this.unset(x, y, 8);\n                this.unset(x + 1, y, 128);\n            }\n            if (rotation == 3) {\n                this.unset(x, y, 32);\n                this.unset(x, y - 1, 2);\n            }\n        }\n        if (position == 1 || position == 3) {\n            if (rotation == 0) {\n                this.unset(x, y, 1);\n                this.unset(x - 1, y + 1, 16);\n            }\n            if (rotation == 1) {\n                this.unset(x, y, 4);\n                this.unset(x + 1, y + 1, 64);\n            }\n            if (rotation == 2) {\n                this.unset(x, y, 16);\n                this.unset(x + 1, y - 1, 1);\n            }\n            if (rotation == 3) {\n                this.unset(x, y, 64);\n                this.unset(x - 1, y - 1, 4);\n            }\n        }\n        if (position == 2) {\n            if (rotation == 0) {\n                this.unset(x, y, 130);\n                this.unset(x - 1, y, 8);\n                this.unset(x, y + 1, 32);\n            }\n            if (rotation == 1) {\n                this.unset(x, y, 10);\n                this.unset(x, y + 1, 32);\n                this.unset(x + 1, y, 128);\n            }\n            if (rotation == 2) {\n                this.unset(x, y, 40);\n                this.unset(x + 1, y, 128);\n                this.unset(x, y - 1, 2);\n            }\n            if (rotation == 3) {\n                this.unset(x, y, 160);\n                this.unset(x, y - 1, 2);\n                this.unset(x - 1, y, 8);\n            }\n        }\n        if (impenetrable) {\n            if (position == 0) {\n                if (rotation == 0) {\n                    this.unset(x, y, 0x10000);\n                    this.unset(x - 1, y, 4096);\n                }\n                if (rotation == 1) {\n                    this.unset(x, y, 1024);\n                    this.unset(x, y + 1, 16384);\n                }\n                if (rotation == 2) {\n                    this.unset(x, y, 4096);\n                    this.unset(x + 1, y, 0x10000);\n                }\n                if (rotation == 3) {\n                    this.unset(x, y, 16384);\n                    this.unset(x, y - 1, 1024);\n                }\n            }\n            if (position == 1 || position == 3) {\n                if (rotation == 0) {\n                    this.unset(x, y, 512);\n                    this.unset(x - 1, y + 1, 8192);\n                }\n                if (rotation == 1) {\n                    this.unset(x, y, 2048);\n                    this.unset(x + 1, y + 1, 32768);\n                }\n                if (rotation == 2) {\n                    this.unset(x, y, 8192);\n                    this.unset(x + 1, y - 1, 512);\n                }\n                if (rotation == 3) {\n                    this.unset(x, y, 32768);\n                    this.unset(x - 1, y - 1, 2048);\n                }\n            }\n            if (position == 2) {\n                if (rotation == 0) {\n                    this.unset(x, y, 0x10400);\n                    this.unset(x - 1, y, 4096);\n                    this.unset(x, y + 1, 16384);\n                }\n                if (rotation == 1) {\n                    this.unset(x, y, 5120);\n                    this.unset(x, y + 1, 16384);\n                    this.unset(x + 1, y, 0x10000);\n                }\n                if (rotation == 2) {\n                    this.unset(x, y, 20480);\n                    this.unset(x + 1, y, 0x10000);\n                    this.unset(x, y - 1, 1024);\n                }\n                if (rotation == 3) {\n                    this.unset(x, y, 0x14000);\n                    this.unset(x, y - 1, 1024);\n                    this.unset(x - 1, y, 4096);\n                }\n            }\n        }\n    }\n\n    public markSolidOccupant(\n        occupantX: number,\n        occupantY: number,\n        width: number,\n        height: number,\n        rotation: number,\n        walkable: boolean,\n        mark: boolean,\n    ): void {\n        let occupied = 256;\n        if (walkable) {\n            occupied += 0x20000;\n        }\n\n        occupantX -= this._insetX;\n        occupantY -= this._insetY;\n\n        if (rotation === 1 || rotation === 3) {\n            const off = width;\n            width = height;\n            height = off;\n        }\n\n        for (let x = occupantX; x < occupantX + width; x++) {\n            for (let y = occupantY; y < occupantY + height; y++) {\n                if (mark) {\n                    this.set(x, y, occupied);\n                } else {\n                    this.unset(x, y, occupied);\n                }\n            }\n        }\n    }\n\n    public markBlocked(x: number, y: number, mark: boolean): void {\n        x -= this._insetX;\n        y -= this._insetY;\n\n        if (this._adjacency[x][y] === null) {\n            this._adjacency[x][y] = 0;\n        }\n\n        if (mark) {\n            // @ts-ignore\n            this._adjacency[x][y] |= 0x200000;\n        } else {\n            // @ts-ignore\n            this._adjacency[x][y] &= 0xdfffff;\n        }\n    }\n\n    public set(x: number, y: number, flag: number): void {\n        let outOfBounds = false;\n\n        let offsetX = 0;\n        let offsetY = 0;\n\n        if (x < 0) {\n            offsetX = -1;\n            x = 8 + x;\n        } else if (x > 7) {\n            offsetX = 1;\n            x = x - 8;\n        }\n\n        if (y < 0) {\n            offsetY = -1;\n            y = 8 + y;\n        } else if (y > 7) {\n            offsetY = 1;\n            y = y - 8;\n        }\n\n        if (offsetX != 0 || offsetY != 0) {\n            this.getSiblingCollisionMap(offsetX, offsetY)?.set(x, y, flag);\n            outOfBounds = true;\n        }\n\n        if (!outOfBounds) {\n            if (this._adjacency[x][y] === null) {\n                this._adjacency[x][y] = 0;\n            }\n\n            // @ts-ignore\n            this._adjacency[x][y] |= flag;\n        }\n    }\n\n    public unset(x: number, y: number, flag: number): void {\n        let outOfBounds = false;\n\n        if (x < 0) {\n            this.getSiblingCollisionMap(-1, 0)?.unset(7, y, flag);\n            outOfBounds = true;\n        } else if (x > 7) {\n            this.getSiblingCollisionMap(1, 0)?.unset(0, y, flag);\n            outOfBounds = true;\n        }\n\n        if (y < 0) {\n            this.getSiblingCollisionMap(0, -1)?.unset(x, 7, flag);\n            outOfBounds = true;\n        } else if (y > 7) {\n            this.getSiblingCollisionMap(0, 1)?.unset(x, 0, flag);\n            outOfBounds = true;\n        }\n\n        if (!outOfBounds) {\n            if (this._adjacency[x][y] === null) {\n                this._adjacency[x][y] = 0;\n            }\n\n            // @ts-ignore\n            this._adjacency[x][y] &= 0xffffff - flag;\n        }\n    }\n\n    public getSiblingCollisionMap(offsetX: number, offsetY: number): CollisionMap | null {\n        if (this.chunk) {\n            const offsetChunk: Chunk = activeWorld.chunkManager.getChunk({\n                x: this.chunk.position.x + offsetX,\n                y: this.chunk.position.y + offsetY,\n                level: this.heightLevel,\n            });\n            return offsetChunk.collisionMap;\n        } else if (this.instance) {\n            const instanceChunk = this.instance.getInstancedChunk(this.x + offsetX, this.y + offsetY, this.heightLevel);\n            return instanceChunk.collisionMap;\n        }\n\n        return null;\n    }\n\n    public get insetX(): number {\n        return this._insetX;\n    }\n\n    public get insetY(): number {\n        return this._insetY;\n    }\n\n    public get adjacency(): (number | null)[][] {\n        return this._adjacency;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/map/landscape-object.ts",
    "content": "import type { LandscapeObject } from '@runejs/filestore';\n\nexport interface ModifiedLandscapeObject extends LandscapeObject {\n    metadata?: { [key: string]: any };\n}\n\nexport const objectKey = (object: LandscapeObject, level: boolean = false): string => {\n    return `${object.x},${object.y}${level ? `,${object.level}` : ''},${object.objectId}`;\n};\n"
  },
  {
    "path": "src/engine/world/map/region.ts",
    "content": "/**\n * Different types of world regions.\n * map: 64x64 tile (13x13 tile chunks) full map region file.\n * chunk: 8x8 tile chunk within a map.\n */\nimport type { Position } from '@engine/world/position';\n\nexport type RegionType = 'mapfile' | 'region' | 'chunk';\n\n/**\n * A type for defining region tile sizes.\n */\nexport type RegionSizeMap = {\n    [key in RegionType]: number;\n};\n\n/**\n * A map of region types to tile sizes.\n */\nexport const regionSizes: RegionSizeMap = {\n    mapfile: 104,\n    region: 64,\n    chunk: 8,\n};\n\nexport abstract class ConstructedChunk {\n    public orientation: number;\n\n    protected constructor(rotation: number = 0) {\n        this.orientation = rotation;\n    }\n\n    public abstract getTemplatePosition(): Position;\n\n    public get templatePosition(): Position {\n        return this.getTemplatePosition();\n    }\n}\n\nexport interface ConstructedRegion {\n    renderPosition: Position;\n    chunks: ConstructedChunk[][][];\n    drawOffsetX?: number;\n    drawOffsetY?: number;\n}\n\nexport const getTemplateRotatedX = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => {\n    if (orientation === 1 || orientation === 3) {\n        const i = sizeX;\n        sizeX = sizeY;\n        sizeY = i;\n    }\n\n    if (orientation === 0) {\n        return localX;\n    }\n    if (orientation === 1) {\n        return 7 - (localY - sizeY + 1);\n    }\n    if (orientation === 2) {\n        return 7 - (localX + sizeX + 1);\n    }\n    return localY;\n};\n\nexport const getTemplateRotatedY = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => {\n    if (orientation === 1 || orientation === 3) {\n        const i = sizeX;\n        sizeX = sizeY;\n        sizeY = i;\n    }\n\n    if (orientation === 0) {\n        return localY;\n    }\n    if (orientation === 1) {\n        return localX;\n    }\n    if (orientation === 2) {\n        return 7 - (localY + sizeY + 1);\n    }\n    return 7 - (localX - sizeX + 1);\n};\n\nexport const getTemplateLocalX = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => {\n    if (orientation === 2) {\n        const i = sizeX;\n        sizeX = sizeY;\n        sizeY = i;\n    }\n\n    if (orientation === 0) {\n        return localX;\n    } else if (orientation === 1) {\n        return 7 - (localY + sizeY) + 1;\n    } else if (orientation === 2) {\n        return 7 - (localX + sizeX) + 1;\n    } else {\n        // 3\n        return localY;\n    }\n};\n\nexport const getTemplateLocalY = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => {\n    if (orientation === 2) {\n        const i = sizeX;\n        sizeX = sizeY;\n        sizeY = i;\n    }\n\n    if (orientation === 0) {\n        return localY;\n    } else if (orientation === 1) {\n        return localX;\n    } else if (orientation === 2) {\n        return 7 - (localY + sizeY) + 1;\n    } else {\n        // 3\n        return 7 - (localX + sizeX) + 1;\n    }\n};\n"
  },
  {
    "path": "src/engine/world/position.ts",
    "content": "import type { Direction } from '@engine/world/direction';\nimport { directionData } from '@engine/world/direction';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { filestore } from '@server/game/game-server';\n\nconst directionDeltaX = [-1, 0, 1, -1, 1, -1, 0, 1];\nconst directionDeltaY = [1, 1, 1, 0, 0, -1, -1, -1];\n\n/**\n * A simplified x/y/level coordinate class.\n */\nexport class Coords {\n    x: number;\n    y: number;\n    level: number;\n\n    static equals(a: Coords, b: Coords): boolean {\n        return a.x === b.x && a.y === b.y && a.level === b.level;\n    }\n}\n\n/**\n * Represents a single position, or coordinate, within the game world.\n */\nexport class Position {\n    public metadata: { [key: string]: any } = {};\n    private _x: number;\n    private _y: number;\n    private _level: number;\n\n    public constructor(position: Position);\n    public constructor(coords: Coords);\n    public constructor(x: number, y: number, level?: number);\n    public constructor(arg0: number | Coords | Position, y?: number, level?: number) {\n        if (typeof arg0 === 'number') {\n            // using ! here, because we know that if arg0 is a number, then y and level are numbers\n            this.move(arg0, y!, level);\n        } else {\n            this.move(arg0.x, arg0.y, arg0.level);\n        }\n    }\n\n    public clone(): Position {\n        return new Position(this.x, this.y, this.level);\n    }\n\n    public withinInteractionDistance(gameObject: LandscapeObject, minimumDistance?: number): boolean;\n    public withinInteractionDistance(position: Position, minimumDistance?: number): boolean;\n    public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance?: number): boolean;\n    public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance: number = 1): boolean {\n        if (target instanceof Position) {\n            return this.distanceBetween(target) <= minimumDistance;\n        } else {\n            const definition = filestore.configStore.objectStore.getObject(target.objectId);\n\n            if (!definition) {\n                logger.warn(`Object with id ${target.objectId} does not exist in the object store.`);\n            }\n\n            const occupantX = target.x;\n            const occupantY = target.y;\n            let width = definition?.rendering?.sizeX || 1;\n            let height = definition?.rendering?.sizeY || 1;\n\n            if (width === undefined || width === null || width < 1) {\n                width = 1;\n            }\n            if (height === undefined || height === null || height < 1) {\n                height = 1;\n            }\n\n            if (width === 1 && height === 1) {\n                return this.distanceBetween(new Position(occupantX, occupantY, target.level)) <= minimumDistance;\n            } else {\n                if (target.orientation === 1 || target.orientation === 3) {\n                    const off = width;\n                    width = height;\n                    height = off;\n                }\n\n                for (let x = occupantX; x < occupantX + width; x++) {\n                    for (let y = occupantY; y < occupantY + height; y++) {\n                        if (this.distanceBetween(new Position(x, y, target.level)) <= minimumDistance) {\n                            return true;\n                        }\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Whether or not the specified position is within the game's view distance of this position.\n     * @param position The game world position to check the distance of.\n     */\n    public withinViewDistance(position: Position): boolean {\n        if (position.level !== this.level) {\n            return false;\n        }\n\n        const offsetX = this.x - position.x;\n        const offsetY = this.y - position.y;\n\n        return offsetX < 16 && offsetY < 16 && offsetX > -16 && offsetY > -16;\n    }\n\n    /**\n     * Checks to see if this position is within the two given boundaries of min and max.\n     * @param min The minimum coordinate to check within.\n     * @param max The maximum coordinate to check within.\n     * @param checkPlane Whether or not to check if the position is within the same plane. Defaults to true.\n     */\n    public within(min: Position, max: Position, checkPlane: boolean = true): boolean {\n        if (checkPlane && (min.level !== max.level || max.level !== this.level)) {\n            return false;\n        }\n\n        return this.x >= min.x && this.x <= max.x && this.y >= min.y && this.y <= max.y;\n    }\n\n    public move(x: number, y: number, level?: number): Position {\n        this._x = x;\n        this._y = y;\n\n        if (level === undefined) {\n            this._level = 0;\n        } else {\n            this._level = level;\n        }\n\n        return this;\n    }\n\n    public equalsIgnoreLevel(position: Position | { x: number; y: number }): boolean {\n        if (!(position instanceof Position)) {\n            position = new Position(position.x, position.y);\n        }\n\n        return this._x === position.x && this._y === position.y;\n    }\n\n    public distanceBetween(other: Position): number {\n        return Math.abs(Math.sqrt((this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y)));\n    }\n\n    public fromDirection(direction: number): Position {\n        return new Position(this.x + directionDeltaX[direction], this.y + directionDeltaY[direction], this.level);\n    }\n\n    public step(steps: number, direction: Direction): Position {\n        return new Position(this.x + steps * directionData[direction].deltaX, this.y + steps * directionData[direction].deltaY, this.level);\n    }\n\n    public copy(): Position {\n        return new Position(this._x, this._y, this._level);\n    }\n\n    public equals(position: Position | { x: number; y: number; level: number }): boolean {\n        if (!(position instanceof Position)) {\n            position = new Position(position.x, position.y, position.level);\n        }\n\n        return this._x === position.x && this._y === position.y && this._level === position.level;\n    }\n\n    public calculateChunkLocalX(position: Position): number {\n        return this._x - 8 * position.chunkX;\n    }\n\n    public calculateChunkLocalY(position: Position): number {\n        return this._y - 8 * position.chunkY;\n    }\n\n    /**\n     * Sets the value of X and returns the current Position instance for chaining.\n     * @param x The new value to set the current Position's X coordinate to.\n     */\n    public setX(x: number): Position {\n        this._x = x;\n        return this;\n    }\n\n    /**\n     * Sets the value of Y and returns the current Position instance for chaining.\n     * @param y The new value to set the current Position's Y coordinate to.\n     */\n    public setY(y: number): Position {\n        this._y = y;\n        return this;\n    }\n\n    /**\n     * Sets the value of Level and returns the current Position instance for chaining.\n     * @param plane The new value to set the current Position's plane to.\n     */\n    public setLevel(plane: number): Position {\n        this._level = plane;\n        return this;\n    }\n\n    /**\n     * Converts this Position into a simple Coords object.\n     */\n    public get coords(): Coords {\n        return {\n            x: this._x,\n            y: this._y,\n            level: this._level,\n        };\n    }\n\n    public get chunkX(): number {\n        return (this._x >> 3) - 6;\n    }\n\n    public get chunkY(): number {\n        return (this._y >> 3) - 6;\n    }\n\n    public get chunkLocalX(): number {\n        return this._x - 8 * this.chunkX;\n    }\n\n    public get chunkLocalY(): number {\n        return this._y - 8 * this.chunkY;\n    }\n\n    public get localX(): number {\n        return this._x - 8 * (this.chunkX + 6);\n    }\n\n    public get localY(): number {\n        return this._y - 8 * (this.chunkY + 6);\n    }\n\n    public get x(): number {\n        return this._x;\n    }\n\n    public set x(value: number) {\n        this._x = value;\n    }\n\n    public get y(): number {\n        return this._y;\n    }\n\n    public set y(value: number) {\n        this._y = value;\n    }\n\n    public get level(): number {\n        return this._level;\n    }\n\n    public set level(value: number) {\n        this._level = value;\n    }\n\n    public get key(): string {\n        return `${this.x},${this.y},${this.level}`;\n    }\n}\n"
  },
  {
    "path": "src/engine/world/skill-util/glory-boost.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport { equipmentIndices } from '@engine/config/item-config';\nimport type { Player } from '@engine/world/actor/player/player';\n\nexport function checkForGemBoost(player: Player): number {\n    // Check if any charged glory is equipped\n    const neckSlotIndex = equipmentIndices['neck'];\n    const neckItem = player.equipment.items[neckSlotIndex];\n\n    if (!neckItem) {\n        return 256;\n    }\n\n    const itemConfig = findItem(neckItem.itemId);\n    if (!itemConfig || !itemConfig.key.startsWith('rs:amulet_of_glory:charged_')) {\n        return 256;\n    }\n\n    return 86;\n}\n"
  },
  {
    "path": "src/engine/world/skill-util/harvest-roll.ts",
    "content": "// Note if adding hunter, Strung rabbit foot makes this out of 94 instead of 99\nimport { findItem } from '@engine/config/config-handler';\nimport { randomBetween } from '@engine/util/num';\nimport type { Item } from '@engine/world/items/item';\n\nexport function rollBirdsNestType(): Item {\n    const roll = randomBetween(0, 99);\n    let itemConfigId;\n\n    if (roll > 3) {\n        // Bird egg\n        if (roll === 0) {\n            itemConfigId = 'rs:birds_egg_red';\n        } else if (roll === 1) {\n            itemConfigId = 'rs:birds_egg_green';\n        } else {\n            itemConfigId = 'rs:birds_egg_blue';\n        }\n    } else if (roll > 34) {\n        itemConfigId = 'rs:birds_nest_ring';\n    } else {\n        itemConfigId = 'rs:birds_nest_seed';\n    }\n\n    const item = findItem(itemConfigId);\n    if (!item) {\n        throw new Error(`Could not find item config for ${itemConfigId}`);\n    }\n\n    return { itemId: item.gameId, amount: 1 };\n}\n\nexport function rollGemType(): Item {\n    const roll = randomBetween(0, 3);\n    let itemConfigId;\n\n    if (roll === 0) {\n        itemConfigId = 'rs:uncut_diamond';\n    } else if (roll === 1) {\n        itemConfigId = 'rs:uncut_ruby';\n    } else if (roll === 2) {\n        itemConfigId = 'rs:uncut_emerald';\n    } else {\n        itemConfigId = 'rs:uncut_sapphire';\n    }\n\n    const item = findItem(itemConfigId);\n    if (!item) {\n        throw new Error(`Could not find item config for ${itemConfigId}`);\n    }\n\n    return { itemId: item.gameId, amount: 1 };\n}\n"
  },
  {
    "path": "src/engine/world/skill-util/harvest-skill.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport type { HarvestTool } from '@engine/world/config/harvest-tool';\nimport { getBestAxe } from '@engine/world/config/harvest-tool';\nimport type { IHarvestable } from '@engine/world/config/harvestable-object';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { logger } from '@runejs/common';\n\n/**\n * Check if a player can harvest a given {@link IHarvestable}\n *\n * @returns a {@link HarvestTool} if the player can harvest the object, or undefined if they cannot.\n */\nexport function canInitiateHarvest(player: Player, target: IHarvestable, skill: Skill): undefined | HarvestTool {\n    const itemConfigId = typeof target.items === 'string' ? target.items : target.items[0].itemConfigId;\n    const item = findItem(itemConfigId);\n\n    if (!item) {\n        logger.error(`Could not find item with config id ${itemConfigId} for harvestable object.`);\n        player.sendMessage('Sorry, there was an error. Please contact a developer.');\n        return;\n    }\n\n    let targetName = item.name.toLowerCase();\n\n    switch (skill) {\n        case Skill.MINING:\n            targetName = targetName.replace(' ore', '');\n            break;\n    }\n\n    // Rest of the function remains the same...\n    if (!player.skills.hasLevel(skill, target.level)) {\n        switch (skill) {\n            case Skill.WOODCUTTING:\n                player.sendMessage(`You need a Woodcutting level of ${target.level} to chop down this tree.`, true);\n                break;\n        }\n        return;\n    }\n\n    let tool;\n    switch (skill) {\n        case Skill.WOODCUTTING:\n            tool = getBestAxe(player);\n            break;\n    }\n\n    if (tool == null) {\n        switch (skill) {\n            case Skill.WOODCUTTING:\n                player.sendMessage('You do not have an axe for which you have the level to use.');\n                break;\n        }\n        return;\n    }\n\n    if (!player.inventory.hasSpace()) {\n        player.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true);\n        player.playSound(soundIds.inventoryFull);\n        return;\n    }\n\n    return tool;\n}\n"
  },
  {
    "path": "src/engine/world/sound/music.ts",
    "content": "export enum MusicPlayerMode {\n    MANUAL = 0,\n    AUTO = 1,\n}\n\nexport enum MusicPlayerLoopMode {\n    ENABLED = 0,\n    DISABLED = 1,\n}\n\nexport enum MusicTabButtonIds {\n    AUTO_BUTTON_ID = 180,\n    MANUAL_BUTTON_ID = 181,\n    LOOP_BUTTON_ID = 251,\n}\n"
  },
  {
    "path": "src/engine/world/task.ts",
    "content": "import { World } from '@engine/world/world';\nimport { lastValueFrom, timer } from 'rxjs';\nimport { take } from 'rxjs/operators';\n\nexport const schedule = async (ticks: number): Promise<number> => {\n    return lastValueFrom(timer(ticks * World.TICK_LENGTH).pipe(take(1)));\n};\n\nexport const wait = async (waitLength: number): Promise<number> => {\n    return lastValueFrom(timer(waitLength).pipe(take(1)));\n};\n"
  },
  {
    "path": "src/engine/world/world.ts",
    "content": "import Quadtree from 'quadtree-lib';\nimport { Subject, lastValueFrom } from 'rxjs';\nimport { take } from 'rxjs/operators';\nimport { v4 } from 'uuid';\n\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\n\nimport { loadActionFiles } from '@engine/action/loader';\nimport { findItem, findNpc, findObject, itemSpawns, npcSpawns } from '@engine/config/config-handler';\nimport { NpcSpawn } from '@engine/config/npc-spawn-config';\nimport { loadPlugins } from '@engine/plugins/loader';\nimport type { Task } from '@engine/task/task';\nimport { TaskScheduler } from '@engine/task/task-scheduler';\nimport { activeWorld } from '@engine/world';\nimport type { Actor } from '@engine/world/actor/actor';\nimport { Npc } from '@engine/world/actor/npc';\nimport { Player } from '@engine/world/actor/player/player';\nimport { ExamineCache } from '@engine/world/config/examine-data';\nimport { parseScenerySpawns } from '@engine/world/config/scenery-spawns';\nimport { TravelLocations } from '@engine/world/config/travel-locations';\nimport type { Direction } from '@engine/world/direction';\nimport { WorldInstance } from '@engine/world/instances';\nimport { ChunkManager } from '@engine/world/map/chunk-manager';\nimport type { ConstructedRegion } from '@engine/world/map/region';\nimport { getTemplateLocalX, getTemplateLocalY } from '@engine/world/map/region';\nimport { Position } from '@engine/world/position';\nimport { schedule } from '@engine/world/task';\nimport { isPlayer } from './actor/util';\n\nexport interface QuadtreeKey {\n    x: number;\n    y: number;\n    actor: Actor;\n}\n\n/**\n * Controls the game world and all entities within it.\n */\nexport class World {\n    public static readonly MAX_PLAYERS = 1600;\n    public static readonly MAX_NPCS = 30000;\n    public static readonly TICK_LENGTH = 600;\n\n    public readonly playerList: Player[] = new Array(World.MAX_PLAYERS).fill(null);\n    public readonly npcList: Npc[] = new Array(World.MAX_NPCS).fill(null);\n    public readonly chunkManager: ChunkManager = new ChunkManager();\n    public readonly examine: ExamineCache = new ExamineCache();\n    public readonly scenerySpawns: LandscapeObject[];\n    public readonly travelLocations: TravelLocations = new TravelLocations();\n    public readonly playerTree: Quadtree<QuadtreeKey>;\n    public readonly npcTree: Quadtree<QuadtreeKey>;\n    public readonly globalInstance = new WorldInstance(v4());\n    public readonly tickComplete: Subject<void> = new Subject<void>();\n    private readonly scheduler = new TaskScheduler();\n\n    private readonly debugCycleDuration: boolean = process.argv.indexOf('-tickTime') !== -1;\n\n    public constructor() {\n        this.scenerySpawns = parseScenerySpawns();\n        this.playerTree = new Quadtree<QuadtreeKey>({\n            width: 10000,\n            height: 10000,\n        });\n        this.npcTree = new Quadtree<QuadtreeKey>({\n            width: 10000,\n            height: 10000,\n        });\n\n        this.setupWorldTick();\n    }\n\n    public async startup(): Promise<void> {\n        await loadPlugins();\n        await loadActionFiles();\n        this.spawnGlobalNpcs();\n        this.spawnWorldItems();\n        this.spawnScenery();\n    }\n\n    public shutdown(): void {\n        this.kickAllPlayers();\n        logger.info(`Shutting down world...`);\n    }\n\n    /**\n     * Adds a task to the world scheduler queue. These tasks will run forever until they are cancelled.\n     *\n     * @warning Did you mean to add a world task, rather than an Actor task?\n     *\n     * If the task has a stack type of `NEVER`, other tasks in the same group will be cancelled.\n     *\n     * @param task The task to add\n     */\n    public enqueueTask(task: Task): void {\n        this.scheduler.enqueue(task);\n    }\n\n    /**\n     * Searched for an object by ID at the given position in any of the player's active instances.\n     * @param actor The actor to find the object for.\n     * @param objectId The game ID of the object.\n     * @param objectPosition The game world position that the object is expected at.\n     */\n    public findObjectAtLocation(\n        actor: Actor,\n        objectId: number,\n        objectPosition: Position,\n    ): { object: LandscapeObject | null; cacheOriginal: boolean } {\n        const x = objectPosition.x;\n        const y = objectPosition.y;\n\n        const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition);\n\n        let customMap = false;\n        if (isPlayer(actor) && actor.metadata.customMap) {\n            customMap = true;\n            const templateMapObject = this.findCustomMapObject(actor, objectId, objectPosition);\n            if (templateMapObject) {\n                return { object: templateMapObject, cacheOriginal: true };\n            }\n        }\n\n        let cacheOriginal = true;\n\n        let tileModifications;\n        let personalTileModifications;\n\n        if (isPlayer(actor)) {\n            const instance = actor.instance;\n\n            if (!instance) {\n                throw new Error(`Player ${actor.username} has no instance.`);\n            }\n\n            tileModifications = instance.getTileModifications(objectPosition);\n            personalTileModifications = actor.personalInstance.getTileModifications(objectPosition);\n        } else {\n            tileModifications = this.globalInstance.getTileModifications(objectPosition);\n        }\n\n        let landscapeObject = customMap ? null : objectChunk.getFilestoreLandscapeObject(objectId, objectPosition);\n        if (!landscapeObject) {\n            const tileObjects = [...tileModifications.mods.spawnedObjects];\n\n            if (isPlayer(actor)) {\n                tileObjects.push(...personalTileModifications.mods.spawnedObjects);\n            }\n\n            landscapeObject =\n                tileObjects.find(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) ||\n                null;\n\n            cacheOriginal = false;\n\n            if (!landscapeObject) {\n                return { object: null, cacheOriginal: false };\n            }\n        }\n\n        const hiddenTileObjects = [...tileModifications.mods.hiddenObjects];\n\n        if (isPlayer(actor)) {\n            hiddenTileObjects.push(...personalTileModifications.mods.hiddenObjects);\n        }\n\n        if (\n            hiddenTileObjects.findIndex(\n                spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y,\n            ) !== -1\n        ) {\n            return { object: null, cacheOriginal: false };\n        }\n\n        return {\n            object: landscapeObject,\n            cacheOriginal,\n        };\n    }\n\n    /**\n     * Locates a map template object from the actor's active custom map (if applicable).\n     * @param actor The actor to find the object for.\n     * @param objectId The ID of the object to find.\n     * @param objectPosition The position of the copied object to find the template of.\n     */\n    public findCustomMapObject(actor: Actor, objectId: number, objectPosition: Position): LandscapeObject | null {\n        const map = (actor?.metadata?.customMap as ConstructedRegion) || null;\n\n        if (!map) {\n            return null;\n        }\n\n        const objectConfig = findObject(objectId);\n\n        if (!objectConfig) {\n            return null;\n        }\n\n        const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition);\n        const mapChunk = activeWorld.chunkManager.getChunkForWorldPosition(map.renderPosition);\n\n        const chunkIndexX = objectChunk.position.x - (mapChunk.position.x - 2);\n        const chunkIndexY = objectChunk.position.y - (mapChunk.position.y - 2);\n\n        const objectTile = map.chunks[actor.position.level][chunkIndexX][chunkIndexY];\n\n        const tileX = objectTile.templatePosition.x;\n        const tileY = objectTile.templatePosition.y;\n        const tileOrientation = objectTile.orientation;\n\n        const objectLocalX = objectPosition.x - (objectChunk.position.x + 6) * 8;\n        const objectLocalY = objectPosition.y - (objectChunk.position.y + 6) * 8;\n\n        const mapTemplateWorldX = tileX;\n        const mapTemplateWorldY = tileY;\n        const mapTemplateChunk = activeWorld.chunkManager.getChunkForWorldPosition(\n            new Position(mapTemplateWorldX, mapTemplateWorldY, objectPosition.level),\n        );\n\n        const templateLocalX = getTemplateLocalX(\n            tileOrientation,\n            objectLocalX,\n            objectLocalY,\n            objectConfig?.rendering?.sizeX || 1,\n            objectConfig?.rendering?.sizeY || 1,\n        );\n        const templateLocalY = getTemplateLocalY(\n            tileOrientation,\n            objectLocalX,\n            objectLocalY,\n            objectConfig?.rendering?.sizeX || 1,\n            objectConfig?.rendering?.sizeY || 1,\n        );\n\n        const templateObjectPosition = new Position(\n            mapTemplateWorldX + templateLocalX,\n            mapTemplateWorldY + templateLocalY,\n            objectPosition.level,\n        );\n        const realObject = mapTemplateChunk.getFilestoreLandscapeObject(objectId, templateObjectPosition);\n\n        if (!realObject) {\n            return null;\n        }\n\n        realObject.x = objectPosition.x;\n        realObject.y = objectPosition.y;\n        realObject.level = objectPosition.level;\n\n        let rotation = realObject.orientation + objectTile.orientation;\n        if (rotation > 3) {\n            rotation -= 4;\n        }\n\n        realObject.orientation = rotation;\n\n        return realObject || null;\n    }\n\n    /**\n     * Saves player data for every active player within the game world before logging\n     * them out for a gentle game server shutdown.\n     */\n    public kickAllPlayers(): void {\n        if (!this.playerList) {\n            return;\n        }\n\n        logger.info(`Kicking all players...`);\n\n        this.playerList.filter(player => player !== null).forEach(player => player.logout());\n\n        logger.info(`Player data save complete, world is now empty.`);\n    }\n\n    /**\n     * Saves player data for every active player within the game world.\n     */\n    public saveOnlinePlayers(): void {\n        if (!this.playerList) {\n            return;\n        }\n\n        logger.info(`Saving player data...`);\n\n        this.playerList.filter(player => player !== null).forEach(player => player.save());\n\n        logger.info(`Player data saved.`);\n    }\n\n    /**\n     * Players a sound at a specific position for all players within range of that position.\n     * @param position The position to play the sound at.\n     * @param soundId The ID of the sound effect.\n     * @param volume The volume the sound should play at.\n     * @param distance The distance which the sound should reach.\n     */\n    public playLocationSound(position: Position, instanceId: string, soundId: number, volume: number, distance: number = 10): void {\n        this.findNearbyPlayers(position, distance, instanceId).forEach(player => {\n            player.outgoingPackets.updateReferencePosition(position);\n            player.outgoingPackets.playSoundAtPosition(soundId, position.x, position.y, volume);\n        });\n    }\n\n    /**\n     * Finds all NPCs within the given distance from the given position that have the specified Npc ID.\n     * @param position The center position to search from.\n     * @param npcId The ID of the NPCs to find.\n     * @param distance The maximum distance to search for NPCs.\n     * @param instanceId The NPC's active instance.\n     */\n    public findNearbyNpcsById(\n        position: Position,\n        npcId: number,\n        distance: number,\n        instanceId: string = activeWorld.globalInstance.instanceId,\n    ): Npc[] {\n        return this.npcTree\n            .colliding({\n                x: position.x - distance / 2,\n                y: position.y - distance / 2,\n                width: distance,\n                height: distance,\n            })\n            .map(quadree => quadree.actor as Npc)\n            .filter(npc => npc.id === npcId && npc.instanceId === instanceId);\n    }\n\n    /**\n     * Finds all NPCs within the game world that have the specified Npc Key.\n     * @param npcKey The Key of the NPCs to find.\n     * @param instanceId The NPC's active instance.\n     */\n    public findNpcsByKey(npcKey: string, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] {\n        return this.npcList.filter(npc => npc && npc.key === npcKey && npc.instanceId === instanceId);\n    }\n\n    /**\n     * Finds all NPCs within the game world that have the specified Npc ID.\n     * @param npcId The ID of the NPCs to find.\n     * @param instanceId The NPC's active instance.\n     */\n    public findNpcsById(npcId: number, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] {\n        return this.npcList.filter(npc => npc && npc.id === npcId && npc.instanceId === instanceId);\n    }\n\n    /**\n     * Finds all NPCs within the specified instance.\n     * @param instanceId The NPC's active instance.\n     */\n    public findNpcsByInstance(instanceId: string): Npc[] {\n        return this.npcList.filter(npc => npc && npc.instanceId === instanceId);\n    }\n\n    /**\n     * Finds all NPCs within the given distance from the given position.\n     * @param position The center position to search from.\n     * @param distance The maximum distance to search for NPCs.\n     * @param instanceId The NPC's active instance.\n     */\n    public findNearbyNpcs(position: Position, distance: number, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] {\n        return this.npcTree\n            .colliding({\n                x: position.x - distance / 2,\n                y: position.y - distance / 2,\n                width: distance,\n                height: distance,\n            })\n            .map(quadree => quadree.actor as Npc)\n            .filter(npc => npc.instanceId === instanceId);\n    }\n\n    /**\n     * Finds all Players within the given distance from the given position.\n     * @param position The center position to search from.\n     * @param distance The maximum distance to search for Players.\n     * @param instanceId The player's active instance.\n     */\n    public findNearbyPlayers(position: Position, distance: number, instanceId: string): Player[] {\n        return this.playerTree\n            .colliding({\n                x: position.x - distance / 2,\n                y: position.y - distance / 2,\n                width: distance,\n                height: distance,\n            })\n            .map(quadree => quadree.actor as Player)\n            .filter(player => player.personalInstance.instanceId === instanceId || player.instance?.instanceId === instanceId);\n    }\n\n    /**\n     * Finds a logged in player via their username.\n     * @param username The player's username.\n     */\n    public findActivePlayerByUsername(username: string): Player | null {\n        username = username.toLowerCase();\n        return this.playerList.find(p => p && p.username.toLowerCase() === username) || null;\n    }\n\n    /**\n     * Spawns the list of pre-configured items into either the global instance or a player's personal instance.\n     * @param player [optional] The player to load the instanced items for. Uses the global world instance if not provided.\n     */\n    public spawnWorldItems(player?: Player): void {\n        const instance = player ? player.personalInstance : this.globalInstance;\n\n        itemSpawns\n            .filter(spawn => (player ? spawn.instance === 'player' : spawn.instance === 'global'))\n            .forEach(itemSpawn => {\n                const itemDetails = findItem(itemSpawn.itemKey);\n                if (itemDetails && itemDetails.gameId !== undefined) {\n                    instance.spawnWorldItem({ itemId: itemDetails.gameId, amount: itemSpawn.amount }, itemSpawn.spawnPosition, {\n                        respawns: itemSpawn.respawn,\n                        owner: player || undefined,\n                    });\n                } else {\n                    logger.error(`Item ${itemSpawn.itemKey} can not be spawned; it has not yet been registered on the server.`);\n                }\n            });\n    }\n\n    public spawnGlobalNpcs(): void {\n        npcSpawns.forEach(npcSpawn => {\n            const npcDetails = findNpc(npcSpawn.npcKey);\n            this.registerNpc(new Npc(npcDetails, npcSpawn));\n        });\n    }\n\n    public async spawnNpc(\n        npcKey: string | number,\n        position: Position,\n        face: Direction,\n        movementRadius: number = 0,\n        instanceId: string = activeWorld.globalInstance.instanceId,\n    ): Promise<Npc> {\n        if (!npcKey) {\n            throw new Error('NPC key must be provided.');\n        }\n\n        const npcData = findNpc(npcKey);\n        const npc = new Npc(npcData, new NpcSpawn(npcData.key ? npcData.key : `unknown_${npcData}`, position, movementRadius, face));\n\n        // TODO (jkm) this function doesn't use the passed in `instanceId`!\n\n        await this.registerNpc(npc);\n\n        return npc;\n    }\n\n    public spawnScenery(): void {\n        this.scenerySpawns.forEach(locationObject => this.globalInstance.spawnGameObject(locationObject));\n    }\n\n    public async setupWorldTick(): Promise<void> {\n        await schedule(1);\n        this.worldTick();\n    }\n\n    public generateFakePlayers(): void {\n        const x: number = 3222;\n        const y: number = 3222;\n        let xOffset: number = 0;\n        let yOffset: number = 0;\n\n        const spawnChunk = this.chunkManager.getChunkForWorldPosition(new Position(x, y, 0));\n\n        for (let i = 0; i < 1000; i++) {\n            // TODO (Jameskmonger) we should be able to create a player without a connection, and without passing nulls in\n            const player = new Player(null as any, null as any, null as any, i, `test${i}`, 'abs', true);\n            this.registerPlayer(player);\n            player.interfaceState.closeAllSlots();\n\n            xOffset++;\n\n            if (xOffset > 20) {\n                xOffset = 0;\n                yOffset--;\n            }\n\n            player.position = new Position(x + xOffset, y + yOffset, 0);\n            const newChunk = this.chunkManager.getChunkForWorldPosition(player.position);\n\n            if (!spawnChunk.equals(newChunk)) {\n                spawnChunk.removePlayer(player);\n                newChunk.addPlayer(player);\n            }\n\n            player.initiateRandomMovement();\n        }\n    }\n\n    public async worldTick(): Promise<void> {\n        const hrStart = Date.now();\n\n        this.scheduler.tick();\n\n        const activePlayers: Player[] = this.playerList.filter(player => player !== null);\n\n        if (activePlayers.length === 0) {\n            return Promise.resolve().then(() => {\n                setTimeout(async () => this.worldTick(), World.TICK_LENGTH); //TODO: subtract processing time\n            });\n        }\n\n        const activeNpcs: Npc[] = this.npcList.filter(npc => npc !== null);\n\n        await Promise.all([...activePlayers.map(async player => player.tick()), ...activeNpcs.map(async npc => npc.tick())]);\n        await Promise.all(activePlayers.map(async player => player.update()));\n        await Promise.all([...activePlayers.map(async player => player.reset()), ...activeNpcs.map(async npc => npc.reset())]);\n\n        const hrEnd = Date.now();\n        const duration = hrEnd - hrStart;\n        const delay = Math.max(World.TICK_LENGTH - duration, 0);\n\n        if (this.debugCycleDuration) {\n            logger.info(`World tick completed in ${duration} ms, next tick in ${delay} ms.`);\n        }\n\n        setTimeout(async () => this.worldTick(), delay);\n        this.tickComplete.next();\n        return Promise.resolve();\n    }\n\n    public async nextTick(): Promise<void> {\n        await lastValueFrom(this.tickComplete.asObservable().pipe(take(1)));\n    }\n\n    public async ticks(count: number): Promise<void> {\n        await lastValueFrom(this.tickComplete.asObservable().pipe(take(count)));\n    }\n\n    public async scheduleNpcRespawn(npc: Npc): Promise<boolean> {\n        await schedule(10);\n        return await this.registerNpc(npc);\n    }\n\n    /**\n     * Returns the number of remaining open player slots before this world reaches maximum capacity.\n     */\n    public playerSlotsRemaining(): number {\n        return this.playerList.filter(player => !player).length;\n    }\n\n    public findPlayer(playerUsername: string): Player | null {\n        playerUsername = playerUsername.toLowerCase();\n        return this.playerList?.find(p => Boolean(p) && p.username.toLowerCase() === playerUsername) || null;\n    }\n\n    public playerOnline(player: Player | string): boolean {\n        if (typeof player === 'string') {\n            player = player.toLowerCase();\n            return this.playerList.findIndex(p => Boolean(p) && p.username.toLowerCase() === player) !== -1;\n        } else {\n            const foundPlayer = this.playerList[player.worldIndex];\n            if (!foundPlayer) {\n                return false;\n            }\n\n            return foundPlayer.equals(player);\n        }\n    }\n\n    /**\n     * Registers a new player to the game world.\n     * Returns false if the world is full, otherwise returns true when the player has been registered.\n     * @param player The player to register.\n     */\n    public registerPlayer(player: Player): boolean {\n        if (!player) {\n            return false;\n        }\n\n        const index = this.playerList.findIndex(p => p === null);\n\n        if (index === -1) {\n            logger.warn('World full!');\n            return false;\n        }\n\n        player.worldIndex = index;\n        this.playerList[index] = player;\n        return true;\n    }\n\n    /**\n     * Clears the given player's game world slot, signalling that they have disconnected fully.\n     * @param player The player to remove from the world list.\n     */\n    public deregisterPlayer(player: Player): void {\n        delete this.playerList[player.worldIndex];\n    }\n\n    public npcExists(npc: Npc): boolean {\n        const foundNpc = this.npcList[npc.worldIndex];\n        if (!foundNpc || !foundNpc.exists) {\n            return false;\n        }\n\n        return foundNpc.equals(npc);\n    }\n\n    public async registerNpc(npc: Npc): Promise<boolean> {\n        if (!npc) {\n            return false;\n        }\n\n        const index = this.npcList.findIndex(n => n === null);\n\n        if (index === -1) {\n            logger.warn('NPC list full!');\n            return false;\n        }\n\n        npc.worldIndex = index;\n        this.npcList[index] = npc;\n        await npc.init();\n        return true;\n    }\n\n    public deregisterNpc(npc: Npc): void {\n        npc.exists = false;\n        delete this.npcList[npc.worldIndex];\n    }\n}\n"
  },
  {
    "path": "src/plugins/buttons/logout-button.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { activeWorld } from '@engine/world';\n\nexport const handler: buttonActionHandler = details => {\n    const { player } = details;\n    const playerName = player.username.toLowerCase();\n    player.logout();\n\n    // Update online players friends lists that have this player as a friend\n    const otherPlayers = activeWorld.playerList.filter(p => p && p.friendsList.indexOf(playerName) !== -1);\n    if (otherPlayers && otherPlayers.length !== 0) {\n        otherPlayers.forEach(otherPlayer => otherPlayer.outgoingPackets.updateFriendStatus(playerName, 0));\n    }\n};\n\nexport default {\n    pluginId: 'rs:logout_button',\n    hooks: [\n        {\n            type: 'button',\n            widgetId: widgets.logoutTab,\n            buttonIds: 6,\n            handler,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/buttons/magic-attack.plugin.ts",
    "content": "import type { TaskExecutor } from '@engine/action/hook/task';\nimport type { MagicOnNPCAction, MagicOnNPCActionHook } from '@engine/action/pipe/magic-on-npc.action';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\n\nconst buttonIds: number[] = [\n    0, // Home Teleport\n];\n\nfunction attack_target(player: Player, elapsedTicks: number): boolean {\n    logger.info('attacking?');\n    return true;\n}\n\nconst spells = ['Wind Strike', 'Confuse', 'Water Strike', 'unknown?', 'Earth Strike'];\nexport const activate = (task: TaskExecutor<MagicOnNPCAction>, elapsedTicks: number = 0) => {\n    const { npc, player, widgetId, buttonId } = task.actionData;\n\n    const attackerX = player.position.x;\n    const attackerY = player.position.y;\n    const victimX = npc.position.x;\n    const victimY = npc.position.y;\n    const offsetX = victimY - attackerY;\n    const offsetY = victimX - attackerX;\n\n    player.walkingQueue.clear();\n\n    //npc world index would be -1 for players\n    player.outgoingPackets.sendProjectile(player.position, offsetX, offsetY, 250, 40, 36, 100, npc.worldIndex + 1, 1);\n    console.info(`${player.username} smites ${npc.name} with ${spells[buttonId]}`);\n};\n\nexport default {\n    pluginId: 'rs:magic',\n    hooks: {\n        type: 'magic_on_npc',\n        widgetId: 192,\n        buttonIds: buttonIds,\n        task: {\n            activate,\n            interval: 0,\n        },\n    } as MagicOnNPCActionHook,\n};\n"
  },
  {
    "path": "src/plugins/buttons/magic-teleports.plugin.ts",
    "content": "import type { TaskExecutor } from '@engine/action/hook/task';\nimport type { ButtonAction, ButtonActionHook } from '@engine/action/pipe/button.action';\nimport { QueueableTask } from '@engine/action/pipe/task/queueable-task';\nimport { widgets } from '@engine/config/config-handler';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { gfxIds } from '@engine/world/config/gfx-ids';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { TravelLocation } from '@engine/world/config/travel-locations';\nimport type { Item } from '@engine/world/items/item';\nimport { Position } from '@engine/world/position';\nimport { openHouse } from '@plugins/skills/construction/house';\nimport { serverConfig } from '@server/game/game-server';\n\nenum Teleports {\n    Home = 591,\n    House = 581,\n    Varrock = 12,\n    Lumbridge = 15,\n    Falador = 18,\n    Camelot = 22,\n    Ardougne = 388,\n    Watchtower = 389,\n    Trollheim = 492,\n    Ape_atoll = 569,\n}\n\n/**\n * Keeps track of the cost of performing basic teleport spells.\n *\n * As of 2024-09-02 the magic system isn't fully implemented, so there isn't\n * really a centralized location for storing and processing spell costs.\n * Defining it here is the alternative.\n *\n * If needed, it can be exported, but it's not exported in order to keep this\n * plugin self-contained.\n */\nconst MagicCosts: Record<number, MagicCost> = {\n    [Teleports.Varrock]: {\n        [itemIds.runes.air]: 3,\n        [itemIds.runes.law]: 1,\n        [itemIds.runes.fire]: 1,\n    },\n    [Teleports.Lumbridge]: {\n        [itemIds.runes.air]: 3,\n        [itemIds.runes.law]: 1,\n        [itemIds.runes.earth]: 1,\n    },\n    [Teleports.Falador]: {\n        [itemIds.runes.air]: 3,\n        [itemIds.runes.law]: 1,\n        [itemIds.runes.water]: 1,\n    },\n    [Teleports.House]: {\n        [itemIds.runes.air]: 1,\n        [itemIds.runes.law]: 1,\n        [itemIds.runes.earth]: 1,\n    },\n    [Teleports.Camelot]: {\n        [itemIds.runes.air]: 5,\n        [itemIds.runes.law]: 1,\n    },\n    [Teleports.Ardougne]: {\n        [itemIds.runes.water]: 2,\n        [itemIds.runes.law]: 2,\n    },\n    [Teleports.Watchtower]: {\n        [itemIds.runes.law]: 2,\n        [itemIds.runes.earth]: 2,\n    },\n    [Teleports.Trollheim]: {\n        [itemIds.runes.law]: 2,\n        [itemIds.runes.fire]: 2,\n    },\n    [Teleports.Ape_atoll]: {\n        [itemIds.runes.fire]: 2,\n        [itemIds.runes.law]: 2,\n        [itemIds.runes.water]: 2,\n        [itemIds.banana]: 1,\n    },\n};\n\n/**\n * Mapping of the various teleport locations. Some are usable directly from\n * the `activeWorld.travelLocations` lookups, but others are not.\n */\nconst TeleportLocations: Record<number, Position> = {\n    [Teleports.Home]: new Position(3218, 3218),\n    [Teleports.Varrock]: new Position(3212, 3424),\n    [Teleports.Lumbridge]: new Position(3224, 3218),\n    [Teleports.Falador]: new Position(2965, 3380),\n    [Teleports.Camelot]: new Position(2757, 3478),\n    [Teleports.Ardougne]: new Position(2662, 3307),\n    [Teleports.Watchtower]: new Position(2934, 4714, 2),\n    [Teleports.Trollheim]: (activeWorld.travelLocations.find('Trollheim') as TravelLocation).position,\n    [Teleports.Ape_atoll]: new Position(2798, 2798, 1),\n};\n\nconst TeleportXP: Record<number, number> = {\n    [Teleports.Varrock]: 35,\n    [Teleports.Lumbridge]: 41,\n    [Teleports.Falador]: 48,\n    [Teleports.House]: 30,\n    [Teleports.Camelot]: 55.5,\n    [Teleports.Ardougne]: 61,\n    [Teleports.Watchtower]: 68,\n    [Teleports.Trollheim]: 68,\n    [Teleports.Ape_atoll]: 74,\n};\n\nconst buttonIds: number[] = [\n    Teleports.Home,\n    Teleports.Varrock,\n    Teleports.Lumbridge,\n    Teleports.Falador,\n    Teleports.House,\n    Teleports.Camelot,\n    Teleports.Ardougne,\n    Teleports.Watchtower,\n    Teleports.Trollheim,\n    Teleports.Ape_atoll,\n];\n\nfunction queueTeleport(player: Player, pos: Position) {\n    player.enqueueBaseTask(\n        new QueueableTask(\n            [],\n            player,\n            () => {\n                player.teleport(pos);\n                player.metadata.castingStationarySpell = false;\n                return {\n                    callbackResult: false,\n                    shouldContinueLooping: false,\n                };\n            },\n            null,\n            null,\n        ),\n    );\n}\n\n/**\n * Casts the home teleport spell (not their player owned home).\n *\n * @param elapsedTicks A counter of the number of elapsed ticks since the\n * teleport started. Used to increment through the teleporting animation up\n * until the actual teleport occurs.\n * @returns `true` once the teleport finishes, `false` until it finishes\n */\nfunction homeTeleport(player: Player, elapsedTicks: number): boolean {\n    if (elapsedTicks === 0) {\n        player.playAnimation(animationIds.homeTeleportDraw);\n        player.playGraphics({ id: gfxIds.homeTeleportDraw, delay: 0, height: 0 });\n        player.outgoingPackets.playSound(soundIds.homeTeleportDraw, 10);\n    } else if (elapsedTicks === 7) {\n        player.playAnimation(animationIds.homeTeleportSit);\n        player.playGraphics({ id: gfxIds.homeTeleportFullDrawnCircle, delay: 0, height: 0 });\n        player.outgoingPackets.playSound(soundIds.homeTeleportSit, 10);\n    } else if (elapsedTicks === 12) {\n        player.playAnimation(animationIds.homeTeleportPullOutAndReadBook);\n        player.playGraphics({ id: gfxIds.homeTeleportPullOutBook, delay: 0, height: 0 });\n        player.outgoingPackets.playSound(soundIds.homeTeleportPullOutBook, 10);\n    } else if (elapsedTicks === 16) {\n        player.playAnimation(animationIds.homeTeleportReadBookAndGlowCircle);\n        player.playGraphics({ id: gfxIds.homeTeleportCircleGlow, delay: 0, height: 0 });\n        player.outgoingPackets.playSound(soundIds.homeTeleportCircleGlowAndTeleport, 10);\n    } else if (elapsedTicks === 20) {\n        player.playAnimation(animationIds.homeTeleport);\n        player.playGraphics({ id: gfxIds.homeTeleport, delay: 0, height: 0 });\n    } else if (elapsedTicks === 22) {\n        queueTeleport(player, TeleportLocations[Teleports.Home]);\n        return true;\n    }\n\n    return false;\n}\n\ntype MagicCost = Record<number, number>;\n\n/**\n * Determines if the player currently has infinite quantities of a resource,\n * such as a fire staff for fire runes.\n *\n * @param resource The item ID for the resource, such as a fire rune.\n */\nfunction hasInfinite(player: Player, resource: number): boolean {\n    switch (resource) {\n        case itemIds.runes.air: {\n            if (player.equipment.has(itemIds.staffs.air)) {\n                return true;\n            }\n            break;\n        }\n        case itemIds.runes.fire: {\n            if (player.equipment.has(itemIds.staffs.fire)) {\n                return true;\n            }\n            break;\n        }\n        case itemIds.runes.water: {\n            if (player.equipment.has(itemIds.staffs.water)) {\n                return true;\n            }\n            break;\n        }\n        case itemIds.runes.earth: {\n            if (player.equipment.has(itemIds.staffs.earth)) {\n                return true;\n            }\n            break;\n        }\n    }\n\n    return false;\n}\n\n/**\n * Deducts the cost of the spell from the player's inventory.\n *\n * @returns `false` if the player lacks the required runes\n */\nfunction expenseMagic(player: Player, cost: MagicCost): boolean {\n    if (!cost) return true;\n\n    const indexesToUpdate: number[] = [];\n    const itemsToUpdate: Record<number, Item> = [];\n\n    for (const requiredItemId in cost) {\n        const itemId: number = Number(requiredItemId);\n        if (hasInfinite(player, itemId)) {\n            continue;\n        }\n\n        const itemIndex: number = player.inventory.findIndex(itemId);\n        if (itemIndex < 0) {\n            return false;\n        }\n\n        const newItem: Item = {\n            amount: player.inventory.amount(itemId) - cost[requiredItemId],\n            itemId: itemId,\n        };\n\n        if (newItem.amount < 0) {\n            return false;\n        }\n\n        itemsToUpdate[itemIndex] = newItem;\n        indexesToUpdate.push(itemIndex);\n    }\n\n    for (let i = 0; i < indexesToUpdate.length; i++) {\n        if (itemsToUpdate[indexesToUpdate[i]].amount === 0) {\n            player.inventory.remove(indexesToUpdate[i]);\n        } else {\n            player.inventory.set(indexesToUpdate[i], itemsToUpdate[indexesToUpdate[i]]);\n        }\n    }\n\n    return true;\n}\n\nfunction genericTeleport(player: Player, elapsedTicks: number, target: Position, teleportId?: number): boolean {\n    if (elapsedTicks === 0) {\n        player.playAnimation(animationIds.teleport);\n        player.outgoingPackets.playSound(soundIds.teleport, 10);\n        player.playGraphics({ id: gfxIds.teleport, delay: 0, height: 100 });\n    } else if (elapsedTicks === 3) {\n        switch (teleportId) {\n            case Teleports.House: {\n                openHouse(player);\n                break;\n            }\n            default: {\n                queueTeleport(player, target);\n\n                // warning: undefined xp values cause the xp to reset to 0,\n                // so make sure to always assert that it's defined\n                if (teleportId && TeleportXP[teleportId]) {\n                    player.enqueueBaseTask(\n                        new QueueableTask(\n                            [],\n                            player,\n                            () => {\n                                player.skills.addExp(Skill.MAGIC, TeleportXP[teleportId]);\n                                return { callbackResult: false, shouldContinueLooping: false };\n                            },\n                            null,\n                            null,\n                        ),\n                    );\n                }\n                break;\n            }\n        }\n        player.playAnimation(animationIds.reset);\n        return true;\n    }\n\n    return false;\n}\n\nconst insufficient = 'You do not have enough runes to cast this spell.';\n\nconst activate = (task: TaskExecutor<ButtonAction>, elapsedTicks: number = 0) => {\n    const { player, buttonId } = task.actionData;\n\n    let completed: boolean = false;\n\n    switch (buttonId) {\n        case Teleports.Home:\n            completed = homeTeleport(player, elapsedTicks);\n            break;\n        case Teleports.Varrock:\n        case Teleports.Lumbridge:\n        case Teleports.Falador:\n        case Teleports.House:\n        case Teleports.Camelot:\n        case Teleports.Ardougne:\n        case Teleports.Watchtower:\n        case Teleports.Trollheim:\n        case Teleports.Ape_atoll: {\n            if (elapsedTicks === 0) {\n                // prevents the player from spamming the spell\n                if (player.metadata?.castingStationarySpell) {\n                    player.sendMessage('You are already teleporting.');\n                    task.stop();\n                    return;\n                }\n\n                player.metadata.castingStationarySpell = true;\n\n                if (!serverConfig.bypassTeleportRequirements && !expenseMagic(player, MagicCosts[buttonId])) {\n                    player.sendMessage(insufficient);\n                    completed = true;\n                    break;\n                }\n\n                player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n            }\n\n            completed = genericTeleport(player, elapsedTicks, TeleportLocations[buttonId], buttonId);\n\n            break;\n        }\n    }\n\n    if (completed) {\n        player.metadata.castingStationarySpell = false;\n        task.stop();\n    }\n};\n\nexport default {\n    pluginId: 'rs:magic_teleports',\n    hooks: [\n        {\n            type: 'button',\n            widgetId: 192,\n            buttonIds: buttonIds,\n            task: {\n                activate,\n                interval: 1,\n            },\n        } as ButtonActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/buttons/player-emotes.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { itemIds } from '@engine/world/config/item-ids';\n\ninterface Emote {\n    animationId: number;\n    name: string;\n    unlockable?: boolean;\n    graphicId?: number;\n}\n\ninterface SkillcapeEmote extends Emote {\n    itemIds: Array<number>;\n}\n\nconst { skillCapes } = itemIds;\n\nexport const skillCapeEmotes: SkillcapeEmote[] = [\n    { animationId: 4959, name: 'Attack', itemIds: [skillCapes.attack.untrimmed, skillCapes.attack.trimmed], graphicId: 823 },\n    { animationId: 4981, name: 'Strength', itemIds: [skillCapes.strength.untrimmed, skillCapes.strength.trimmed], graphicId: 828 },\n    { animationId: 4961, name: 'Defence', itemIds: [skillCapes.defence.untrimmed, skillCapes.defence.trimmed], graphicId: 824 },\n    { animationId: 4973, name: 'Ranged', itemIds: [skillCapes.ranged.untrimmed, skillCapes.ranged.trimmed], graphicId: 832 },\n    { animationId: 4979, name: 'Prayer', itemIds: [skillCapes.prayer.untrimmed, skillCapes.prayer.trimmed], graphicId: 829 },\n    { animationId: 4939, name: 'Magic', itemIds: [skillCapes.magic.untrimmed, skillCapes.magic.trimmed], graphicId: 813 },\n    {\n        animationId: 4947,\n        name: 'Runecrafting',\n        itemIds: [skillCapes.runecrafting.untrimmed, skillCapes.runecrafting.trimmed],\n        graphicId: 817,\n    },\n    {\n        animationId: 4971,\n        name: 'Constitution',\n        itemIds: [skillCapes.constitution.untrimmed, skillCapes.constitution.trimmed],\n        graphicId: 833,\n    },\n    { animationId: 4977, name: 'Agility', itemIds: [skillCapes.agility.untrimmed, skillCapes.agility.trimmed], graphicId: 830 },\n    { animationId: 4969, name: 'Herblore', itemIds: [skillCapes.herblore.untrimmed, skillCapes.herblore.trimmed], graphicId: 835 },\n    { animationId: 4965, name: 'Thieving', itemIds: [skillCapes.thieving.untrimmed, skillCapes.thieving.trimmed], graphicId: 826 },\n    { animationId: 4949, name: 'Crafting', itemIds: [skillCapes.crafting.untrimmed, skillCapes.crafting.trimmed], graphicId: 818 },\n    { animationId: 4937, name: 'Fletching', itemIds: [skillCapes.fletching.untrimmed, skillCapes.fletching.trimmed], graphicId: 812 },\n    { animationId: 4967, name: 'Slayer', itemIds: [skillCapes.slayer.untrimmed, skillCapes.slayer.trimmed], graphicId: 827 },\n    {\n        animationId: 4953,\n        name: 'Construction',\n        itemIds: [skillCapes.construction.untrimmed, skillCapes.construction.trimmed],\n        graphicId: 820,\n    },\n    { animationId: 4941, name: 'Mining', itemIds: [skillCapes.mining.untrimmed, skillCapes.mining.trimmed], graphicId: 814 },\n    { animationId: 4943, name: 'Smithing', itemIds: [skillCapes.smithing.untrimmed, skillCapes.smithing.trimmed], graphicId: 815 },\n    { animationId: 4951, name: 'Fishing', itemIds: [skillCapes.fishing.untrimmed, skillCapes.fishing.trimmed], graphicId: 819 },\n    { animationId: 4955, name: 'Cooking', itemIds: [skillCapes.cooking.untrimmed, skillCapes.cooking.trimmed], graphicId: 821 },\n    { animationId: 4975, name: 'Firemaking', itemIds: [skillCapes.firemaking.untrimmed, skillCapes.firemaking.trimmed], graphicId: 831 },\n    { animationId: 4957, name: 'Woodcutting', itemIds: [skillCapes.woodcutting.untrimmed, skillCapes.woodcutting.trimmed], graphicId: 822 },\n    { animationId: 4963, name: 'Farming', itemIds: [skillCapes.farming.untrimmed, skillCapes.farming.trimmed], graphicId: 825 },\n    { animationId: 4945, name: 'Quest point', itemIds: [skillCapes.questpoint.untrimmed], graphicId: 816 },\n];\n\nexport const emotes: { [key: number]: Emote } = {\n    1: { animationId: 855, name: 'YES' },\n    2: { animationId: 856, name: 'NO' },\n    3: { animationId: 858, name: 'BOW' },\n    4: { animationId: 859, name: 'ANGRY' },\n    5: { animationId: 857, name: 'THINKING' },\n    6: { animationId: 863, name: 'WAVE' },\n    7: { animationId: 2113, name: 'SHRUG' },\n    8: { animationId: 862, name: 'CHEER' },\n    9: { animationId: 864, name: 'BECKON' },\n    10: { animationId: 861, name: 'LAUGH' },\n    11: { animationId: 2109, name: 'JUMP FOR JOY' },\n    12: { animationId: 2111, name: 'YAWN' },\n    13: { animationId: 866, name: 'DANCE' },\n    14: { animationId: 2106, name: 'JIG' },\n    15: { animationId: 2107, name: 'SPIN' },\n    16: { animationId: 2108, name: 'HEADBANG' },\n    17: { animationId: 860, name: 'CRY' },\n    18: { animationId: 1368, name: 'BLOW KISS' },\n    19: { animationId: 2105, name: 'PANIC' },\n    20: { animationId: 2110, name: 'RASPBERRY' },\n    21: { animationId: 865, name: 'CLAP' },\n    22: { animationId: 2112, name: 'SALUTE' },\n    23: { animationId: 2127, name: 'GOBLIN BOW', unlockable: true },\n    24: { animationId: 2128, name: 'GOBLIN SALUTE', unlockable: true },\n    25: { animationId: 1131, name: 'GLASS BOX', unlockable: true },\n    26: { animationId: 1130, name: 'CLIMB ROPE', unlockable: true },\n    27: { animationId: 1129, name: 'LEAN', unlockable: true },\n    28: { animationId: 1128, name: 'GLASS WALL', unlockable: true },\n    32: { animationId: 4276, name: 'IDEA', unlockable: true, graphicId: 712 },\n    30: { animationId: 4278, name: 'STAMP', unlockable: true },\n    31: { animationId: 4280, name: 'FLAP', unlockable: true },\n    29: { animationId: 4275, name: 'FACEPALM', unlockable: true },\n    33: { animationId: 3544, name: 'ZOMBIE WALK', unlockable: true },\n    34: { animationId: 3543, name: 'ZOMBIE DANCE', unlockable: true },\n    35: { animationId: 2836, name: 'SCARED', unlockable: true },\n    36: { animationId: 6111, name: 'RABBIT HOP', unlockable: true }, // @TODO missing in 435 cache???\n    37: { animationId: -1, name: 'SKILLCAPE' }, // @TODO skillcape emotes\n};\n\nexport function unlockEmote(player: Player, emoteName: string): void {\n    const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || [];\n    if (unlockedEmotes.indexOf(emoteName) === -1) {\n        unlockedEmotes.push(emoteName);\n        player.savedMetadata.unlockedEmotes = unlockedEmotes;\n    }\n    unlockEmotes(player);\n}\n\nexport function lockEmote(player: Player, emoteName: string): void {\n    const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || [];\n    const index = unlockedEmotes.indexOf(emoteName);\n\n    if (index !== -1) {\n        unlockedEmotes.splice(index, 1);\n        player.savedMetadata.unlockedEmotes = unlockedEmotes;\n    }\n\n    unlockEmotes(player);\n}\n\nexport function unlockEmotes(player: Player): void {\n    let sosConfig = 0;\n    let eventConfig = 0;\n    let goblinConfig = 0;\n\n    const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || [];\n\n    for (const name of unlockedEmotes) {\n        if ((name === 'GOBLIN BOW' || name === 'GOBLIN SALUTE') && goblinConfig === 0) goblinConfig += 7;\n        if (name === 'FLAP') sosConfig += 1;\n        if (name === 'FACEPALM') sosConfig += 2;\n        if (name === 'IDEA') sosConfig += 4;\n        if (name === 'STAMP') sosConfig += 8;\n        if (name === 'GLASS WALL') eventConfig += 1;\n        if (name === 'GLASS BOX') eventConfig += 2;\n        if (name === 'CLIMB ROPE') eventConfig += 4;\n        if (name === 'LEAN') eventConfig += 8;\n        if (name === 'SCARED') eventConfig += 16;\n        if (name === 'ZOMBIE DANCE') eventConfig += 32;\n        if (name === 'ZOMBIE WALK') eventConfig += 64;\n        if (name === 'RABBIT HOP') eventConfig += 128;\n        if (name === 'SKILLCAPE') eventConfig += 256;\n    }\n\n    player.outgoingPackets.updateClientConfig(465, goblinConfig);\n    player.outgoingPackets.updateClientConfig(802, sosConfig);\n    player.outgoingPackets.updateClientConfig(313, eventConfig);\n}\n\nconst buttonIds = Object.keys(emotes).map(v => parseInt(v));\n\nexport const handler: buttonActionHandler = details => {\n    const { player, buttonId } = details;\n\n    const emote = emotes[buttonId];\n\n    if (emote.name === 'SKILLCAPE') {\n        const equippedBackItem = player.getEquippedItem('back');\n\n        if (equippedBackItem) {\n            if (skillCapeEmotes.some(item => item.itemIds.includes(equippedBackItem.itemId))) {\n                const skillcapeEmote = skillCapeEmotes.filter(item => item.itemIds.includes(equippedBackItem.itemId));\n                player.playAnimation(skillcapeEmote[0].animationId);\n\n                if (skillcapeEmote[0].graphicId) {\n                    player.playGraphics({ id: skillcapeEmote[0].graphicId, delay: 0, height: 0 });\n                }\n            }\n        } else {\n            player.sendMessage(`You need to be wearing a skillcape in order to perform that emote.`, true);\n        }\n    } else {\n        if (emote.unlockable) {\n            const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || [];\n\n            if (unlockedEmotes.indexOf(emote.name) === -1) {\n                player.sendMessage(`You have not unlocked this emote.`, true);\n                return;\n            }\n        }\n\n        player.playAnimation(emote.animationId);\n\n        if (emote.graphicId !== undefined) {\n            player.playGraphics({ id: emote.graphicId, height: 0 });\n        }\n    }\n};\n\nexport default {\n    pluginId: 'rs:player_emotes',\n    hooks: [{ type: 'button', widgetId: widgets.emotesTab, buttonIds, handler }],\n};\n"
  },
  {
    "path": "src/plugins/buttons/player-setting-button.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { widgets } from '@engine/config/config-handler';\n\nconst buttonIds: number[] = [\n    0, // walk/run\n    11,\n    12,\n    13,\n    14,\n    15, // music volume\n    16,\n    17,\n    18,\n    19,\n    20, // sound effect volume\n    29,\n    30,\n    31,\n    32,\n    33, // area effect volume\n    2, // split private chat\n    3, // mouse buttons\n    7,\n    8,\n    9,\n    10, // screen brightness\n    1, // chat effects\n    4, // accept aid\n    5, // house options\n];\n\nexport const handler: buttonActionHandler = details => {\n    const { player, buttonId } = details;\n    player.settingChanged(buttonId);\n};\n\nexport default {\n    pluginId: 'rs:player_setting_button',\n    hooks: [{ type: 'button', widgetId: widgets.settingsTab, buttonIds: buttonIds, handler }],\n};\n"
  },
  {
    "path": "src/plugins/combat/combat-styles.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { EquipmentChangeAction, equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action';\nimport type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport type { ItemDetails, WeaponStyle } from '@engine/config/item-config';\nimport { weaponWidgetIds } from '@engine/config/item-config';\nimport { combatStyles } from '@engine/world/actor/combat';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { SidebarTab } from '@engine/world/actor/player/player';\nimport { widgetScripts } from '@engine/world/config/widget';\nimport { serverConfig } from '@server/game/game-server';\n\nexport function updateCombatStyle(player: Player, weaponStyle: WeaponStyle, styleIndex: number): void {\n    player.savedMetadata.combatStyle = [weaponStyle, styleIndex];\n    player.settings.attackStyle = styleIndex;\n\n    const buttonId = combatStyles[weaponStyle]?.[styleIndex]?.button_id;\n    if (buttonId !== undefined) {\n        player.outgoingPackets.updateClientConfig(widgetScripts.attackStyle, buttonId);\n    }\n}\n\nexport function showUnarmed(player: Player): void {\n    player.modifyWidget(widgets.defaultCombatStyle, { childId: 0, text: 'Unarmed' });\n    player.setSidebarWidget(SidebarTab.COMBAT, widgets.defaultCombatStyle);\n    let style = 0;\n    if (player.savedMetadata.combatStyle) {\n        style = player.savedMetadata.combatStyle[1] || null;\n        if (style && style > 2) {\n            style = 2;\n        }\n    }\n    updateCombatStyle(player, 'unarmed', style);\n}\n\nexport function setWeaponWidget(player: Player, weaponStyle: WeaponStyle, itemDetails: ItemDetails | null): void {\n    player.modifyWidget(weaponWidgetIds[weaponStyle], { childId: 0, text: itemDetails?.name || 'Unknown' });\n    player.setSidebarWidget(SidebarTab.COMBAT, weaponWidgetIds[weaponStyle]);\n    if (player.savedMetadata.combatStyle) {\n        updateCombatStyle(player, weaponStyle, player.savedMetadata.combatStyle[1] || 0);\n    }\n}\n\nexport function updateCombatStyleWidget(player: Player): void {\n    const equippedItem = player.getEquippedItem('main_hand');\n    if (equippedItem) {\n        const itemDetails = findItem(equippedItem.itemId);\n        const weaponStyle = itemDetails?.equipmentData?.weaponInfo?.style || null;\n\n        if (weaponStyle) {\n            setWeaponWidget(player, weaponStyle, itemDetails);\n        } else {\n            showUnarmed(player);\n        }\n    } else {\n        showUnarmed(player);\n    }\n}\n\nconst equip: equipmentChangeActionHandler = ({ player, itemDetails, equipmentSlot }) => {\n    if (equipmentSlot === 'main_hand') {\n        const weaponStyle = itemDetails?.equipmentData?.weaponInfo?.style || null;\n\n        if (!weaponStyle) {\n            showUnarmed(player);\n            return;\n        }\n\n        setWeaponWidget(player, weaponStyle, itemDetails);\n    }\n};\n\nconst initAction: playerInitActionHandler = ({ player }) => {\n    if (!serverConfig.tutorialEnabled || player.savedMetadata.tutorialComplete) {\n        updateCombatStyleWidget(player);\n    }\n};\n\nconst combatStyleSelection: buttonActionHandler = ({ player, buttonId }) => {\n    const equippedItem = player.getEquippedItem('main_hand');\n    let weaponStyle: string | null = 'unarmed';\n\n    if (equippedItem) {\n        weaponStyle = findItem(equippedItem.itemId)?.equipmentData?.weaponInfo?.style || null;\n        if (!weaponStyle || !combatStyles[weaponStyle]) {\n            weaponStyle = 'unarmed';\n        }\n    }\n\n    const combatStyle = combatStyles[weaponStyle].findIndex(combatStyle => combatStyle.button_id === buttonId);\n    if (combatStyle !== -1) {\n        player.savedMetadata.combatStyle = [weaponStyle, combatStyle];\n    }\n};\n\nexport default {\n    pluginId: 'rs:combat_styles',\n    hooks: [\n        {\n            type: 'equipment_change',\n            eventType: 'equip',\n            handler: equip,\n        },\n        {\n            type: 'equipment_change',\n            eventType: 'unequip',\n            handler: (details: EquipmentChangeAction): void => {\n                if (details.equipmentSlot === 'main_hand') {\n                    showUnarmed(details.player);\n                }\n            },\n        },\n        {\n            type: 'player_init',\n            handler: initAction,\n        },\n        {\n            type: 'button',\n            widgetIds: Object.values(weaponWidgetIds),\n            handler: combatStyleSelection,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/bank-command.plugin.ts",
    "content": "import { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter } from '@engine/action/hook/hook-filters';\nimport type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action';\nimport type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { objectIds } from '@engine/world/config/object-ids';\n\nconst action: commandActionHandler = details => {\n    const interactionActions = getActionHooks<ObjectInteractionActionHook>('object_interaction').filter(plugin =>\n        advancedNumberHookFilter(plugin.objectIds, objectIds.bankBooth[0], plugin.options, 'use-quickly'),\n    );\n    interactionActions.forEach(plugin => {\n        if (!plugin.handler) {\n            return;\n        }\n\n        plugin.handler({\n            player: details.player,\n            object: {\n                objectId: objectIds.bankBooth[0],\n                level: details.player.position.level,\n                x: details.player.position.x,\n                y: details.player.position.y,\n                orientation: 0,\n                type: 0,\n            },\n            option: 'use-quickly',\n            position: details.player.position,\n            objectConfig: undefined as any,\n            cacheOriginal: undefined as any,\n        });\n    });\n};\n\nexport default {\n    pluginId: 'rs:bank_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['bank'],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/camera-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { Position } from '@engine/world/position';\n\nconst moveCameraAction: commandActionHandler = ({ player, args }) => {\n    const { x, y, height, speed, acceleration } = args;\n    player.outgoingPackets.snapCameraTo(\n        new Position(x as number, y as number, player.position.level),\n        height as number,\n        speed as number,\n        acceleration as number,\n    );\n};\n\nconst turnCameraAction: commandActionHandler = ({ player, args }) => {\n    const { x, y, height, speed, acceleration } = args;\n    player.outgoingPackets.turnCameraTowards(\n        new Position(x as number, y as number, player.position.level),\n        height as number,\n        speed as number,\n        acceleration as number,\n    );\n};\n\nconst lookCameraAction: commandActionHandler = ({ player, args }) => {\n    const { cameraX, cameraY, cameraHeight, lookX, lookY, lookHeight, speed, acceleration } = args;\n    player.outgoingPackets.snapCameraTo(\n        new Position(cameraX as number, cameraY as number, player.position.level),\n        cameraHeight as number,\n        speed as number,\n        acceleration as number,\n    );\n    player.outgoingPackets.turnCameraTowards(\n        new Position(lookX as number, lookY as number, player.position.level),\n        lookHeight as number,\n        speed as number,\n        acceleration as number,\n    );\n};\n\nconst lookTestAction: commandActionHandler = ({ player }) => {\n    const cameraX = 3219;\n    const cameraY = 3238;\n    const cameraHeight = 500;\n    const lookX = 3219;\n    const lookY = 3250;\n    const lookHeight = 500;\n    const speed = 3;\n    const acceleration = 50;\n\n    player.outgoingPackets.snapCameraTo(new Position(cameraX, cameraY), cameraHeight, speed, acceleration);\n    player.outgoingPackets.turnCameraTowards(new Position(lookX, lookY), lookHeight, speed, acceleration);\n};\n\nexport default {\n    pluginId: 'rs:camera_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['looktest', 'lt'],\n            handler: lookTestAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['cameralook'],\n            args: [\n                {\n                    name: 'cameraX',\n                    type: 'number',\n                },\n                {\n                    name: 'cameraY',\n                    type: 'number',\n                },\n                {\n                    name: 'cameraHeight',\n                    type: 'number',\n                },\n                {\n                    name: 'lookX',\n                    type: 'number',\n                },\n                {\n                    name: 'lookY',\n                    type: 'number',\n                },\n                {\n                    name: 'lookHeight',\n                    type: 'number',\n                },\n                {\n                    name: 'speed',\n                    type: 'number',\n                    defaultValue: 0,\n                },\n                {\n                    name: 'acceleration',\n                    type: 'number',\n                    defaultValue: 100,\n                },\n            ],\n            handler: lookCameraAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['mcam', 'movecamera', 'move_camera', 'setcam', 'setcamera', 'set_camera'],\n            args: [\n                {\n                    name: 'x',\n                    type: 'number',\n                },\n                {\n                    name: 'y',\n                    type: 'number',\n                },\n                {\n                    name: 'height',\n                    type: 'number',\n                },\n                {\n                    name: 'speed',\n                    type: 'number',\n                    defaultValue: 0,\n                },\n                {\n                    name: 'acceleration',\n                    type: 'number',\n                    defaultValue: 100,\n                },\n            ],\n            handler: moveCameraAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['tcam', 'turncamera', 'turn_camera'],\n            args: [\n                {\n                    name: 'x',\n                    type: 'number',\n                },\n                {\n                    name: 'y',\n                    type: 'number',\n                },\n                {\n                    name: 'height',\n                    type: 'number',\n                },\n                {\n                    name: 'speed',\n                    type: 'number',\n                    defaultValue: 0,\n                },\n                {\n                    name: 'acceleration',\n                    type: 'number',\n                    defaultValue: 100,\n                },\n            ],\n            handler: turnCameraAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/clear-inventory-command.plugin.ts",
    "content": "import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action';\n\nexport default {\n    pluginId: 'rs:clear_inventory_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['clear'],\n            handler: (details: PlayerCommandAction): void => details.player.inventory.clear(),\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/client-config-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const configId = args.configId as number;\n    const configValue = args.configValue as number;\n\n    player.outgoingPackets.updateClientConfig(configId, configValue);\n};\n\nexport default {\n    pluginId: 'rs:client_config_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['config', 'conf'],\n            args: [\n                {\n                    name: 'configId',\n                    type: 'number',\n                },\n                {\n                    name: 'configValue',\n                    type: 'number',\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/current-position-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player } = details;\n    player.sendLogMessage(`@[ ${player.position.x}, ${player.position.y}, ${player.position.level} ]`, details.isConsole);\n};\n\nexport default {\n    pluginId: 'rs:current_position_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['pos', 'loc', 'position', 'location', 'coords', 'coordinates', 'mypos', 'myloc'],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/data-dump-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport type { DataDumpResult } from '@engine/config/data-dump';\nimport { dumpItems, dumpNpcs, dumpObjects, dumpWidgets } from '@engine/config/data-dump';\n\nconst action: commandActionHandler = ({ player, args, isConsole }) => {\n    const dataType = args.dataType as string;\n\n    const functionMap: { [key: string]: () => DataDumpResult } = {\n        npcs: dumpNpcs,\n        items: dumpItems,\n        objects: dumpObjects,\n        widgets: dumpWidgets,\n    };\n\n    const types = Object.keys(functionMap);\n\n    if (types.indexOf(dataType) === -1) {\n        player.sendLogMessage(`Invalid data type, please use one of the following:`, isConsole);\n        player.sendLogMessage(`[ ${types.join(', ')} ]`, isConsole);\n        return;\n    }\n\n    let dataName = dataType;\n    if (dataType.endsWith('s')) {\n        dataName = dataType.substring(0, dataType.length - 2);\n    }\n\n    player.sendLogMessage(`Dumping ${dataName} data...`, isConsole);\n    const result = functionMap[dataType]();\n\n    if (result.successful) {\n        player.sendLogMessage(`Saved ${dataName} data to ${result.filePath}.`, isConsole);\n    } else {\n        player.sendLogMessage(`Error dumping ${dataName} data.`, isConsole);\n    }\n};\n\nexport default {\n    pluginId: 'rs:data_dump_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['dump', 'data', 'datadump', 'dd'],\n            args: [\n                {\n                    name: 'dataType',\n                    type: 'string',\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/dump-metadata-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player } = details;\n    const metadata = { ...player.metadata };\n    for (const metadataKey of Object.keys(metadata)) {\n        if (typeof (metadata as any)[metadataKey] === 'function') {\n            (metadata as any)[metadataKey] = typeof (metadata as any)[metadataKey];\n        }\n        if (Array.isArray((metadata as any)[metadataKey]) && (metadata as any)[metadataKey].length > 30) {\n            (metadata as any)[metadataKey] = `Array (${(metadata as any)[metadataKey].length} entries)`;\n        }\n        if (\n            typeof (metadata as any)[metadataKey] === 'object' &&\n            (metadata as any)[metadataKey] !== null &&\n            'unsubscribe' in (metadata as any)[metadataKey]\n        ) {\n            (metadata as any)[metadataKey] = `Observable { closed: ${(metadata as any)[metadataKey].closed} }`;\n        }\n    }\n    console.log(metadata);\n    const stringified = JSON.stringify(metadata, null, 4);\n    stringified.split('\\n').forEach(split => {\n        player.sendLogMessage(split, details.isConsole);\n        console.log(split);\n    });\n};\n\nexport default {\n    pluginId: 'rs:dump_metadata_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['dump_metadata'],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/give-item-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { findItem } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const inventorySlot = player.inventory.getFirstOpenSlot();\n\n    if (inventorySlot === -1) {\n        player.sendLogMessage(`You don't have enough free space to do that.`, details.isConsole);\n        return;\n    }\n\n    const itemSearch: string = args.itemSearch as string;\n    let itemId: number | null = null;\n\n    if (itemSearch.match(/^[0-9]+$/)) {\n        itemId = parseInt(itemSearch, 10);\n    } else {\n        if (itemSearch.indexOf(':') !== -1) {\n            itemId = findItem(itemSearch)?.gameId || null;\n        } else {\n            // @TODO nested item ids\n            itemId = itemIds[itemSearch];\n        }\n    }\n\n    if (!itemId || isNaN(itemId)) {\n        throw new Error(`Item name not found.`);\n    }\n\n    let amount: number = args.amount as number;\n\n    if (amount > 2000000000) {\n        throw new Error(`Unable to give more than 2,000,000,000.`);\n    }\n\n    const itemDefinition = findItem(itemId);\n    if (!itemDefinition) {\n        throw new Error(`Item ID ${itemId} not found!`);\n    }\n\n    let actualAmount = 0;\n    if (itemDefinition.stackable) {\n        const item = { itemId, amount };\n        player.giveItem(item);\n        actualAmount = amount;\n    } else {\n        if (amount > 28) {\n            amount = 28;\n        }\n\n        for (let i = 0; i < amount; i++) {\n            if (player.giveItem({ itemId, amount: 1 })) {\n                actualAmount++;\n            } else {\n                break;\n            }\n        }\n    }\n\n    player.sendLogMessage(`Added ${actualAmount}x ${itemDefinition.name} to inventory.`, details.isConsole);\n};\n\nexport default {\n    pluginId: 'rs:give_item_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['give', 'item', 'spawn'],\n            args: [\n                {\n                    name: 'itemSearch',\n                    type: 'string',\n                },\n                {\n                    name: 'amount',\n                    type: 'number',\n                    defaultValue: 1,\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/groups-debug.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { findItemTagsInGroupFilter, findItemTagsInGroups } from '@engine/config/config-handler';\n\nconst selectGroups: commandActionHandler = ({ player, args, isConsole }) => {\n    const groups: string | number = args.groupkeys;\n    if (!groups || typeof groups !== 'string') {\n        player.sendLogMessage('invalid input', isConsole);\n        return;\n    }\n    player.sendLogMessage('results:', isConsole);\n    findItemTagsInGroups(groups.split(',')).forEach(itemName => {\n        player.sendLogMessage(itemName, isConsole);\n    });\n    return;\n};\n\nconst filterGroups: commandActionHandler = ({ player, args, isConsole }) => {\n    const groups: string | number = args.groupkeys;\n    if (!groups || typeof groups !== 'string') {\n        player.sendLogMessage('invalid input', isConsole);\n        return;\n    }\n\n    player.sendLogMessage('results:', isConsole);\n    findItemTagsInGroupFilter(groups.split(',')).forEach(itemName => {\n        player.sendLogMessage(itemName, isConsole);\n    });\n    return;\n};\n\nexport default {\n    pluginId: 'promises:groups-debug',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['selectgroups'],\n            args: [\n                {\n                    name: 'groupkeys',\n                    type: 'string',\n                },\n            ],\n            handler: selectGroups,\n        },\n        {\n            type: 'player_command',\n            commands: ['filtergroups'],\n            args: [\n                {\n                    name: 'groupkeys',\n                    type: 'string',\n                },\n            ],\n            handler: filterGroups,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/pathing-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { Position } from '@engine/world/position';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const x: number = args.x as number;\n    const y: number = args.y as number;\n    const pathingDiameter: number = args.diameter as number;\n\n    player.pathfinding.walkTo(new Position(x, y, player.position.level), { pathingSearchRadius: pathingDiameter });\n};\n\nexport default {\n    pluginId: 'rs:pathing_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['path'],\n            args: [\n                {\n                    name: 'x',\n                    type: 'number',\n                },\n                {\n                    name: 'y',\n                    type: 'number',\n                },\n                {\n                    name: 'diameter',\n                    type: 'number',\n                    defaultValue: 64,\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/player-animation-command.plugin.ts",
    "content": "import type { PlayerCommandActionHook, commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = (details): void => {\n    const { player, args } = details;\n\n    const animationId: number = args.animationId as number;\n    player.playAnimation(animationId);\n};\n\nexport default {\n    pluginId: 'rs:player_animation_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['anim', 'animation', 'playanim'],\n            args: [\n                {\n                    name: 'animationId',\n                    type: 'number',\n                },\n            ],\n            handler: action,\n        } as PlayerCommandActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/player-graphics-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const graphicsId: number = args.graphicsId as number;\n    const height: number = args.height as number;\n\n    player.playGraphics({ id: graphicsId, delay: 0, height: height });\n};\n\nexport default {\n    pluginId: 'rs:player_graphics_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['gfx', 'graphics'],\n            args: [\n                {\n                    name: 'graphicsId',\n                    type: 'number',\n                },\n                {\n                    name: 'height',\n                    type: 'number',\n                    defaultValue: 120,\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/quest-list-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { questMap } from '@engine/plugins/loader';\n\nconst action: commandActionHandler = details => {\n    for (const quest of Object.values(questMap)) {\n        details.player.sendLogMessage(quest.id, details.isConsole);\n    }\n};\n\nexport default {\n    pluginId: 'promises:quest-list-command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['quest-list', 'quests'],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/region-debug-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\n\nconst debugMapRegion = (\n    player: Player,\n    mapRegionX: number,\n    mapRegionY: number,\n    worldX: number,\n    worldY: number,\n    level: number = -1,\n): void => {\n    const key = `${mapRegionX},${mapRegionY}`;\n    player.sendMessage(`Region ${key} - ${activeWorld.chunkManager.getRegionIdForWorldPosition(player.position)}`);\n\n    if (!activeWorld.chunkManager.regionMap.has(key)) {\n        player.sendMessage(`Map region not loaded.`);\n        return;\n    }\n\n    if (level === -1) {\n        level = player.position.level;\n    }\n\n    const region = activeWorld.chunkManager.regionMap.get(key);\n\n    if (!region) {\n        player.sendMessage(`Map region not loaded.`);\n        logger.error(`Map region not loaded. ${key}`);\n        return;\n    }\n\n    let debug: string = `\\nRegion ${key},${level}\\n\\n`;\n    for (let y = 63; y >= 0; y--) {\n        const line = new Array(64).fill('?');\n        for (let x = 0; x < 64; x++) {\n            const tileWorldX = worldX + x;\n            const tileWorldY = worldY + y;\n            if (tileWorldX === player.position.x && tileWorldY === player.position.y) {\n                line[x] = '@';\n            } else if (region.mapFile?.tileSettings) {\n                const tileSettings = activeWorld.chunkManager.getTile(new Position(tileWorldX, tileWorldY, level)).settings;\n\n                if (!tileSettings) {\n                    line[x] = '.';\n                } else if (tileSettings > 9) {\n                    line[x] = 'x';\n                } else {\n                    line[x] = tileSettings + '';\n                }\n            }\n        }\n        debug += `|${line.join('')}|\\n`;\n    }\n\n    logger.info(debug);\n};\n\nconst regionDebugHandler: commandActionHandler = ({ player, args }) => {\n    const chunkX = player.position.chunkX + 6;\n    const chunkY = player.position.chunkY + 6;\n    const mapRegionX = Math.floor(chunkX / 8);\n    const mapRegionY = Math.floor(chunkY / 8);\n    const worldX = (mapRegionX & 0xff) * 64;\n    const worldY = mapRegionY * 64;\n\n    debugMapRegion(player, mapRegionX, mapRegionY, worldX, worldY, (args?.level as number) || -1);\n};\n\nconst tileDebugHandler: commandActionHandler = ({ player }) => {\n    const tile = activeWorld.chunkManager.getTile(player.position);\n\n    const tile0 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(0));\n    const tile1 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(1));\n    const tile2 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(2));\n    const tile3 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(3));\n\n    const chunkX = player.position.chunkX + 6;\n    const chunkY = player.position.chunkY + 6;\n    const mapRegionX = Math.floor(chunkX / 8);\n    const mapRegionY = Math.floor(chunkY / 8);\n    const worldX = (mapRegionX & 0xff) * 64;\n    const worldY = mapRegionY * 64;\n\n    player.sendMessage(\n        [\n            `Tile ${player.position.key} settings: ${tile.settings}`,\n            `Local Pos: ${player.position.x - worldX},${player.position.y - worldY}`,\n            `Tile@0=(${tile0.settings}), Tile@1=(${tile1.settings}), Tile@2=(${tile2.settings}), Tile@3=(${tile3.settings})`,\n        ],\n        { console: true },\n    );\n};\n\nexport default {\n    pluginId: 'rs:region_debug_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['regioninfo', 'region', 'myregion', 'regiondebug', 'region_info', 'my_region', 'region_debug'],\n            args: [\n                {\n                    name: 'level',\n                    type: 'number',\n                    defaultValue: -1,\n                },\n            ],\n            handler: regionDebugHandler,\n        },\n        {\n            type: 'player_command',\n            commands: ['tileinfo', 'tile', 'mytile', 'tiledebug', 'tile_info', 'my_tile', 'tile_debug'],\n            handler: tileDebugHandler,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/reset-camera-command.plugin.ts",
    "content": "import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action';\n\nexport default {\n    pluginId: 'rs:reset_camera_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['reset_camera', 'resetcamera'],\n            handler: ({ player }: PlayerCommandAction): void => player.outgoingPackets.resetCamera(),\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/sound-song-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst songAction: commandActionHandler = details => {\n    const { player, args } = details;\n    player.outgoingPackets.playSong(args.songId as number);\n};\n\nconst soundAction: commandActionHandler = details => {\n    const { player, args } = details;\n    player.playSound(args.soundId as number, args.volume as number);\n};\n\nconst quickSongAction: commandActionHandler = details => {\n    const { player, args } = details;\n    player.outgoingPackets.playQuickSong(args.songId as number, args.prevSongId as number);\n};\n\nexport default {\n    pluginId: 'rs:sound_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: 'song',\n            args: [\n                {\n                    name: 'songId',\n                    type: 'number',\n                },\n            ],\n            handler: songAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['sound', 'so'],\n            args: [\n                {\n                    name: 'soundId',\n                    type: 'number',\n                },\n                {\n                    name: 'volume',\n                    type: 'number',\n                    defaultValue: 10,\n                },\n            ],\n            handler: soundAction,\n        },\n        {\n            type: 'player_command',\n            commands: 'quicksong',\n            args: [\n                {\n                    name: 'songId',\n                    type: 'number',\n                },\n                {\n                    name: 'prevSongId',\n                    type: 'number',\n                },\n            ],\n            handler: quickSongAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/spawn-npc-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { findNpc } from '@engine/config/config-handler';\nimport type { NpcDetails } from '@engine/config/npc-config';\nimport { NpcSpawn } from '@engine/config/npc-spawn-config';\nimport { activeWorld } from '@engine/world';\nimport { Npc } from '@engine/world/actor/npc';\n\nconst action: commandActionHandler = ({ player, args }) => {\n    let npcKey: string | number = args.npcKey;\n    let npcDetails: NpcDetails | null = null;\n\n    if (typeof npcKey === 'string' && npcKey.match(/^[0-9]+$/)) {\n        npcKey = parseInt(npcKey, 10);\n    }\n\n    if (typeof npcKey === 'string') {\n        npcDetails = findNpc(npcKey);\n        npcKey = npcDetails.gameId;\n    }\n\n    const movementRadius: number = args.movementRadius as number;\n\n    const npc = new Npc(\n        npcDetails ? npcDetails : npcKey,\n        new NpcSpawn(npcDetails?.key ? npcDetails.key : `unknown-${npcKey}`, player.position.clone(), movementRadius, 'WEST'),\n        player.instance,\n    );\n\n    activeWorld.registerNpc(npc);\n};\n\nexport default {\n    pluginId: 'rs:spawn_npc_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['npc', 'spawnnpc', 'spawn_npc'],\n            args: [\n                {\n                    name: 'npcKey',\n                    type: 'either',\n                },\n                {\n                    name: 'movementRadius',\n                    type: 'number',\n                    defaultValue: 0,\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/spawn-scenery-command.plugin.ts",
    "content": "import { writeFileSync } from 'fs';\nimport type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { dump } from 'js-yaml';\n\nconst spawnSceneryAction: commandActionHandler = ({ player, args }) => {\n    const locationObjectSearch: string = (args.locationObjectSearch as string).trim();\n    let locationObjectId: number;\n\n    if (locationObjectSearch.match(/^[0-9]+$/)) {\n        locationObjectId = parseInt(locationObjectSearch, 10);\n    } else {\n        // @TODO nested object ids\n        locationObjectId = objectIds[locationObjectSearch];\n    }\n\n    if (isNaN(locationObjectId)) {\n        throw new Error(`Location object name not found.`);\n    }\n\n    const objectType = args.objectType as number;\n    const objectOrientation = args.objectOrientation as number;\n\n    const position = player.position.copy();\n\n    const locationObject: LandscapeObject = {\n        objectId: locationObjectId,\n        x: position.x,\n        y: position.y,\n        level: position.level,\n        type: objectType,\n        orientation: objectOrientation,\n    };\n\n    player.metadata.lastSpawnedScenery = locationObject;\n\n    if (!player.metadata.spawnedScenery) {\n        player.metadata.spawnedScenery = [];\n    }\n\n    player.metadata.spawnedScenery.push(locationObject);\n\n    player.instance.spawnGameObject(locationObject);\n};\n\nconst undoSceneryAction: commandActionHandler = details => {\n    const { player } = details;\n\n    const o = player.metadata.lastSpawnedScenery;\n\n    if (!o) {\n        return;\n    }\n\n    player.instance.despawnGameObject(o);\n    delete player.metadata.lastSpawnedScenery;\n\n    if (player.metadata.spawnedScenery) {\n        player.metadata.spawnedScenery.pop();\n    }\n};\n\nconst dumpSceneryAction: commandActionHandler = details => {\n    const { player } = details;\n\n    const path = `data/dump/scene-${new Date().getTime()}.yml`;\n    writeFileSync(path, dump(player.metadata.spawnedScenery));\n    logger.info(path);\n    player.metadata.spawnedScenery = [];\n};\n\nexport default {\n    pluginId: 'rs:spawn_scenery_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['scene', 'sc'],\n            args: [\n                {\n                    name: 'locationObjectSearch',\n                    type: 'string',\n                },\n                {\n                    name: 'objectOrientation',\n                    type: 'number',\n                    defaultValue: 0,\n                },\n                {\n                    name: 'objectType',\n                    type: 'number',\n                    defaultValue: 10,\n                },\n            ],\n            handler: spawnSceneryAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['undoscene', 'undosc'],\n            handler: undoSceneryAction,\n        },\n        {\n            type: 'player_command',\n            commands: ['dumpscene', 'dumpsc'],\n            handler: dumpSceneryAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/spawn-test-players-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { activeWorld } from '@engine/world';\nimport { Player } from '@engine/world/actor/player/player';\nimport { Position } from '@engine/world/position';\nimport { World } from '@engine/world/world';\n\nconst handler: commandActionHandler = ({ player, args }) => {\n    const playerCount = args.playerCount as number;\n\n    if (playerCount > World.MAX_PLAYERS - 1) {\n        player.sendMessage(`Error: Max player count is ${World.MAX_PLAYERS - 1}.`);\n        return;\n    }\n\n    const x: number = player.position.x;\n    const y: number = player.position.y;\n    let xOffset: number = 0;\n    let yOffset: number = 0;\n\n    const spawnChunk = activeWorld.chunkManager.getChunkForWorldPosition(new Position(x, y, 0));\n\n    const worldSlotsRemaining = activeWorld.playerSlotsRemaining() - 1;\n    if (worldSlotsRemaining <= 0) {\n        player.sendMessage(`Error: The game world is full.`);\n        return;\n    }\n\n    const playerSpawnCount = playerCount > worldSlotsRemaining ? worldSlotsRemaining : playerCount;\n\n    if (playerSpawnCount < playerCount) {\n        player.sendMessage(`Warning: There was only room for ${playerSpawnCount}/${playerCount} player spawns.`);\n    }\n\n    // TODO (JameskmongeR) what's the difference between this and `generateFakePlayers`\n\n    for (let i = 0; i < playerSpawnCount; i++) {\n        // TODO (Jameskmonger) we should be able to create a player without a connection, and without passing nulls in\n        const testPlayer = new Player(null as any, null as any, null as any, i, `test${i}`, 'abs', true);\n        activeWorld.registerPlayer(testPlayer);\n        testPlayer.interfaceState.closeAllSlots();\n\n        xOffset++;\n\n        if (xOffset > 20) {\n            xOffset = 0;\n            yOffset--;\n        }\n\n        testPlayer.position = new Position(x + xOffset, y + yOffset, 0);\n        const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(testPlayer.position);\n\n        if (!spawnChunk.equals(newChunk)) {\n            spawnChunk.removePlayer(testPlayer);\n            newChunk.addPlayer(testPlayer);\n        }\n\n        testPlayer.initiateRandomMovement();\n    }\n};\n\nexport default {\n    pluginId: 'rs:spawn_test_players_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['spawn_players', 'spawnplayers'],\n            args: [\n                {\n                    name: 'playerCount',\n                    type: 'number',\n                },\n            ],\n            handler,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/stat-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst setLevelAction: commandActionHandler = ({ player, args }) => {\n    const skillId = args?.skillId || null;\n    const level: number | null = (args?.level as number) || null;\n\n    if (!skillId || !level) {\n        player.sendMessage(`Invalid syntax: Use ::setlevel skill_id skill_level`);\n        return;\n    }\n\n    const skill = player.skills[skillId];\n    if (!skill) {\n        player.sendMessage(`Skill ${skillId} not found.`);\n        return;\n    }\n\n    const exp = player.skills.getExpForLevel(level);\n    skill.exp = exp;\n    skill.level = level;\n    player.outgoingPackets.updateSkill(player.skills.getSkillId(skillId as any), level, exp);\n};\n\nexport default {\n    pluginId: 'rs:stat_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['setlevel', 'setlvl'],\n            args: [\n                {\n                    name: 'skillId',\n                    type: 'string',\n                },\n                {\n                    name: 'level',\n                    type: 'number',\n                },\n            ],\n            handler: setLevelAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/teleport-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { activeWorld } from '@engine/world';\nimport { Position } from '@engine/world/position';\n\nconst action: commandActionHandler = details => {\n  const { player, args } = details;\n\n  const x = args.XorPlayerName;\n\n  if (typeof x === 'string') {\n    const playerWithName = activeWorld.findPlayer(x);\n    if (playerWithName) {\n      player.teleport(playerWithName.position);\n      return;\n    }\n  }\n\n  const xCoord: number = typeof x === 'string' ? parseInt(x, 10) : x;\n\n  if (isNaN(xCoord)) {\n    return;\n  }\n  const y: number = args.y as number;\n  const level: number = args.level as number;\n\n  player.teleport(new Position(xCoord, y, level));\n};\n\nconst goUpAction: commandActionHandler = details => {\n  const { player } = details;\n\n  player.teleport(new Position(player.position.x, player.position.y, player.position.level + 1));\n};\n\nconst goDownAction: commandActionHandler = details => {\n  const { player } = details;\n\n  if (player.position.level > 0) {\n    player.teleport(new Position(player.position.x, player.position.y, player.position.level - 1));\n  }\n};\n\nconst setLevelCommand: commandActionHandler = details => {\n  const { player, args } = details;\n  const level: number = args.level as number;\n  if (!isNaN(level) && level >= 0 && level <= 255) {\n    player.teleport(new Position(player.position.x, player.position.y, level));\n  }\n}\n\nexport default {\n  pluginId: 'rs:teleport_command_plugin',\n  hooks: [\n    {\n      type: 'player_command',\n      commands: [ 'move', 'goto', 'teleport', 'tele', 'moveto', 'setpos' ],\n      args: [\n        {\n          name: 'XorPlayerName',\n          type: 'string',\n        },\n        {\n          name: 'y',\n          type: 'number',\n          defaultValue: 3222,\n        },\n        {\n          name: 'level',\n          type: 'number',\n          defaultValue: 0,\n        },\n      ],\n      handler: action,\n    },\n    {\n      type: 'player_command',\n      commands: [ 'up', 'goup' ],\n      handler: goUpAction,\n    },\n    {\n      type: 'player_command',\n      commands: [ 'down', 'godown' ],\n      handler: goDownAction,\n    },\n    {\n      type: 'player_command',\n      commands: [ 'setheightlevel', 'heightlevel', 'hl' ],\n      args: [\n        {\n          name: 'level',\n          type: 'number',\n        },\n      ],\n      handler: setLevelCommand,\n    }\n  ],\n};\n"
  },
  {
    "path": "src/plugins/commands/transform-command.plugin.ts",
    "content": "import type { PlayerCommandActionHook, commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    player.transformInto(details && details.args ? details.args['npcKey'] : null);\n};\n\nexport default {\n    pluginId: 'rs:transform_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['transform'],\n            args: [\n                {\n                    name: 'npcKey',\n                    type: 'either',\n                    defaultValue: undefined,\n                },\n            ],\n            handler: action,\n        } as PlayerCommandActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/travel-back-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player } = details;\n\n    if (player.metadata.lastPosition) {\n        player.teleport(player.metadata.lastPosition);\n    }\n};\n\nexport default {\n    pluginId: 'rs:travel_back_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['back'],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/travel-command.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\nimport { activeWorld } from '@engine/world';\nimport type { TravelLocation } from '@engine/world/config/travel-locations';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const search: string = args.search as string;\n    const location = activeWorld.travelLocations.find(search) as TravelLocation;\n\n    if (location) {\n        player.teleport(location.position);\n        player.sendLogMessage(`Welcome to ${location.name}`, details.isConsole);\n    } else {\n        player.sendLogMessage(`Unknown location ${search}`, details.isConsole);\n    }\n};\n\nexport default {\n    pluginId: 'rs:travel_command',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['travel'],\n            args: [\n                {\n                    name: 'search',\n                    type: 'string',\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/commands/widget-commands.plugin.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst action: commandActionHandler = details => {\n    const { player, args } = details;\n\n    const widgetId: number = args.widgetId as number;\n    const secondaryWidgetId: number = args.secondaryWidgetId as number;\n\n    if (secondaryWidgetId === 1) {\n        player.interfaceState.openWidget(widgetId, {\n            slot: 'screen',\n        });\n    } else {\n        player.interfaceState.openWidget(widgetId, {\n            slot: 'screen',\n            multi: true,\n        });\n        player.interfaceState.openWidget(secondaryWidgetId, {\n            slot: 'tabarea',\n            multi: true,\n        });\n    }\n};\n\nexport default {\n    pluginId: 'rs:widget_commands',\n    hooks: [\n        {\n            type: 'player_command',\n            commands: ['widget'],\n            args: [\n                {\n                    name: 'widgetId',\n                    type: 'number',\n                },\n                {\n                    name: 'secondaryWidgetId',\n                    type: 'number',\n                    defaultValue: 1,\n                },\n            ],\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/dialogue/dialogue-option.plugin.ts",
    "content": "import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action';\n\nconst dialogueIds = [64, 65, 66, 67, 241, 242, 243, 244, 228, 230, 232, 234, 210, 211, 212, 213, 214];\n\n/**\n * Handles a basic NPC/Player/Option/Text dialogue choice/action.\n */\nexport const action: widgetInteractionActionHandler = details => {\n    const { player, widgetId, childId } = details;\n    player.interfaceState.closeWidget('chatbox', widgetId, childId);\n};\n\nexport default {\n    pluginId: 'rs:dialog_choice',\n    hooks: [\n        {\n            type: 'widget_interaction',\n            widgetIds: dialogueIds,\n            handler: action,\n            cancelActions: true,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/dialogue/item-selection.plugin.ts",
    "content": "import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action';\n\n/**\n * Handles an item selection dialogue choice.\n */\nexport const action: widgetInteractionActionHandler = details => {\n    const { player, widgetId, childId } = details;\n    player.interfaceState.closeWidget('chatbox', widgetId, childId);\n};\n\nexport default {\n    pluginId: 'rs:item_selection_choice',\n    hooks: [\n        {\n            type: 'widget_interaction',\n            widgetIds: [303, 304, 305, 306, 307, 309],\n            handler: action,\n            cancelActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/buckets/empty-container.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\n\nexport const handler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot } = details;\n\n    const inventory = player.inventory;\n    const item = getItemFromContainer(itemId, itemSlot, inventory);\n\n    if (!item) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    inventory.remove(itemSlot);\n    player.playSound(soundIds.emptyBucket, 5);\n    switch (itemId) {\n        case itemIds.jugOfWater:\n            player.giveItem(itemIds.jug);\n            break;\n        default:\n            player.giveItem(itemIds.bucket);\n            break;\n    }\n\n    // @TODO only update necessary slots\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory);\n};\n\nexport default {\n    pluginId: 'rs:empty_container',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'empty',\n            itemIds: [itemIds.bucketOfMilk, itemIds.bucketOfWater, itemIds.jugOfWater],\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/buckets/fill-container.plugin.ts",
    "content": "import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport { findItem } from '@engine/config/config-handler';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { logger } from '@runejs/common';\n\nconst FountainIds: number[] = [879];\nconst SinkIds: number[] = [14878, 873];\nconst WellIds: number[] = [878];\nexport const handler: itemOnObjectActionHandler = details => {\n    const { player, objectConfig, item } = details;\n    const itemDef = findItem(item.itemId);\n\n    if (!itemDef) {\n        logger.error(`No item found for fill container plugin: ${item.itemId}`);\n        return;\n    }\n\n    if (item.itemId !== itemIds.bucket && WellIds.indexOf(objectConfig.gameId) > -1) {\n        player.sendMessage(`If I drop my ${itemDef.name.toLowerCase()} down there, I don't think I'm likely to get it back.`);\n        return;\n    }\n\n    player.playAnimation(animationIds.fillContainerWithWater);\n    player.playSound(soundIds.fillContainerWithWater, 7);\n    player.removeFirstItem(item.itemId);\n    switch (item.itemId) {\n        case itemIds.bucket:\n            player.giveItem(itemIds.bucketOfWater);\n            break;\n        case itemIds.jug:\n            player.giveItem(itemIds.jugOfWater);\n            break;\n    }\n\n    const objectName = details.objectConfig.name || '';\n    if (!objectName) {\n        logger.warn(`Fill container object ${details.object.objectId} has no name.`);\n    }\n\n    player.sendMessage(`You fill the ${itemDef.name.toLowerCase()} from the ${objectName.toLowerCase()}.`);\n};\n\nexport default {\n    pluginId: 'rs:fill_container',\n    hooks: [\n        {\n            type: 'item_on_object',\n            objectIds: [...FountainIds, ...WellIds, ...SinkIds],\n            itemIds: [itemIds.bucket, itemIds.jug],\n            walkTo: true,\n            handler,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/capes/skillcape-emotes.plugin.ts",
    "content": "import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { lockEmote, unlockEmote } from '@plugins/buttons/player-emotes.plugin';\n\nexport const skillcapeIds: Array<number> = Object.keys(itemIds.skillCapes).flatMap(skill => [\n    itemIds.skillCapes[skill].untrimmed,\n    itemIds.skillCapes[skill].trimmed,\n]);\n\nexport const equip: equipmentChangeActionHandler = details => {\n    const { player } = details;\n    unlockEmote(player, 'SKILLCAPE');\n};\n\nexport const unequip: equipmentChangeActionHandler = details => {\n    const { player } = details;\n    lockEmote(player, 'SKILLCAPE');\n    player.stopAnimation();\n    player.stopGraphics();\n};\n\nexport default {\n    pluginId: 'rs:skillcape_emotes',\n    hooks: [\n        {\n            type: 'equipment_change',\n            eventType: 'equip',\n            handler: equip,\n            itemIds: skillcapeIds,\n        },\n        {\n            type: 'equipment_change',\n            eventType: 'unequip',\n            handler: unequip,\n            itemIds: skillcapeIds,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/consumables/eating.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport { randomBetween } from '@engine/util/num';\nimport type { SkillName } from '@engine/world/actor/skills';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\n\nexport const action: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, itemDetails } = details;\n    if (!itemDetails.consumable) {\n        player.sendMessage('Item is not registered as consumable!');\n        return;\n    }\n    if (!itemDetails.metadata.consume_effects) {\n        player.sendMessage('Item is missing consume effects!');\n        return;\n    }\n    if (!itemDetails.metadata.consume_effects.clock) {\n        player.sendMessage('Item is missing clock!');\n        return;\n    }\n    if (itemDetails.metadata.consume_effects.special) {\n        player.sendMessage('Cannot handle special foods yet!');\n        return;\n    }\n    const clock = 'clock_' + itemDetails.metadata.consume_effects.clock;\n    // Check if player recently ate\n    if (player.metadata[clock]) {\n        return;\n    }\n\n    const inventoryItem = player.inventory.items[itemSlot];\n\n    if (!inventoryItem || inventoryItem.itemId !== itemId) {\n        return;\n    }\n\n    const replacementItemDetails = itemDetails.metadata.consume_effects.replaced_by\n        ? findItem(itemDetails.metadata.consume_effects.replaced_by)\n        : null;\n\n    if (replacementItemDetails) {\n        player.inventory.items[itemSlot] = { itemId: replacementItemDetails.gameId, amount: 1 };\n    } else {\n        player.inventory.items[itemSlot] = null;\n    }\n    player.playSound(soundIds.eat);\n    player.playAnimation(animationIds.eat);\n\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n\n    // this used to use `setTimeout` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // Set a timeout so player cant spam eat\n    // player.metadata[clock] = true;\n    // setTimeout(() => {\n    //     player.metadata[clock] = false;\n    // }, World.TICK_LENGTH * 3);\n\n    if (itemDetails.metadata.consume_effects.energy) {\n        // TODO: Give player run energy\n    }\n    if (itemDetails.metadata.consume_effects.skills) {\n        const skillModifiers = itemDetails.metadata.consume_effects.skills;\n        for (const sk in skillModifiers) {\n            const skill: SkillName = sk as SkillName;\n            let value;\n            if (Array.isArray(skillModifiers[skill])) {\n                value = randomBetween(skillModifiers[skill][0], skillModifiers[skill][1]);\n            } else {\n                value = skillModifiers[skill];\n            }\n            const playerSkill = player.skills[skill];\n            const maxLevel = playerSkill.levelForExp;\n            const currentLevel = playerSkill.level || playerSkill.levelForExp;\n\n            if (skill === 'hitpoints') {\n                let newHealth: number = currentLevel + value;\n                if (newHealth > maxLevel) {\n                    newHealth = maxLevel;\n                }\n                playerSkill.level = newHealth;\n                player.sendMessage(`You eat the ${itemDetails.name}, and it restores ${newHealth - currentLevel} health.`);\n            } else {\n                let newLevel: number = currentLevel + value;\n                if (newLevel > maxLevel + value) {\n                    newLevel = maxLevel + value;\n                }\n                playerSkill.level = newLevel;\n            }\n            player.outgoingPackets.updateSkill(player.skills.getSkillId(skill), playerSkill.level, playerSkill.exp);\n        }\n    }\n};\n\nexport default {\n    pluginId: 'rs:eating',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'eat',\n            handler: action,\n            cancelOtherActions: true,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/drop-item.plugin.ts",
    "content": "import type { ActionCancelType } from '@engine/action/action-pipeline';\nimport type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { dialogue, execute } from '@engine/world/actor/dialogue';\nimport { Rights } from '@engine/world/actor/player/player';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\nimport { serverConfig } from '@server/game/game-server';\n\nexport const handler: itemInteractionActionHandler = ({ player, itemId, itemSlot }) => {\n    const inventory = player.inventory;\n    const item = getItemFromContainer(itemId, itemSlot, inventory);\n\n    if (!item) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    if (!serverConfig.adminDropsEnabled && player.rights === Rights.ADMIN) {\n        dialogue(\n            [player],\n            [\n                text => 'Administrators are not allowed to drop items.',\n                options => [\n                    `Destroy the item!`,\n                    [\n                        execute(() => {\n                            inventory.remove(itemSlot);\n                            player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null);\n                        }),\n                    ],\n                    `Bank the item!`,\n                    [\n                        execute(() => {\n                            inventory.remove(itemSlot);\n                            player.bank.add(item);\n                            player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null);\n                        }),\n                    ],\n                ],\n            ],\n        );\n\n        return;\n    }\n\n    inventory.remove(itemSlot);\n    player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null);\n    player.playSound(soundIds.dropItem, 5);\n    player.instance.spawnWorldItem(item, player.position, { owner: player, expires: 300 });\n    // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now\n    player.actionsCancelled.next(null as unknown as ActionCancelType);\n};\n\nexport default {\n    pluginId: 'rs:drop_item',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'drop',\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/equipment/equip-item.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\n\nexport const handler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, itemDetails } = details;\n\n    if (!itemDetails) {\n        // The item is not yet configured on the server.\n        player.sendMessage(`Item ${itemId} is not yet configured on the server.`);\n        return;\n    }\n\n    player.equipItem(itemId, itemSlot, itemDetails.equipmentData?.equipmentSlot);\n};\n\nexport default {\n    pluginId: 'rs:equip_item',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'equip',\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/equipment/equipment-stats.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { widgets } from '@engine/config/config-handler';\n\nexport const handler: buttonActionHandler = details => {\n    const { player } = details;\n\n    player.updateBonuses();\n    player.syncBonuses();\n\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipmentStats, player.equipment);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n\n    player.interfaceState.openWidget(widgets.equipmentStats.widgetId, {\n        multi: true,\n        slot: 'screen',\n    });\n    player.interfaceState.openWidget(widgets.inventory.widgetId, {\n        multi: true,\n        slot: 'tabarea',\n    });\n};\n\nexport default {\n    pluginId: 'rs:equipment_stat_view',\n    hooks: [{ type: 'button', widgetId: widgets.equipment.widgetId, buttonIds: 24, handler }],\n};\n"
  },
  {
    "path": "src/plugins/items/equipment/unequip-item.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\n\nexport const handler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, itemDetails } = details;\n\n    const equipment = player.equipment;\n    const item = getItemFromContainer(itemId, itemSlot, equipment);\n\n    if (!item) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    if (!itemDetails) {\n        // The item is not yet configured on the server.\n        player.sendMessage(`Item ${itemId} is not yet configured on the server.`);\n        return;\n    }\n\n    player.unequipItem(itemDetails.equipmentData?.equipmentSlot);\n};\n\nexport default {\n    pluginId: 'rs:unequip_item',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: [widgets.equipment, widgets.equipmentStats],\n            options: 'remove',\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/herblore/clean-herb.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { logger } from '@runejs/common';\n\ninterface IGrimyHerb {\n    grimy: ItemDetails;\n    clean: ItemDetails;\n    level: number;\n    experience: number;\n}\n\n/**\n * A list of all the herbs that can be cleaned.\n *\n * (Jameskmonger) I have put ! after findItem() because we know the items exist.\n */\nconst herbs: IGrimyHerb[] = [\n    {\n        grimy: findItem('rs:grimy_guam')!,\n        clean: findItem('rs:herb_guam')!,\n        level: 3,\n        experience: 2.5,\n    },\n    {\n        grimy: findItem('rs:grimy_marrentill')!,\n        clean: findItem('rs:herb_marrentill')!,\n        level: 5,\n        experience: 3.8,\n    },\n    {\n        grimy: findItem('rs:grimy_tarromin')!,\n        clean: findItem('rs:herb_tarromin')!,\n        level: 11,\n        experience: 5,\n    },\n    {\n        grimy: findItem('rs:grimy_harralander')!,\n        clean: findItem('rs:herb_harralander')!,\n        level: 20,\n        experience: 6.3,\n    },\n    {\n        grimy: findItem('rs:grimy_ranarr')!,\n        clean: findItem('rs:herb_ranarr')!,\n        level: 25,\n        experience: 7.5,\n    },\n    {\n        grimy: findItem('rs:grimy_toadflax')!,\n        clean: findItem('rs:herb_toadflax')!,\n        level: 30,\n        experience: 8,\n    },\n    {\n        grimy: findItem('rs:grimy_irit')!,\n        clean: findItem('rs:herb_irit')!,\n        level: 40,\n        experience: 8.8,\n    },\n    {\n        grimy: findItem('rs:grimy_avantoe')!,\n        clean: findItem('rs:herb_avantoe')!,\n        level: 48,\n        experience: 10,\n    },\n    {\n        grimy: findItem('rs:grimy_kwuarm')!,\n        clean: findItem('rs:herb_kwuarm')!,\n        level: 54,\n        experience: 11.3,\n    },\n    {\n        grimy: findItem('rs:grimy_snapdragon')!,\n        clean: findItem('rs:herb_snapdragon')!,\n        level: 59,\n        experience: 11.8,\n    },\n    {\n        grimy: findItem('rs:grimy_cadantine')!,\n        clean: findItem('rs:herb_cadantine')!,\n        level: 65,\n        experience: 12.5,\n    },\n    {\n        grimy: findItem('rs:grimy_lantadyme')!,\n        clean: findItem('rs:herb_lantadyme')!,\n        level: 67,\n        experience: 13.1,\n    },\n    {\n        grimy: findItem('rs:grimy_dwarf_weed')!,\n        clean: findItem('rs:herb_dwarf_weed')!,\n        level: 70,\n        experience: 13.8,\n    },\n    {\n        grimy: findItem('rs:grimy_torstol')!,\n        clean: findItem('rs:herb_torstol')!,\n        level: 75,\n        experience: 15,\n    },\n];\n\nexport const action: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot } = details;\n    const herb = herbs.find(herb => herb.grimy.gameId === itemId);\n    if (!herb) {\n        return;\n    }\n    if (!player.skills.hasLevel('herblore', herb.level)) {\n        player.sendMessage(`You need a Herblore level of ${herb.level} to identify this herb.`, true);\n        return;\n    }\n\n    const inventoryItem = player.inventory.items[itemSlot];\n\n    // Always check for cheaters\n    if (!inventoryItem) {\n        logger.warn(`[herblore] Player ${player.username} tried to clean herb without having it in their inventory.`);\n        return;\n    }\n\n    if (inventoryItem.itemId !== herb.grimy.gameId) {\n        logger.warn(`[herblore] Player ${player.username} tried to clean herb but itemId did not match.`);\n        return;\n    }\n\n    player.skills.addExp('herblore', herb.experience);\n    player.inventory.set(itemSlot, { itemId: herb.clean.gameId, amount: 1 });\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n    player.playSound(soundIds.herblore.clean_herb);\n};\n\nexport default {\n    type: 'item_action',\n    widgets: widgets.inventory,\n    options: 'identify',\n    itemIds: herbs.map(herb => herb.grimy.gameId),\n    action,\n    cancelOtherActions: true,\n};\n"
  },
  {
    "path": "src/plugins/items/move-item.plugin.ts",
    "content": "import type { itemSwapActionHandler } from '@engine/action/pipe/item-swap.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ItemContainer } from '@engine/world/items/item-container';\n\ntype WidgetDetail = [number, number, (player: Player) => ItemContainer];\n\nconst movableWidgets: WidgetDetail[] = [\n    // Player Bank Screen\n    [widgets.bank.screenWidget.widgetId, widgets.bank.screenWidget.containerId, player => player.bank],\n];\n\nfunction moveItem(\n    player: Player,\n    container: ItemContainer,\n    widget: { widgetId: number; containerId: number },\n    fromSlot: number,\n    toSlot: number,\n): void {\n    if (toSlot > container.size - 1 || fromSlot > container.size - 1) {\n        return;\n    }\n\n    if (fromSlot < toSlot) {\n        let slot = toSlot;\n        let current = container.remove(fromSlot);\n        while (slot >= fromSlot) {\n            const temp = container.remove(slot);\n            container.set(slot, current);\n            current = temp;\n            slot--;\n        }\n    } else {\n        let slot = toSlot;\n        let current = container.remove(fromSlot);\n        while (slot <= fromSlot) {\n            const temp = container.remove(slot);\n            container.set(slot, current);\n            current = temp;\n            slot++;\n        }\n    }\n\n    player.outgoingPackets.sendUpdateAllWidgetItems(widget, container);\n}\n\nexport const action: itemSwapActionHandler = details => {\n    const { player, widgetId, containerId, fromSlot, toSlot } = details;\n\n    const widgetDetails = movableWidgets.filter(widgetDetail => widgetDetail[0] === widgetId && widgetDetail[1] === containerId);\n    if (widgetDetails && widgetDetails[0]) {\n        const itemContainer: ItemContainer = widgetDetails[0][2](player);\n        moveItem(player, itemContainer, { widgetId, containerId }, fromSlot, toSlot);\n    }\n};\n\nexport default {\n    pluginId: 'rs:move_item',\n    hooks: [\n        {\n            type: 'move_item',\n            widgetIds: movableWidgets.map(widgetDetails => widgetDetails[0]),\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/pickup-item.plugin.ts",
    "content": "import type { ActionCancelType } from '@engine/action/action-pipeline';\nimport type { spawnedItemInteractionHandler } from '@engine/action/pipe/spawned-item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { Item } from '@engine/world/items/item';\nimport { logger } from '@runejs/common';\n\nexport const handler: spawnedItemInteractionHandler = ({ player, worldItem, itemDetails }) => {\n    const inventory = player.inventory;\n    const amount = worldItem.amount;\n    let slot = -1;\n\n    if (itemDetails.stackable) {\n        const existingItemIndex = inventory.findIndex(worldItem.itemId);\n        if (existingItemIndex !== -1) {\n            const existingItem = inventory.items[existingItemIndex];\n            if (existingItem && existingItem.amount + worldItem.amount >= 2147483647) {\n                // @TODO create new item stack\n                return;\n            } else {\n                slot = existingItemIndex;\n            }\n        }\n    }\n\n    if (slot === -1) {\n        slot = inventory.getFirstOpenSlot();\n    }\n\n    if (slot === -1) {\n        player.sendMessage(`You don't have enough free space to do that.`);\n        return;\n    }\n\n    if (!worldItem.instance) {\n        logger.error(`World item ${worldItem.itemId} has no instance`);\n        return;\n    }\n\n    worldItem.instance.despawnWorldItem(worldItem);\n\n    const item: Item = {\n        itemId: worldItem.itemId,\n        amount,\n    };\n\n    const addedItem = inventory.add(item);\n\n    if (!addedItem) {\n        logger.error(`Failed to add item ${item.itemId} to inventory for player ${player.username}`);\n        return;\n    }\n\n    player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, addedItem.slot, addedItem.item);\n    player.playSound(soundIds.pickupItem, 3);\n    // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now\n    player.actionsCancelled.next(null as unknown as ActionCancelType);\n};\n\nexport default {\n    pluginId: 'rs:pickup_item',\n    hooks: [\n        {\n            type: 'spawned_item_interaction',\n            options: 'pick-up',\n            handler,\n            walkTo: true,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/pots/empty-pot.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\n\nexport const action: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot } = details;\n\n    const inventory = player.inventory;\n    const item = getItemFromContainer(itemId, itemSlot, inventory);\n\n    if (!item) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    inventory.remove(itemSlot);\n    player.playSound(soundIds.potContentModified, 5);\n    player.giveItem(itemIds.pot);\n};\n\nexport default {\n    pluginId: 'rs:empty_pot',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'empty',\n            itemIds: [itemIds.potOfFlour],\n            handler: action,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/helpers/rotten-potato-helpers.ts",
    "content": "import type { ItemOnItemAction } from '@engine/action/pipe/item-on-item.action';\nimport type { WidgetInteractionAction } from '@engine/action/pipe/widget-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport { Rights } from '@engine/world/actor/player/player';\n\n/**\n * The rotten potato item.\n *\n * (Jameskmonger) I have put ! after findItem() because we know the item exists.\n */\nexport const RottenPotatoItem: ItemDetails = findItem('rs:rotten_potato')!;\n\nexport const ExecuteIfAdmin = (details: ItemOnItemAction | WidgetInteractionAction, callback) => {\n    if (details.player.rights === Rights.ADMIN) {\n        callback(details);\n        return;\n    }\n    while (details.player.inventory.has(RottenPotatoItem.gameId)) {\n        details.player.inventory.removeFirst(RottenPotatoItem.gameId, false);\n    }\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n};\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts",
    "content": "import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\n\nconst INTRO_PAGE_COUNT = 1;\nconst ITEMS_PER_PAGE = 15;\nconst pageCount = INTRO_PAGE_COUNT + Math.round(activeWorld.travelLocations.locations.length / ITEMS_PER_PAGE);\n\nexport function openTravel(player: Player, page: number) {\n    const widget = player.interfaceState.openWidget(27, {\n        slot: 'screen',\n        fakeWidget: 3100002,\n        metadata: {\n            page: page,\n        },\n    });\n\n    // Prev page button\n    player.modifyWidget(widget.widgetId, {\n        childId: 95,\n        hidden: widget.metadata.page === 1, // hide prev page button if we are on the first page\n    });\n\n    const isLastPage = p => pageCount === page * 2; // 2 \"pages\" per page\n\n    // Next page buttton\n    player.modifyWidget(widget.widgetId, {\n        childId: 97,\n        hidden: isLastPage(page),\n    });\n\n    // prev page label\n    player.modifyWidget(widget.widgetId, {\n        childId: 98,\n        text: widget.metadata.page * 2 - 1 === 1 ? `` : `Page ${widget.metadata.page * 2 - 1} `,\n    });\n\n    // next page label\n    player.modifyWidget(widget.widgetId, {\n        childId: 99,\n        text: `Page ${widget.metadata.page * 2} <nbsp></nbsp>`,\n    });\n\n    // clear default lines of both open pages\n    for (let i = 0; i < ITEMS_PER_PAGE * 2; i++) {\n        player.modifyWidget(widget.widgetId, {\n            childId: 33 + i,\n            text: '',\n            hidden: true,\n        });\n    }\n\n    let currentLocation = ITEMS_PER_PAGE * 2 * (page - INTRO_PAGE_COUNT - 1) + INTRO_PAGE_COUNT * ITEMS_PER_PAGE;\n\n    const historyPage = [\n        '<col=CCCCFF>Last locations</col>',\n        '',\n        '',\n        ...(player.savedMetadata.lastTravel || new Array(10)).map((location: number | undefined) =>\n            location === undefined ? '' : activeWorld.travelLocations.locations[location]?.name,\n        ),\n    ];\n\n    if (widget.metadata.page * 2 - 1 === 1) {\n        for (let i = 0; i < ITEMS_PER_PAGE * 2; i += 2) {\n            player.modifyWidget(widget.widgetId, {\n                childId: 101 + i,\n                text: historyPage[currentLocation + ITEMS_PER_PAGE] || '',\n                hidden: false,\n            });\n            currentLocation++;\n        }\n    } else {\n        for (let i = 0; i < ITEMS_PER_PAGE * 2; i += 2) {\n            player.modifyWidget(widget.widgetId, {\n                childId: 101 + i,\n                text: activeWorld.travelLocations.locations[currentLocation]?.name || '',\n                hidden: !activeWorld.travelLocations.locations[currentLocation]?.name,\n            });\n            currentLocation++;\n        }\n    }\n\n    for (let i = 0; i < 30; i += 2) {\n        player.modifyWidget(widget.widgetId, {\n            childId: 131 + i - 1,\n            hidden: !activeWorld.travelLocations.locations[currentLocation]?.name,\n        });\n        player.modifyWidget(widget.widgetId, {\n            childId: 131 + i,\n            text: activeWorld.travelLocations.locations[currentLocation]?.name || '',\n            hidden: !activeWorld.travelLocations.locations[currentLocation]?.name,\n        });\n        currentLocation++;\n    }\n}\n\nexport const travelMenuInteract: widgetInteractionActionHandler = details => {\n    const playerWidget = details.player.interfaceState.findWidget(27);\n\n    if (!playerWidget || !playerWidget.metadata.page) {\n        return;\n    }\n    switch (details.childId) {\n        case 160:\n            openTravel(details.player, 1);\n            return;\n        case 94:\n            openTravel(details.player, playerWidget.metadata.page - 1);\n            return;\n        case 96:\n            openTravel(details.player, playerWidget.metadata.page + 1);\n            return;\n    }\n    let selectedIndex: number | undefined = undefined;\n    if (details.childId >= 101 && details.childId <= 129) {\n        selectedIndex = (details.childId - 99) / 2 - 1;\n    }\n    if (details.childId >= 131 && details.childId <= 159) {\n        selectedIndex = (details.childId - 129) / 2 - 1 + 15;\n    }\n    if (selectedIndex != undefined) {\n        let teleportIndex = selectedIndex + 30 * (playerWidget.metadata.page - 1);\n        if (!details.player.savedMetadata.lastTravel) {\n            details.player.savedMetadata.lastTravel = new Array(10);\n        }\n\n        if (teleportIndex < INTRO_PAGE_COUNT * ITEMS_PER_PAGE) {\n            if (teleportIndex < 3 && teleportIndex > 12) {\n                openTravel(details.player, playerWidget.metadata.page);\n            }\n            teleportIndex = details.player.savedMetadata.lastTravel[teleportIndex - 3];\n            if (!teleportIndex) {\n                openTravel(details.player, playerWidget.metadata.page);\n            }\n        } else {\n            teleportIndex = teleportIndex - INTRO_PAGE_COUNT * ITEMS_PER_PAGE;\n        }\n\n        const newTravelLog = [teleportIndex, ...details.player.savedMetadata.lastTravel.slice(0, 9)];\n        for (let index = 1; newTravelLog.length; index++) {\n            const element = newTravelLog[index];\n            if (element === teleportIndex) {\n                newTravelLog[index] = newTravelLog[index + 1];\n            }\n            if (element === undefined) {\n                if (newTravelLog[index + 1] === undefined) {\n                    break;\n                }\n                newTravelLog[index] = newTravelLog[index + 1];\n            }\n        }\n        details.player.savedMetadata.lastTravel = newTravelLog;\n\n        details.player.teleport(activeWorld.travelLocations.locations[teleportIndex].position);\n\n        details.player.interfaceState.closeAllSlots();\n    } else {\n        openTravel(details.player, playerWidget.metadata.page);\n    }\n};\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/hooks/rotten-potato-command-hook.ts",
    "content": "import type { commandActionHandler } from '@engine/action/pipe/player-command.action';\n\nconst spawnPotato: commandActionHandler = details => {\n    details.player.giveItem('rs:rotten_potato');\n};\n\nexport default spawnPotato;\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/hooks/rotten-potato-eat.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { dialogue, execute } from '@engine/world/actor/dialogue';\n\nenum DialogueOption {\n    SET_ALL_STATS,\n    WIPE_INVENTORY,\n    SETUP_POH,\n    TELEPORT_TO_PLAYER,\n    SPAWN_AGGRESSIVE_NPC,\n}\n\nconst eatPotato: itemInteractionActionHandler = async details => {\n    let chosenOption: DialogueOption;\n\n    await dialogue(\n        [details.player],\n        [\n            options => [\n                `Set all stats`,\n                [execute(() => (chosenOption = DialogueOption.SET_ALL_STATS))],\n                `Wipe inventory`,\n                [execute(() => (chosenOption = DialogueOption.WIPE_INVENTORY))],\n                `Setup POH`,\n                [execute(() => (chosenOption = DialogueOption.SETUP_POH))],\n                `Teleport to player`,\n                [execute(() => (chosenOption = DialogueOption.TELEPORT_TO_PLAYER))],\n                `Spawn aggressive NPC`,\n                [execute(() => (chosenOption = DialogueOption.SPAWN_AGGRESSIVE_NPC))],\n            ],\n        ],\n    );\n\n    switch (chosenOption!) {\n        case DialogueOption.SET_ALL_STATS:\n            break;\n        case DialogueOption.TELEPORT_TO_PLAYER:\n            break;\n        default:\n            break;\n    }\n};\n\nexport default eatPotato;\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/hooks/rotten-potato-item-on-item.ts",
    "content": "import type { itemOnItemActionHandler } from '@engine/action/pipe/item-on-item.action';\nimport { findItem } from '@engine/config/config-handler';\nimport { RottenPotatoItem } from '@plugins/items/rotten-potato/helpers/rotten-potato-helpers';\n\nconst itemOnPotato: itemOnItemActionHandler = details => {\n    const slotToDelete = details.usedItem.itemId === RottenPotatoItem.gameId ? details.usedWithSlot : details.usedSlot;\n\n    const inventoryItem = details.player.inventory.items[slotToDelete];\n\n    if (!inventoryItem) {\n        details.player.sendMessage(`You don't have that item in your inventory.`);\n        return;\n    }\n\n    const item = inventoryItem.itemId;\n    const itemDetails = findItem(item);\n    details.player.removeItem(slotToDelete);\n    details.player.sendLogMessage(`Whee... ${itemDetails?.name || 'Unknown item'} All gone!`, false);\n};\n\nexport default itemOnPotato;\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/hooks/rotten-potato-item-on-player.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport type { itemOnPlayerActionHandler } from '@engine/action/pipe/item-on-player.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Item } from '@engine/world/items/item';\nimport { logger } from '@runejs/common';\n\nexport const potatoOnPlayer: itemOnPlayerActionHandler = details => {\n    const widget = details.player.interfaceState.openWidget(widgets.bank.depositBoxWidget.widgetId, {\n        slot: 'screen',\n        fakeWidget: 3100001,\n    });\n    widget.metadata['player'] = details.otherPlayer;\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, details.otherPlayer.inventory);\n\n    details.player.modifyWidget(widgets.bank.depositBoxWidget.widgetId, {\n        childId: widgets.bank.depositBoxWidget.titleText,\n        text: `${details.otherPlayer.username}'s Inventory`,\n    });\n};\nexport const potatoManipulatePlayerInventory: itemInteractionActionHandler = details => {\n    const playerWidget = details.player.interfaceState.findWidget(widgets.bank.depositBoxWidget.widgetId);\n\n    if (!playerWidget) {\n        return;\n    }\n    const otherPlayer: Player = playerWidget.metadata['player'];\n\n    if (!otherPlayer) {\n        return;\n    }\n\n    // If the item is a noted item, we need to de-note it\n    const itemIdToAdd: number = details.itemId;\n\n    let countToRemove: number;\n    if (details.option.endsWith('all')) {\n        countToRemove = -1;\n    } else {\n        countToRemove = +details.option.replace('deposit-', '');\n    }\n\n    const slotsWithItem = otherPlayer.inventory.findAll(details.itemId);\n    let itemAmount = 0;\n    slotsWithItem.forEach(slot => {\n        const item = otherPlayer.inventory.items[slot];\n\n        if (!item) {\n            throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        if (item.itemId !== details.itemId) {\n            throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        itemAmount += item.amount;\n    });\n    if (countToRemove == -1 || countToRemove > itemAmount) {\n        countToRemove = itemAmount;\n    }\n\n    if (!details.player.inventory.canFit({ itemId: itemIdToAdd, amount: countToRemove })) {\n        details.player.sendMessage('Your inventory is full.');\n        return;\n    }\n\n    const itemToAdd: Item = { itemId: itemIdToAdd, amount: 0 };\n    while (countToRemove > 0 && otherPlayer.inventory.has(details.itemId)) {\n        const invIndex = otherPlayer.inventory.findIndex(details.itemId);\n        const invItem = otherPlayer.inventory.items[invIndex];\n\n        if (!invItem) {\n            logger.error(`Could not find item ${details.itemId} in inventory at slot ${invIndex} in rotten potato`);\n            return;\n        }\n\n        if (countToRemove >= invItem.amount) {\n            itemToAdd.amount += invItem.amount;\n            countToRemove -= invItem.amount;\n            otherPlayer.inventory.remove(invIndex);\n        } else {\n            itemToAdd.amount += countToRemove;\n            invItem.amount -= countToRemove;\n            countToRemove = 0;\n        }\n    }\n\n    details.player.inventory.addStacking(itemToAdd);\n\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, otherPlayer.inventory);\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n    otherPlayer.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, otherPlayer.inventory);\n};\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/hooks/rotten-potato-peel.ts",
    "content": "import { getActionHooks } from '@engine/action/hook/action-hook';\nimport { advancedNumberHookFilter } from '@engine/action/hook/hook-filters';\nimport type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action';\nimport { dialogue, execute } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { openTravel } from '@plugins/items/rotten-potato/helpers/rotten-potato-travel';\n\nfunction openBank(player: Player) {\n    const interactionActions = getActionHooks<ObjectInteractionActionHook>('object_interaction').filter(plugin =>\n        advancedNumberHookFilter(plugin.objectIds, objectIds.bankBooth[0], plugin.options, 'use-quickly'),\n    );\n    interactionActions.forEach(plugin => {\n        if (!plugin.handler) {\n            return;\n        }\n\n        plugin.handler({\n            player: player,\n            object: {\n                objectId: objectIds.bankBooth[0],\n                level: player.position.level,\n                x: player.position.x,\n                y: player.position.y,\n                orientation: 0,\n                type: 0,\n            },\n            option: 'use-quickly',\n            position: player.position,\n            objectConfig: undefined as any,\n            cacheOriginal: undefined as any,\n        });\n    });\n}\n\nenum DialogueOption {\n    BANK,\n    TELEPORT_MENU,\n    TELEPORT_TO_RARE_DROP,\n    FORCE_RARE_DROP,\n}\n\nconst peelPotato: itemInteractionActionHandler = async details => {\n    let chosenOption: DialogueOption;\n    // console.log(world.travelLocations.locations)\n    await dialogue(\n        [details.player],\n        [\n            options => [\n                `Bank menu`,\n                [execute(() => (chosenOption = DialogueOption.BANK))],\n                `Travel Far!`,\n                [execute(() => (chosenOption = DialogueOption.TELEPORT_MENU))],\n                // `Teleport to RARE!`, [\n                //     execute(() => chosenOption = DialogueOption.TELEPORT_TO_RARE_DROP)\n                // ],\n                // `Spawn RARE!`, [\n                //     execute(() => chosenOption = DialogueOption.FORCE_RARE_DROP)\n                // ],\n            ],\n        ],\n    );\n\n    // using ! here because we have just set it in the dialogue\n    switch (chosenOption!) {\n        case DialogueOption.BANK:\n            openBank(details.player);\n            break;\n        case DialogueOption.TELEPORT_MENU:\n            openTravel(details.player, 1);\n            break;\n        default:\n            break;\n    }\n};\n\nexport default peelPotato;\n"
  },
  {
    "path": "src/plugins/items/rotten-potato/rotten-potato.plugin.ts",
    "content": "import type { WidgetInteractionActionHook } from '@engine/action/pipe/widget-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { ExecuteIfAdmin, RottenPotatoItem } from '@plugins/items/rotten-potato/helpers/rotten-potato-helpers';\nimport { travelMenuInteract } from '@plugins/items/rotten-potato/helpers/rotten-potato-travel';\nimport spawnPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-command-hook';\nimport eatPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-eat';\nimport itemOnPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-item-on-item';\nimport { potatoManipulatePlayerInventory, potatoOnPlayer } from '@plugins/items/rotten-potato/hooks/rotten-potato-item-on-player';\nimport peelPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-peel';\n\nexport default {\n    pluginId: 'promises:rotten-potato',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            itemIds: RottenPotatoItem.gameId,\n            options: 'peel',\n            handler: details => ExecuteIfAdmin(details, peelPotato),\n            cancelOtherActions: true,\n        },\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            itemIds: RottenPotatoItem.gameId,\n            options: 'eat',\n            handler: details => ExecuteIfAdmin(details, eatPotato),\n            cancelOtherActions: true,\n        },\n        {\n            type: 'item_on_player',\n            itemIds: RottenPotatoItem.gameId,\n            handler: details => ExecuteIfAdmin(details, potatoOnPlayer),\n            walkTo: false,\n        },\n        {\n            type: 'player_command',\n            commands: ['potato'],\n            handler: details => ExecuteIfAdmin(details, spawnPotato),\n            cancelOtherActions: true,\n        },\n        {\n            type: 'item_on_item',\n            items: [{ item1: RottenPotatoItem.gameId }],\n            handler: details => ExecuteIfAdmin(details, itemOnPotato),\n            cancelOtherActions: true,\n        },\n        {\n            type: 'item_interaction',\n            widgets: { ...widgets.bank.depositBoxWidget, widgetId: 3100001 },\n            options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'],\n            handler: details => ExecuteIfAdmin(details, potatoManipulatePlayerInventory),\n        },\n        {\n            type: 'widget_interaction',\n            widgetIds: 3100002,\n            handler: details => ExecuteIfAdmin(details, travelMenuInteract),\n            multi: true,\n        } as WidgetInteractionActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/runecrafting/tiaras.plugin.ts",
    "content": "import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action';\n\nexport const equip: equipmentChangeActionHandler = details => {\n    const { player } = details;\n    player.outgoingPackets.updateClientConfig(491, 1);\n};\nexport const unequip: equipmentChangeActionHandler = details => {\n    const { player } = details;\n    player.outgoingPackets.updateClientConfig(491, 0);\n};\n\nexport default {\n    pluginId: 'rs:tiaras',\n    hooks: [\n        {\n            type: 'equipment_change',\n            eventType: 'equip',\n            handler: equip,\n            itemIds: 5527,\n        },\n        {\n            type: 'equipment_change',\n            eventType: 'unequip',\n            handler: unequip,\n            itemIds: 5527,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/shopping/buy-from-shop.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findItem, findShop, widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport type { Item } from '@engine/world/items/item';\nimport type { ItemContainer } from '@engine/world/items/item-container';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\nimport { logger } from '@runejs/common';\n\nfunction removeCoins(inventory: ItemContainer, coinsIndex: number, cost: number): void {\n    const coins = inventory.items[coinsIndex];\n\n    if (!coins) {\n        logger.error(`Could not find coins in inventory at slot ${coinsIndex} while trying to remove coins`);\n        return;\n    }\n\n    const amountAfterPurchase = coins.amount - cost;\n    inventory.set(coinsIndex, { itemId: itemIds.coins, amount: amountAfterPurchase });\n}\n\nexport const handler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, widgetId, option } = details;\n\n    if (!player.interfaceState.findWidget(widgetId)) {\n        return;\n    }\n\n    const openedShopKey = player.metadata.lastOpenedShopKey;\n    if (!openedShopKey) {\n        return;\n    }\n\n    const shop = findShop(openedShopKey);\n    if (!shop) {\n        return;\n    }\n\n    const shopContainer = shop.container;\n    const shopItem = getItemFromContainer(itemId, itemSlot, shopContainer);\n\n    if (!shopItem) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    if (shopItem.amount <= 0) {\n        // Out of stock\n        return;\n    }\n\n    const buyAmounts = {\n        'buy-1': 1,\n        'buy-5': 5,\n        'buy-10': 10,\n    };\n    let buyAmount = buyAmounts[option];\n    if (shopItem.amount < buyAmount) {\n        buyAmount = shopItem.amount;\n    }\n\n    const buyItem = findItem(itemId);\n    if (!buyItem) {\n        logger.error(`Could not find cache item for item id ${itemId} in shop ${openedShopKey}`);\n        return;\n    }\n    const buyItemValue = shop.getBuyFromShopPrice(buyItem);\n    player.sendMessage(`${buyItem.key} : ${buyItemValue}, ${buyItem.value}`);\n    let buyCost = buyAmount * buyItemValue;\n    const coinsIndex = player.hasCoins(buyCost);\n\n    if (coinsIndex === -1) {\n        player.sendMessage(`You don't have enough coins.`);\n        return;\n    }\n\n    const inventory = player.inventory;\n\n    if (buyItem.stackable) {\n        const inventoryStackSlot = inventory.items.findIndex(item => itemId === itemId);\n\n        if (inventoryStackSlot === -1) {\n            if (inventory.getFirstOpenSlot() === -1) {\n                player.sendMessage(`You don't have enough space in your inventory.`);\n                return;\n            }\n        } else {\n            const inventoryItem = inventory.items[inventoryStackSlot];\n\n            if (!inventoryItem) {\n                logger.error(\n                    `Coult not find inventory item at slot ${inventoryStackSlot} for player ${player.username} while trying to stack`,\n                );\n                return;\n            }\n\n            if (inventoryItem.amount + buyAmount >= 2147483647) {\n                player.sendMessage(`You don't have enough space in your inventory.`);\n                return;\n            }\n\n            shopContainer.set(itemSlot, { itemId, amount: shopItem.amount - buyAmount });\n            removeCoins(inventory, coinsIndex, buyCost);\n\n            const item: Item = {\n                itemId,\n                amount: inventoryItem.amount + buyAmount,\n            };\n\n            inventory.set(inventoryStackSlot, item);\n        }\n    } else {\n        let bought = 0;\n\n        for (let i = 0; i < buyAmount; i++) {\n            if (inventory.add({ itemId, amount: 1 }) !== null) {\n                bought++;\n            } else {\n                break;\n            }\n        }\n\n        if (bought !== buyAmount) {\n            player.sendMessage(`You don't have enough space in your inventory.`);\n        }\n\n        shopContainer.set(itemSlot, { itemId, amount: shopItem.amount - bought });\n        buyCost = bought * buyItemValue;\n        removeCoins(inventory, coinsIndex, buyCost);\n    }\n\n    player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.shop, itemSlot, shopContainer.items[itemSlot]);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, inventory);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory);\n};\n\nexport default {\n    pluginId: 'rs:shop_buy',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.shop,\n            options: ['buy-1', 'buy-5', 'buy-10'],\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/shopping/item-value.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findItem, findShop, widgets } from '@engine/config/config-handler';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\n\nexport const shopSellValueHandler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, widgetId, option } = details;\n\n    if (!player.interfaceState.findWidget(widgetId)) {\n        return;\n    }\n\n    const openedShopKey = player.metadata.lastOpenedShopKey;\n    if (!openedShopKey) {\n        return;\n    }\n\n    const shop = findShop(openedShopKey);\n    if (!shop) {\n        return;\n    }\n\n    const shopContainer = shop.container;\n    const shopItem = getItemFromContainer(itemId, itemSlot, shopContainer);\n    console.log(itemId, itemSlot, openedShopKey, shopContainer);\n\n    if (!shopItem) {\n        // The specified item was not found in the specified slot.\n        player.sendMessage(`ERROR item not in shopslot.`);\n        return;\n    }\n\n    if (shopItem.amount <= 0) {\n        player.sendMessage(`The shop has ran out of stock.`);\n        // Out of stock\n        return;\n    }\n    const buyItem = findItem(itemId);\n    if (!buyItem) {\n        // The specified item was not found in the specified slot.\n        player.sendMessage(`Error item does not exist. [id:${itemId}]`);\n        return;\n    }\n    player.sendMessage(`${buyItem.name}: currently costs ${shop.getBuyFromShopPrice(buyItem)} coins.`);\n};\n\nexport const shopPurchaseValueHandler: itemInteractionActionHandler = ({ player, itemDetails }) => {\n    const openedShopKey = player.metadata.lastOpenedShopKey;\n    if (!openedShopKey) {\n        return;\n    }\n\n    const shop = findShop(openedShopKey);\n    if (!shop) {\n        return;\n    }\n\n    const shopBuyPrice = shop.getBuyFromShopPrice(itemDetails);\n\n    if (shopBuyPrice === -1) {\n        player.sendMessage(`You can't sell this item to this shop.`);\n    } else {\n        player.sendMessage(`${itemDetails.name}: shop will buy for ${shopBuyPrice} coins.`);\n    }\n};\n\nexport default {\n    pluginId: 'rs:shop_item_value',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.shop,\n            options: 'value',\n            handler: shopSellValueHandler,\n            cancelOtherActions: false,\n        },\n        {\n            type: 'item_interaction',\n            widgets: widgets.shopPlayerInventory,\n            options: 'value',\n            handler: shopPurchaseValueHandler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/shopping/sell-to-shop.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findShop, widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { getItemFromContainer } from '@engine/world/items/item-container';\n\nexport const handler: itemInteractionActionHandler = details => {\n    const { player, itemId, itemSlot, option, itemDetails } = details;\n\n    if (!player.interfaceState.findWidget(widgets.shop.widgetId)) {\n        return;\n    }\n\n    const openedShopKey = player.metadata.lastOpenedShopKey;\n    if (!openedShopKey) {\n        return;\n    }\n\n    const shop = findShop(openedShopKey);\n    if (!shop) {\n        return;\n    }\n\n    const inventory = player.inventory;\n    const inventoryItem = getItemFromContainer(itemId, itemSlot, inventory);\n\n    if (!inventoryItem) {\n        // The specified item was not found in the specified slot.\n        return;\n    }\n\n    const sellAmounts = {\n        'sell-1': 1,\n        'sell-5': 5,\n        'sell-10': 10,\n    };\n    let sellAmount = sellAmounts[option];\n    const shopContainer = shop.container;\n    const shopSpaces = shopContainer.items.filter(item => item === null);\n\n    const shopItemIndex = shopContainer.items.findIndex(item => item !== null && item.itemId === itemId);\n    if (shopItemIndex === -1 && shopSpaces.length === 0) {\n        player.sendMessage(`There isn't enough space in the shop.`);\n        return;\n    }\n\n    const shopItem = shopContainer.items[shopItemIndex];\n\n    if (itemDetails.stackable) {\n        if (inventoryItem.amount < sellAmount) {\n            inventory.remove(itemSlot);\n            sellAmount = inventoryItem.amount;\n        } else {\n            inventory.set(itemSlot, { itemId, amount: inventoryItem.amount - sellAmount });\n        }\n    } else {\n        const foundItems = inventory.items.map((item, i) => (item !== null && item.itemId === itemId ? i : null)).filter(i => i !== null);\n        if (foundItems.length < sellAmount) {\n            sellAmount = foundItems.length;\n        }\n\n        for (let i = 0; i < sellAmount; i++) {\n            const item = foundItems[i];\n\n            if (!item) {\n                throw new Error(`Inventory item was not present, for item id ${itemId} in inventory, while trying to sell`);\n            }\n\n            inventory.remove(item);\n        }\n    }\n\n    const itemValue = shop.getSellToShopPrice(itemDetails); // @TODO scale price per item, not per sale\n\n    if (!shopItem) {\n        shopContainer.set(shopContainer.getFirstOpenSlot(), { itemId, amount: sellAmount });\n    } else {\n        shopItem.amount += sellAmount;\n    }\n\n    const sellPrice = sellAmount * itemValue; // @TODO scale price per item, not per sale\n    if (sellPrice > 0) {\n        let coinsIndex = player.hasCoins(1);\n\n        if (coinsIndex === -1) {\n            coinsIndex = inventory.getFirstOpenSlot();\n            inventory.set(coinsIndex, { itemId: itemIds.coins, amount: sellPrice });\n        } else {\n            // TODO (Jameskmonger) consider being explicit to prevent dupes\n            inventory.items[coinsIndex]!.amount += sellPrice;\n        }\n    }\n\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, shopContainer);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, inventory);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory);\n};\n\nexport default {\n    pluginId: 'rs:shop_sell',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.shopPlayerInventory,\n            options: ['sell-1', 'sell-5', 'sell-10'],\n            handler,\n            cancelOtherActions: false,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/items/swap-items.plugin.ts",
    "content": "import type { itemSwapActionHandler } from '@engine/action/pipe/item-swap.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ItemContainer } from '@engine/world/items/item-container';\n\ntype WidgetDetail = [number, number, (player: Player) => ItemContainer];\n\nconst swappableWidgets: WidgetDetail[] = [\n    // Player Inventory\n    [widgets.inventory.widgetId, widgets.inventory.containerId, player => player.inventory],\n    // Player Bank Screen\n    [widgets.bank.screenWidget.widgetId, widgets.bank.screenWidget.containerId, player => player.bank],\n];\n\nfunction swapItems(container: ItemContainer, fromSlot: number, toSlot: number): void {\n    if (toSlot > container.size - 1 || fromSlot > container.size - 1) {\n        return;\n    }\n\n    container.swap(fromSlot, toSlot);\n}\n\nexport const action: itemSwapActionHandler = details => {\n    const { player, widgetId, containerId, fromSlot, toSlot } = details;\n\n    const widgetDetails = swappableWidgets.filter(widgetDetail => widgetDetail[0] === widgetId && widgetDetail[1] === containerId);\n    if (widgetDetails && widgetDetails[0]) {\n        const itemContainer: ItemContainer = widgetDetails[0][2](player);\n        swapItems(itemContainer, fromSlot, toSlot);\n    }\n};\n\nexport default {\n    pluginId: 'rs:swap_items',\n    hooks: [\n        {\n            type: 'item_swap',\n            widgetIds: swappableWidgets.map(widgetDetails => widgetDetails[0]),\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/music/music-regions.plugin.ts",
    "content": "import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { findMusicTrack, findSongIdByRegionId, musicRegionMap, musicRegions, widgets } from '@engine/config/config-handler';\nimport { colors } from '@engine/util/colors';\nimport { MusicPlayerMode } from '@engine/world/sound/music';\n\nmusicRegions.forEach(song => song.regionIds.forEach(region => musicRegionMap.set(region, song.songId)));\n\nfunction getByValue(map, searchValue) {\n    for (const [key, value] of map.entries()) {\n        if (value === searchValue) return key;\n    }\n}\n\nconst regionChangedHandler = ({ player, currentMapRegionId }): void => {\n    const songId = findSongIdByRegionId(currentMapRegionId);\n    if (songId == null) {\n        return;\n    }\n\n    const musicTrack = findMusicTrack(songId);\n\n    if (!musicTrack) {\n        return;\n    }\n\n    const songName = musicTrack.songName;\n    // player.sendMessage(`Playing ${songId}:${getByValue(songs, songId)} at region ${currentMapRegionId}`);\n    if (!player.musicTracks.includes(songId)) {\n        player.musicTracks.push(songId);\n        player.sendMessage('You have unlocked a new music track: <col=ef101f>' + songName + '.</col>');\n        player.modifyWidget(widgets.musicPlayerTab, { childId: musicTrack.musicTabButtonId, textColor: colors.green });\n    }\n    if (player.settings.musicPlayerMode === MusicPlayerMode.AUTO) {\n        player.playSong(songId);\n    }\n};\n\nconst playerInitHandler: playerInitActionHandler = ({ player }): void => {\n    // Plays the appropriate location's song on player init\n    regionChangedHandler({ player, currentMapRegionId: ((player.position.x >> 6) << 8) + (player.position.y >> 6) });\n};\n\nexport default {\n    pluginId: 'rs:music_regions',\n    hooks: [\n        {\n            type: 'region_change',\n            regionType: 'region',\n            handler: regionChangedHandler,\n        },\n        {\n            type: 'player_init',\n            handler: playerInitHandler,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/music/music-tab.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { findMusicTrackByButtonId, findSongIdByRegionId, widgets } from '@engine/config/config-handler';\nimport { activeWorld } from '@engine/world';\nimport { widgetScripts } from '@engine/world/config/widget';\nimport { MusicPlayerMode, MusicTabButtonIds } from '@engine/world/sound/music';\nimport { logger } from '@runejs/common';\n\nexport const handler: buttonActionHandler = details => {\n    const { player, buttonId } = details;\n\n    if (buttonId === MusicTabButtonIds.AUTO_BUTTON_ID) {\n        player.settings.musicPlayerMode = MusicPlayerMode.AUTO;\n        const songIdForCurrentRegion = findSongIdByRegionId(activeWorld.chunkManager.getRegionIdForWorldPosition(player.position));\n\n        if (!songIdForCurrentRegion) {\n            logger.warn(`No song found for current region`);\n            return;\n        }\n\n        if (player.savedMetadata['currentSongIdPlaying'] !== songIdForCurrentRegion) {\n            player.playSong(songIdForCurrentRegion);\n        }\n    } else if (buttonId === MusicTabButtonIds.MANUAL_BUTTON_ID) {\n        player.settings.musicPlayerMode = MusicPlayerMode.MANUAL;\n    } else if (buttonId === MusicTabButtonIds.LOOP_BUTTON_ID) {\n        player.settings.musicPlayerLoopMode ^= 1;\n    }\n\n    const musicTrack = findMusicTrackByButtonId(buttonId);\n    if (musicTrack === null) {\n        return;\n    } else if (player.musicTracks.includes(musicTrack.songId)) {\n        player.playSong(musicTrack.songId);\n        player.settings.musicPlayerMode = MusicPlayerMode.MANUAL;\n        player.outgoingPackets.updateClientConfig(widgetScripts.musicPlayerAutoManual, 0);\n    } else {\n        player.sendMessage(\"You haven't unlocked this piece of music yet!\");\n    }\n};\n\nexport default {\n    pluginId: 'rs:music_tab',\n    hooks: [{ type: 'button', widgetId: widgets.musicPlayerTab, handler }],\n};\n"
  },
  {
    "path": "src/plugins/npcs/al-kharid/dommik-crafting-shop.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\nimport { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action';\n\nconst tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:dommiks_crafting_store')?.open(player);\n\nconst talkToAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    dialogueAction(player)\n        .then(async d => d.npc(npc, DialogueEmote.CALM_TALK_1, ['Would you like to buy some crafting equipment?']))\n        .then(async d =>\n            d.options('Would you like to buy some crafting equipment?', [\n                \"No thanks. I've got all the Crafting equipment I need.\",\n                \"Let's see what you've got, then.\",\n            ]),\n        )\n        .then(async d => {\n            switch (d.action) {\n                case 1:\n                    return d\n                        .player(DialogueEmote.JOYFUL, [\"No thanks; I've got all the Crafting equipment I need.\"])\n                        .then(async d => d.npc(npc, DialogueEmote.CALM_TALK_2, ['Okay. Fare well on your travels.']))\n                        .then(d => {\n                            d.close();\n                            return d;\n                        });\n                case 2:\n                    return d.player(DialogueEmote.CALM_TALK_1, ['No, thank you.']).then(d => {\n                        tradeAction(details);\n                        return d;\n                    });\n            }\n        });\n};\n\nexport default {\n    pluginId: 'rs:dommik_crafting_shop',\n    hooks: [\n        { type: 'npc_interaction', npcs: 'rs:alkharid_dommik', options: 'trade', walkTo: true, handler: tradeAction },\n        { type: 'npc_interaction', npcs: 'rs:alkharid_dommik', options: 'talk-to', walkTo: true, handler: talkToAction },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/al-kharid/gem-trader.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\nimport { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action';\n\nconst tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:alkharid_gem_trader')?.open(player);\n\nconst talkToAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    dialogueAction(player)\n        .then(async d =>\n            d.npc(npc, DialogueEmote.CALM_TALK_1, ['Good day to you, traveller.', 'Would you be interested in buying some gems?']),\n        )\n        .then(async d => d.options('Would you be interested in buying some gems?', ['Yes, please.', 'No, thank you.']))\n        .then(async d => {\n            switch (d.action) {\n                case 1:\n                    return d.player(DialogueEmote.JOYFUL, ['Yes, please!']).then(d => {\n                        tradeAction(details);\n                        return d;\n                    });\n                case 2:\n                    return d\n                        .player(DialogueEmote.CALM_TALK_1, ['No, thank you.'])\n                        .then(async d => d.npc(npc, DialogueEmote.ANNOYED, ['Eh, suit yourself.']))\n                        .then(d => {\n                            d.close();\n                            return d;\n                        });\n            }\n        });\n};\n\nexport default {\n    pluginId: 'rs:gem_trader',\n    hooks: [\n        { type: 'npc_interaction', npcs: 'rs:alkharid_gem_trader', options: 'trade', walkTo: true, handler: tradeAction },\n        { type: 'npc_interaction', npcs: 'rs:alkharid_gem_trader', options: 'talk-to', walkTo: true, handler: talkToAction },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/al-kharid/karim.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { logger } from '@runejs/common';\n\nconst talkToAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    dialogue(\n        [player, { npc, key: 'karim' }],\n        [\n            karim => [Emote.HAPPY, `Would you like to buy a nice kebab? Only one gold.`],\n            options => [\n                `I think i'll give it a miss.`,\n                [player => [Emote.DROWZY, `I think i'll give it a miss.`]],\n                `Yes please.`,\n                [\n                    player => [Emote.HAPPY, `Yes please.`],\n                    execute(() => {\n                        const inventory = player.inventory;\n                        if (inventory.has(itemIds.coins)) {\n                            const index = inventory.findIndex(itemIds.coins);\n                            const item = inventory.items[index];\n\n                            if (!inventory.hasSpace()) {\n                                player.sendMessage(`You don't have enough space in your inventory.`);\n                                return;\n                            }\n\n                            if (!item) {\n                                logger.warn(`Could not find item with id ${itemIds.coins} in player inventory. [Karim plugin]`);\n                                return;\n                            }\n\n                            inventory.remove(index);\n                            if (item.amount !== 1) {\n                                inventory.add({ itemId: itemIds.coins, amount: item.amount - 1 });\n                            }\n\n                            inventory.add({ itemId: itemIds.kebab, amount: 1 });\n                            player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory);\n                            return;\n                        }\n\n                        if (!inventory.has(itemIds.coins)) {\n                            dialogue(\n                                [player, { npc, key: 'karim' }],\n                                [\n                                    player => [Emote.ANGRY, `Oops, I forgot to bring any money with me.`],\n                                    karim => [Emote.GENERIC, `Come back when you have some.`],\n                                ],\n                            );\n                        }\n                    }),\n                ],\n            ],\n        ],\n    );\n};\n\nexport default {\n    pluginId: 'rs:karim',\n    hooks: [{ type: 'npc_interaction', npcs: 'rs:alkharid_karim', options: 'talk-to', walkTo: true, handler: talkToAction }],\n};\n"
  },
  {
    "path": "src/plugins/npcs/al-kharid/louie-armoured-legs.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\n\nconst tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:louies_armored_legs')?.open(player);\n\nexport default {\n    pluginId: 'rs:louie_armored_legs',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:alkharid_louie',\n            options: 'trade',\n            walkTo: true,\n            handler: tradeAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/al-kharid/ranael-super-skirt.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\n\nconst tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:ranaels_skirt_store')?.open(player);\n\nexport default {\n    pluginId: 'rs:ranael_super_skirt',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:alkharid_ranael',\n            walkTo: true,\n            options: 'trade',\n            handler: tradeAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/falador/custom-guards.plugin.ts",
    "content": "import type { npcInitActionHandler } from '@engine/action/pipe/npc-init.action';\nimport { findNpc } from '@engine/config/config-handler';\nimport { randomBetween } from '@engine/util/num';\nimport { activeWorld } from '@engine/world';\nimport type { Npc } from '@engine/world/actor/npc';\nimport { Position } from '@engine/world/position';\nimport { World } from '@engine/world/world';\n\nconst npcs = ['rs:guard:0', 'rs:guard:1'];\nconst npcObjects = npcs.map(sNpc => findNpc(sNpc));\n\ninterface DialogueNpcTree {\n    a?: string;\n    b?: string;\n    a_anim?: number;\n    b_anim?: number;\n}\n\nconst dialogueTrees: DialogueNpcTree[][] = [\n    [\n        {\n            a: 'Hello sir!',\n            a_anim: 863,\n        },\n        {\n            b: 'Hello solider.',\n            b_anim: 863,\n        },\n        {\n            a: 'What is my purpose?',\n        },\n        {\n            b: 'You get pickpocketed and murdered for scrolls with clues.',\n        },\n        {\n            a_anim: 837,\n            a: 'Oh my god.',\n        },\n        {\n            b: 'Yeah welcome to the club pal.',\n            b_anim: 2113,\n        },\n    ],\n    [\n        {\n            a: 'Um... So, Sir',\n        },\n        {\n            a_anim: 2836,\n            a: 'That enhancment treatment we discussed...?',\n        },\n        {\n            b: `Well... It's kind of weird to be saying this, given the nature of my work,`,\n        },\n        {\n            b: `but I really think you should think twice about this enhancement therapy,`,\n        },\n        {\n            b: `It's all still pretty experimental.`,\n        },\n        {\n            a: `I've made up my mind about it, Sir. I gotta go big. Real big.`,\n        },\n        {\n            b: `You're young, and I have been around the block,`,\n        },\n        {\n            b: `so let me give a bit of advice for free: Size isn't everything.`,\n        },\n        {\n            a: `In my world it is. All the guys are getting it done.`,\n        },\n        {\n            b: `Really? I didn't know that. And they are not having any problems?`,\n        },\n        {\n            b: `I mean... performing their duties so to speak.`,\n        },\n        {\n            b_anim: 14,\n            a: `On the contrary, Sir. They are real beasts, all of them.`,\n        },\n        {\n            a: `Always at it, day and night. Really competing to see who's the biggest.`,\n        },\n        {\n            b_anim: 404,\n            b: `Wow. I mean... I don't judge, of course.`,\n        },\n        {\n            b: `I mean, this one time in military school. I mean, I was pretty drunk, of course...`,\n        },\n        {\n            a: `You're losing me, Sir. But can you get me those steroids or not?`,\n        },\n        {\n            a: `I don't wanna be the smallest guy in the room.`,\n        },\n        {\n            b_anim: 404,\n            b: `Steroids? Oh, steroids! I thought you were talking about something else...`,\n        },\n        {\n            a: `Losing me again, Sir. What did you think I was talking about?`,\n        },\n        {\n            b: `Oh, nothing... I'll get you the damn steroids, don't worry.`,\n        },\n        {\n            a: `I mean, I thought I was pretty clear but... `,\n        },\n        {\n            a: `Well you seem pretty fixated on guys, though. But you know,`,\n        },\n        {\n            a: `that's just fine. This is a medieval world we live in, and as far as I'm concerned,`,\n        },\n        {\n            a: `who you love is entirely up to you.`,\n        },\n        {\n            b_anim: 856,\n            b: `Shut up!`,\n        },\n    ],\n];\n\nfunction startIdleDialogueTree(npc: Npc, closeNpc: Npc, dialogueTree: DialogueNpcTree[]) {\n    if (npc.busy || closeNpc.busy) {\n        return;\n    }\n    npc.busy = true;\n    closeNpc.busy = true;\n    npc.face(closeNpc);\n    closeNpc.face(npc);\n    setTimeout(() => doDialogue(npc, closeNpc, 0, dialogueTree), 3 * World.TICK_LENGTH);\n}\nfunction doDialogue(a: Npc, b: Npc, dialogueIndex: number, dialogueTree: DialogueNpcTree[]) {\n    a.stopAnimation();\n    b.stopAnimation();\n    if (dialogueIndex > dialogueTree.length - 1 || !a.exists || !b.exists) {\n        a.busy = false;\n        b.busy = false;\n        a.clearFaceActor();\n        b.clearFaceActor();\n        return;\n    }\n    const currentDialogue = dialogueTree[dialogueIndex];\n    if (currentDialogue.a) {\n        a.say(currentDialogue.a);\n    }\n    if (currentDialogue.b) {\n        b.say(currentDialogue.b);\n    }\n    if (currentDialogue.a_anim) {\n        a.playAnimation(currentDialogue.a_anim);\n    }\n    if (currentDialogue.b_anim) {\n        b.playAnimation(currentDialogue.b_anim);\n    }\n    setTimeout(() => doDialogue(a, b, dialogueIndex + 1, dialogueTree), 5 * World.TICK_LENGTH);\n}\n\nconst npcIdleAction = (npc: Npc) => {\n    if (Math.random() >= 0.14) {\n        const currentLocation = new Position(npc.position);\n        const closeNpcs = activeWorld.findNearbyNpcs(currentLocation, 4);\n        for (const closeNpc of closeNpcs) {\n            if (closeNpc === npc) {\n                continue;\n            }\n            if (npcObjects.find(oNpc => oNpc.gameId === closeNpc.id)) {\n                if (closeNpc.busy) {\n                    continue;\n                }\n                startIdleDialogueTree(npc, closeNpc, dialogueTrees[randomBetween(0, dialogueTrees.length - 1)]);\n                return;\n            }\n        }\n    }\n};\n\nconst guardInitAction: npcInitActionHandler = ({ npc }) => {\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    // setInterval(() => npcIdleAction(npc), (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'promises:custom_guards',\n    hooks: [\n        {\n            type: 'npc_init',\n            npcs: npcs,\n            handler: guardInitAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/lumbridge/bob.plugin.ts",
    "content": "import type { NpcInteractionActionHook } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\nimport type { ContentPlugin } from '@engine/plugins/content-plugin';\n\nconst bobHook: NpcInteractionActionHook = {\n    type: 'npc_interaction',\n    npcs: 'rs:lumbridge_bob',\n    options: 'trade',\n    walkTo: true,\n    handler: ({ player }) => findShop('rs:lumbridge_bobs_axes')?.open(player),\n};\n\nconst bobPlugin: ContentPlugin = {\n    pluginId: 'rs:bob',\n    hooks: [bobHook],\n};\n\nexport default bobPlugin;\n"
  },
  {
    "path": "src/plugins/npcs/lumbridge/hans.plugin.ts",
    "content": "import type { NpcInteractionActionHook } from '@engine/action/pipe/npc-interaction.action';\nimport { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue';\nimport { Achievements, giveAchievement } from '@engine/world/actor/player/achievements';\nimport { animationIds } from '@engine/world/config/animation-ids';\n\nconst handler = async ({ player, npc }) => {\n    let sadEnding = false;\n\n    const dialogueParticipants = [player, { npc, key: 'hans' }];\n\n    const dialogueTree = [\n        hans => [Emote.GENERIC, `Welcome to RuneJS!`],\n        (hans, tag_Hans_Question) => [\n            Emote.HAPPY,\n            `How do you feel about RuneJS so far?\\n` + `Please take a moment to let us know what you think!`,\n        ],\n        options => [\n            `Love it!`,\n            [\n                player => [Emote.HAPPY, `Loving it so far, thanks for asking!`],\n                hans => [Emote.HAPPY, `You're very welcome! Glad to hear it.`],\n            ],\n            `Kind of cool.`,\n            [\n                player => [Emote.GENERIC, `It's kind of cool, I guess. Bit of a weird gimmick.`],\n                hans => [Emote.HAPPY, `Please let us know if you have any suggestions.`],\n            ],\n            `Not my cup of tea, honestly.`,\n            [player => [Emote.SKEPTICAL, `Not really my cup of tea, but keep at it.`], hans => [Emote.GENERIC, `Thanks for the support!`]],\n            `It's literally the worst.`,\n            [\n                player => [Emote.ANGRY, `Literally the worst thing I've ever seen. You disgust me on a personal level.`],\n                hans => [Emote.SAD, `I-is that so?... Well I'm... I'm sorry to hear that.`],\n                execute(() => (sadEnding = true)),\n            ],\n            `What?`,\n            [player => [Emote.DROWZY, `What?...`], goto('tag_Hans_Question')],\n        ],\n    ];\n\n    const dialogueSuccessful = await dialogue(dialogueParticipants, dialogueTree);\n\n    npc.clearFaceActor();\n    player.clearFaceActor();\n\n    if (dialogueSuccessful) {\n        if (sadEnding) {\n            // @todo These appear to be broken, debug\n            // npc.playAnimation(animationIds.cry);\n            // npc.say(`Jerk!`);\n            player.sendMessage(`Hans wanders off rather dejectedly.`);\n        } else {\n            player.sendMessage(`Hans wanders off aimlessly through the courtyard.`);\n        }\n\n        giveAchievement(Achievements.WELCOME, player);\n    }\n};\n\nexport default {\n    pluginId: 'rs:hans',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:hans',\n            options: 'talk-to',\n            walkTo: true,\n            handler,\n        } as NpcInteractionActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/lumbridge/lumbridge-farm-helpers.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { Emote, dialogue, goto } from '@engine/world/actor/dialogue';\n\nconst millieDialogue: npcInteractionActionHandler = async details =>\n    dialogue(\n        [details.player, { npc: details.npc, key: 'millie' }],\n        [\n            millie => [Emote.GENERIC, `Hello Adventurer. Welcome to Mill Lane Mill. Can I help you?`],\n            options => [\n                `Who are you?`,\n                [\n                    player => [Emote.WONDERING, `Who are you?`],\n                    millie => [\n                        Emote.HAPPY,\n                        `I'm Miss Millicent Miller the Miller of Mill Lane Mill. ` + `Our family have been milling flour for generations.`,\n                    ],\n                    player => [Emote.GENERIC, `It's a good business to be in. People will always need flour.`],\n                    goto('tag_Mill_Flour'),\n                ],\n                `What is this place?`,\n                [\n                    player => [Emote.WONDERING, `What is this place?`],\n                    millie => [\n                        Emote.HAPPY,\n                        `This is Mill Lane Mill. Millers of the finest flour in Gielinor, ` +\n                            `and home to the Miller family for many generations.`,\n                    ],\n                    millie => [Emote.GENERIC, `We take grain from the field nearby and mill into flour.`],\n                    goto('tag_Mill_Flour'),\n                ],\n                `How do I mill flour?`,\n                [\n                    (player, tag_Mill_Flour) => [Emote.WONDERING, `How do I mill flour?`],\n                    millie => [\n                        Emote.GENERIC,\n                        `Making flour is pretty easy. First of all you need to get some grain. ` +\n                            `You can pick some from wheat fields. There is one just outside the Mill, but there are ` +\n                            `many others scattered across Gielinor.`,\n                    ],\n                    millie => [\n                        Emote.GENERIC,\n                        `Feel free to pick wheat from our field! There always seems to be plenty ` + `of wheat there.`,\n                    ],\n                    player => [Emote.WONDERING, `Then I bring my wheat here?`],\n                    millie => [\n                        Emote.GENERIC,\n                        `Yes, or one of the other mills in Gielinor. They all work the same way. ` +\n                            `Just take your grain to the top floor of the mill (up two ladders, there are three floors ` +\n                            `including this one) and then place some`,\n                    ],\n                    millie => [\n                        Emote.GENERIC,\n                        `grain into the hopper. Then you need to start the grinding process by ` +\n                            `pulling the hopper lever. You can add more grain, but each time you add grain you have to ` +\n                            `pull the hopper lever again.`,\n                    ],\n                    player => [Emote.WONDERING, `So where does the flour go then?`],\n                    millie => [\n                        Emote.GENERIC,\n                        `The flour appears in this room here, you'll need a pot to put the flour ` +\n                            `into. One pot will hold the flour made by one load of grain`,\n                    ],\n                    millie => [\n                        Emote.GENERIC,\n                        `And that's it! You now have some pots of finely ground flour of the ` +\n                            `highest quality. Ideal for making tasty cakes or delicous bread. I'm not a cook so you'll ` +\n                            `have to ask a cook to find`,\n                    ],\n                    millie => [Emote.GENERIC, `out how to bake things.`],\n                    player => [Emote.HAPPY, `Great! Thanks for your help.`],\n                ],\n                `I'm fine, thanks.`,\n                [player => [Emote.GENERIC, `I'm fine, thanks.`]],\n            ],\n        ],\n    );\n\nconst gillieDialogue: npcInteractionActionHandler = async details =>\n    dialogue(\n        [details.player, { npc: details.npc, key: 'gillie' }],\n        [\n            gillie => [Emote.HAPPY, `Hello, I'm Gillie the Milkmaid. What can I do for you?`],\n            options => [\n                `Who are you?`,\n                [\n                    player => [Emote.WONDERING, `Who are you?`],\n                    gillie => [Emote.GENERIC, `My name is Gillie Groats. My father is a farmer and I milk the cows for him.`],\n                    player => [Emote.WONDERING, `Do you have nay buckets of milk spare?`],\n                    gillie => [\n                        Emote.GENERIC,\n                        `I'm afraid not. We need all of our milk to sell to market, ` +\n                            `but you can milk the cow yourself if you need the milk.`,\n                    ],\n                    player => [Emote.GENERIC, `Thanks.`],\n                ],\n                `So how do you milk a cow then?`,\n                [\n                    player => [Emote.WONDERING, `So how do you milk a cow then?`],\n                    gillie => [Emote.HAPPY, `It's very easy. First you need an empty bucket to hold the milk.`],\n                    gillie => [Emote.HAPPY, `Then find a dairy cow to milk - you can't milk just any cow.`],\n                    player => [Emote.SKEPTICAL, `How do I find a dairy cow?`],\n                    gillie => [\n                        Emote.GENERIC,\n                        `They are easy to spot - they are dark brown and white, unlike ` +\n                            `beef cows, which are light brown and white. We also tether them to a post to stop them ` +\n                            `wandering around all over the place.`,\n                    ],\n                    gillie => [Emote.GENERIC, `There are a couple very near, in this field.`],\n                    gillie => [Emote.GENERIC, `Then just milk the cow and your bucket will fill with tasty, untritious milk.`],\n                ],\n                `I'm fine, thanks.`,\n                [player => [Emote.GENERIC, `I'm fine, thanks.`]],\n            ],\n        ],\n    );\n\nexport default {\n    pluginId: 'rs:lumbridge_farm_helpers',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:gillie_groats',\n            options: 'talk-to',\n            walkTo: true,\n            handler: gillieDialogue,\n        },\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:millie_miller',\n            options: 'talk-to',\n            walkTo: true,\n            handler: millieDialogue,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/lumbridge/shopkeeper.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\n\nconst action: npcInteractionActionHandler = ({ player }) => {\n    findShop('rs:lumbridge_general_store')?.open(player);\n};\nexport default {\n    pluginId: 'rs:lumbridge_general_store',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:lumbridge_shop_keeper',\n            options: 'trade',\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/port-sarim/betty.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\n\nconst shopAction: npcInteractionActionHandler = details => findShop('rs:bettys_magic_emporium')?.open(details.player);\n\nconst dialogueAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    let openShop = false;\n    dialogue(\n        [details.player, { npc: details.npc, key: 'betty' }],\n        [\n            betty => [Emote.HAPPY, `Welcome to the magic emporium.`],\n            options => [\n                `Can I see your wares?`,\n                [\n                    player => [Emote.HAPPY, `Can I see your wares?`],\n                    execute(() => {\n                        openShop = true;\n                    }),\n                ],\n                `Sorry I'm not into magic.`,\n                [\n                    player => [Emote.GENERIC, `Sorry I'm not into magic.`],\n                    betty => [Emote.HAPPY, `Well, if you see anyone who is into magic, please send them my way.`],\n                ],\n            ],\n        ],\n    );\n\n    if (openShop) {\n        shopAction(details);\n    }\n};\n\nexport default {\n    pluginId: 'rs:betty_shop',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:betty',\n            options: 'trade',\n            walkTo: true,\n            handler: shopAction,\n        },\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:betty',\n            options: 'talk-to',\n            walkTo: true,\n            handler: dialogueAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/varrock/blue-moon-inn.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nconst talkToBartender: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    dialogue(\n        [player, { npc, key: 'bartender' }],\n        [\n            bartender => [Emote.HAPPY, 'What can I do yer for?'],\n            options => [\n                `A glass of your finest ale please.`,\n                [\n                    player => [Emote.HAPPY, `A glass of your finest ale please.`],\n                    bartender => [Emote.HAPPY, `No problemo. That'll be 2 coins.`],\n                    execute(() => {\n                        const index = player.inventory.findIndex(itemIds.coins);\n                        const hasCoins = player.inventory.has(itemIds.coins);\n\n                        if (!hasCoins) {\n                            dialogue(\n                                [player, { npc, key: 'bartender' }],\n                                [player => [Emote.VERY_SAD, `Oh dear. I don't seem to have enough money.`]],\n                            );\n                        }\n\n                        if (hasCoins && player.inventory.amountInStack(index) >= 2) {\n                            const amount = player.inventory.amountInStack(index);\n                            // Check inventory.\n                            if (!player.inventory.hasSpace()) {\n                                player.sendMessage(`You don't have enough space in your inventory.`);\n                                return;\n                            }\n\n                            // Take the coins\n                            player.inventory.remove(index);\n                            if (amount - 2 !== 0) {\n                                player.inventory.add({\n                                    itemId: itemIds.coins,\n                                    amount: amount - 2,\n                                });\n                            }\n\n                            // Give the beer.\n                            player.inventory.add(itemIds.beer);\n                            player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n                        }\n                    }),\n                ],\n                `Can you recommend where an adventurer might make his fortune?`,\n                [\n                    player => [Emote.WONDERING, `Can you recommend where an adventurer might make his fortune?`],\n                    bartender => [Emote.LAUGH, `Ooh I don't know if I should be giving away information, makes the game too easy.`],\n                    options => [\n                        `Oh ah well...`,\n                        [player => [Emote.WORRIED, `Oh ah well...`]],\n                        `Game? What are you talking about?`,\n                        [\n                            player => [Emote.WORRIED, `Game? What are you talking about?`],\n                            bartender => [Emote.GENERIC, `This world around us... is an online game... called Old School RuneScape.`],\n                            player => [Emote.GENERIC, `Nope, still don't understand what you are talking about. What does 'online' mean?`],\n                            bartender => [\n                                Emote.GENERIC,\n                                `It's a sort of connection between magic boxes across the world, big boxes on people's desktops and little ones people can carry. They can talk to each other to play games.`,\n                            ],\n                            player => [Emote.GENERIC, `I give up. You're obviously completely mad!`],\n                        ],\n                        `Just a small clue?`,\n                        [\n                            player => [Emote.WONDERING, `Just a small clue?`],\n                            bartender => [\n                                Emote.VERY_SAD,\n                                `Go and talk to the bartender at the Jolly Boar Inn, he doesn't seem to mind giving away clues.`,\n                            ],\n                        ],\n                    ],\n                ],\n                `Do you know where I can get some good equipment?`,\n                [\n                    player => [Emote.WONDERING, `Do you know where I can get some good equipment?`],\n                    bartender => [\n                        Emote.HAPPY,\n                        `Well, there's the sword shop across the road, or there's also all sorts of shops up around the market.`,\n                    ],\n                ],\n            ],\n        ],\n    );\n};\n\nconst talkToCook: npcInteractionActionHandler = details => {\n    const { npc, player } = details;\n\n    dialogue(\n        [player, { npc, key: 'cook' }],\n        [\n            cook => [Emote.ANGRY, `What do you want? I'm busy!`],\n            options => [\n                `Can you sell me any food?`,\n                [\n                    player => [Emote.WONDERING, `Can you sell me any food?`],\n                    cook => [\n                        Emote.GENERIC,\n                        `I suppose I could sell you some cabbage, if you're willing to pay for it. Cabbage is good for you.`,\n                    ],\n                    execute(() => {\n                        const hasCoins = player.inventory.has(itemIds.coins);\n                        const index = player.inventory.findIndex(itemIds.coins);\n\n                        // The player doesn't have any coins.\n                        if (!hasCoins) {\n                            dialogue(\n                                [player, { npc, key: 'cook' }],\n                                [\n                                    player => [Emote.VERY_SAD, `Oh, I haven't got any money.`],\n                                    cook => [Emote.ANGRY, `Why are you asking me to sell you food if you haven't got any money? Go away!`],\n                                ],\n                            );\n                        }\n\n                        // The player has enough coins\n                        if (hasCoins && player.inventory.amountInStack(index) >= 2) {\n                            const amount = player.inventory.amountInStack(index);\n                            dialogue(\n                                [player, { npc, key: 'cook' }],\n                                [\n                                    options => [\n                                        `Alright I'll buy a cabbage.`,\n                                        [\n                                            player => [Emote.HAPPY, `Alright I'll buy a cabbage.`],\n                                            execute(() => {\n                                                // Check inventory.\n                                                if (!player.inventory.hasSpace()) {\n                                                    player.sendMessage(`You don't have enough space in your inventory.`);\n                                                    return;\n                                                }\n\n                                                // Take the coins\n                                                player.inventory.remove(index);\n                                                if (amount - 2 !== 0) {\n                                                    player.inventory.add({\n                                                        itemId: itemIds.coins,\n                                                        amount: amount - 2,\n                                                    });\n                                                }\n\n                                                // Give the cabbage.\n                                                player.inventory.add(itemIds.cabbage);\n                                                player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n                                            }),\n                                            cook => [\n                                                Emote.HAPPY,\n                                                `It's a deal. Now, make sure you eat it all up. Cabbage is good for you.`,\n                                            ],\n                                        ],\n                                        `No thanks, I don't like cabbage.`,\n                                        [\n                                            player => [Emote.GENERIC, `No thanks, I don't like cabbage.`],\n                                            cook => [Emote.SAD, `Bah! People these days only appreciate junk food.`],\n                                        ],\n                                    ],\n                                ],\n                            );\n                        }\n                    }),\n                ],\n                `Can you give any free food?`,\n                [\n                    // PLAYER: Can you give any free food?\n                    player => [Emote.GENERIC, `Can you give any free food?`],\n                    cook => [Emote.GENERIC, `Can you give my any free money?`],\n                    player => [Emote.GENERIC, `Why should I give you free money?`],\n                    cook => [Emote.GENERIC, `Why should I give you free food?`],\n                    player => [Emote.GENERIC, `Oh, forget it.`],\n                ],\n                `I don't want anything from this horrible kitchen.`,\n                [\n                    player => [Emote.SHOCKED, `I don't want anything from this horrible kitchen.`],\n                    cook => [Emote.ANGRY, `How dare you? I put a lot of effort into cleaning this kitchen.`],\n                    cook => [Emote.ANGRY, `My daily sweat and elbow-grease keep this kitchen clean!`],\n                    player => [Emote.GENERIC, `Ewww!`],\n                    cook => [Emote.SAD, `Oh, just leave me alone.`],\n                ],\n            ],\n        ],\n    );\n};\n\nexport default {\n    pluginId: 'rs:blue_moon_inn_dialogue',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:blue_moon_innk_bartender',\n            handler: talkToBartender,\n        },\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:blue_moon_inn_cook',\n            handler: talkToCook,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/varrock/master-smithing-tutor.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nconst talkTo: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    dialogue(\n        [player, { npc, key: 'tutor' }],\n        [\n            player => [Emote.GENERIC, `Hello.`],\n            tutor => [Emote.GENERIC, `Well met! Are you interested in hearing about the art of smithing?`],\n            options => [\n                `How can I train my smithing?`,\n                [\n                    (player, tag_how_to_train) => [Emote.WONDERING, `How can I train my smithing?`],\n                    tutor => [Emote.GENERIC, `To be able to smith anything, you're going to need one of these beauties.`],\n                    execute(() => {\n                        player.inventory.add(itemIds.hammer);\n                        player.sendMessage('The Master Smithing Tutor gives you a hammer.', true);\n                        player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n                    }),\n                    tutor => [Emote.GENERIC, `You're going to get your hand on some metal bars.`],\n                    tutor => [Emote.GENERIC, `You could do this by mining your own ores and smelting them at a furnace.`],\n                    tutor => [Emote.GENERIC, `There is a furnace in Lumbridge, just north of the castle opposite the general store.`],\n                    tutor => [Emote.GENERIC, `If you are looking for some ore, there is a mine east of Varrock.`],\n                    tutor => [Emote.GENERIC, `There you can find some copper and tin ore. Don't forget to bring a pickaxe.`],\n                    tutor => [Emote.GENERIC, `When you have your bars, bring them to an anvil to open the smithing interface.`],\n                    tutor => [Emote.GENERIC, `If the item name is in black, this means you do not have the level to smith the item.`],\n                    tutor => [Emote.GENERIC, `If the name is in white, this means you have the level to smith the item.`],\n                    tutor => [Emote.GENERIC, `You will see the bars required to smith the item underneath the name of the item.`],\n                    tutor => [\n                        Emote.GENERIC,\n                        `If the number of bars is in orange, this means that you do not have enough bars to smith the item.`,\n                    ],\n                    tutor => [Emote.GENERIC, `If it is in green, this means you have enough bars for the item.`],\n                    player => [Emote.GENERIC, `Thanks for the advice.`],\n                    options => [\n                        `What kinds of things can I smith?`,\n                        [goto('tag_what_kinds')],\n                        `Not right now, thank you.`,\n                        [goto('tag_no_thanks')],\n                    ],\n                ],\n                `What kinds of things can I smith?`,\n                [\n                    (player, tag_what_kinds) => [Emote.WONDERING, `What kinds of things can I smith?`],\n                    tutor => [Emote.GENERIC, `There are many things you can make, from weapons to your good old fashioned armour.`],\n                    tutor => [\n                        Emote.GENERIC,\n                        `Weapons are the cheapest things to smith. They range from a measly one bar, all the way to three bars.`,\n                    ],\n                    tutor => [\n                        Emote.GENERIC,\n                        `Armour can be the costliest item to smith, the cost of each item ranges from a measly one bar all the way up to a whopping five bars.`,\n                    ],\n                    tutor => [\n                        Emote.GENERIC,\n                        `Some weapons and armours, such as darts, will require you to have gained knowledge on how to smith them.`,\n                    ],\n                    tutor => [Emote.GENERIC, `This is due to the complex nature of the weapon.`],\n                    tutor => [Emote.GENERIC, `You might find other items don't require conventional bars you would gather.`],\n                    tutor => [\n                        Emote.GENERIC,\n                        `Some may require you to piece back together or even infuse a crystal into a piece of armour.`,\n                    ],\n                    tutor => [Emote.GENERIC, `Is there anything else you want to know?`],\n                    options => [`How can i train my smithing?`, [goto('tag_how_to_train')], `No, thank you.`, [goto('tag_no_thanks')]],\n                ],\n                `Not right now, thank you.`,\n                [\n                    (player, tag_no_thanks) => [Emote.GENERIC, `Not right now, thank you.`],\n                    tutor => [Emote.GENERIC, `Well, just come back any time you want to know anything!`],\n                ],\n            ],\n        ],\n    );\n};\n\nexport default {\n    pluginId: 'rs:master_smithing_tutor',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:master_smithing_tutor',\n            options: ['talk-to'],\n            walkTo: true,\n            handler: talkTo,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/varrock/wilough.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findNpc } from '@engine/config/config-handler';\nimport { Emote, dialogue } from '@engine/world/actor/dialogue';\n\nconst talkTo: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    const shilop = findNpc('rs:varrock_shilop');\n\n    dialogue(\n        [player, { npc, key: 'wilough' }, { npc: shilop.gameId, key: 'shilop' }],\n        [\n            player => [Emote.GENERIC, `Hello again.`],\n            wilough => [Emote.GENERIC, `You think you're tough do you?`],\n            player => [Emote.GENERIC, `Pardon?`],\n            wilough => [Emote.ANGRY, `I can beat anyone up!`],\n            shilop => [Emote.BLANK_STARE, `He can you know!`],\n            player => [Emote.BLANK_STARE, `Really?`],\n        ],\n    );\n};\n\nexport default {\n    pluginId: 'rs:varrock_wilough_dialogue',\n    hooks: [\n        {\n            npcs: 'rs:varrock_wilough',\n            type: 'npc_interaction',\n            options: ['talk-to'],\n            walkTo: true,\n            handler: talkTo,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/npcs/varrock/zaff-superior-staffs.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport { findShop } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\n\nconst tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:zaffs_superior_staffs')?.open(player);\n\nconst talkToAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    dialogue(\n        [player, { npc, key: 'zaff' }],\n        [\n            zaff => [Emote.GENERIC, `Would you like to buy or sell some staffs?`],\n            options => [\n                `Yes, please!`,\n                [\n                    execute(() => {\n                        tradeAction(details);\n                    }),\n                ],\n\n                'Have you any extra stock of battlestaffs I can buy?',\n                [\n                    player => [Emote.GENERIC, 'Have you any extra stock of battlestaffs I can buy?'],\n                    zaff => [Emote.WONDERING, \"No, I'm afraid I can't help you.\"],\n                    execute(() => {\n                        player.sendMessage(\n                            \"You must complete the Varrock Achievement Diary before you can access Zaff's extra battlestaff stock.\",\n                        );\n                    }),\n                ],\n\n                'No, thank you.',\n                [\n                    player => [Emote.GENERIC, 'No, thank you.'],\n                    zaff => [Emote.GENERIC, \"Well 'stick' your head in if you change your mind.\"],\n                    player => [Emote.GENERIC, \"Huh, terrible pun! You just can't get the 'staff' these days!\"],\n                ],\n            ],\n        ],\n    );\n};\n\nexport default {\n    pluginId: 'rs:zaffs_staffs',\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:varrock_zaff',\n            options: 'trade',\n            walkTo: true,\n            handler: tradeAction,\n        },\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:varrock_zaff',\n            options: 'talk-to',\n            walkTo: true,\n            handler: talkToAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/bank/bank.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { widgetScripts } from '@engine/world/config/widget';\nimport type { Item } from '@engine/world/items/item';\nimport { fromNote, toNote } from '@engine/world/items/item';\nimport type { ItemContainer } from '@engine/world/items/item-container';\nimport { logger } from '@runejs/common';\n\nconst buttonIds: number[] = [\n    92, // as note\n    93, // as item\n    98, // swap\n    99, // insert\n];\n\nexport const openBankInterface: objectInteractionActionHandler = ({ player }) => {\n    player.interfaceState.openWidget(widgets.bank.screenWidget.widgetId, {\n        slot: 'screen',\n        multi: true,\n    });\n    player.interfaceState.openWidget(widgets.bank.tabWidget.widgetId, {\n        slot: 'tabarea',\n        multi: true,\n    });\n\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.tabWidget, player.inventory);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.screenWidget, player.bank);\n    player.outgoingPackets.updateClientConfig(widgetScripts.bankInsertMode, player.settings.bankInsertMode);\n    player.outgoingPackets.updateClientConfig(widgetScripts.bankWithdrawNoteMode, player.settings.bankWithdrawNoteMode);\n};\n\nexport const openPinSettings: objectInteractionActionHandler = ({ player }) => {\n    player.interfaceState.openWidget(widgets.bank.pinSettingsWidget.widgetId, {\n        slot: 'screen',\n    });\n};\n\nexport const depositItem: itemInteractionActionHandler = details => {\n    // Check if player might be spawning widget client-side\n    if (!details.player.interfaceState.findWidget(widgets.bank.screenWidget.widgetId)) {\n        return;\n    }\n\n    // Check if the player has the item\n    if (!details.player.hasItemInInventory(details.itemId)) {\n        return;\n    }\n\n    let itemIdToAdd: number = details.itemId;\n    const fromNoteId: number = fromNote(details.itemId);\n    if (fromNoteId > -1) {\n        itemIdToAdd = fromNoteId;\n    }\n\n    let countToRemove: number;\n    switch (details.option) {\n        case 'deposit-1':\n            // Deposit 1\n            countToRemove = 1;\n            break;\n        case 'deposit-5':\n            // Deposit 5\n            countToRemove = 5;\n            break;\n        case 'deposit-10':\n            // Deposit 10\n            countToRemove = 10;\n            break;\n        case 'deposit-all':\n            // Deposit all\n            countToRemove = -1;\n            break;\n        default:\n            // Should never happen\n            throw new Error('Unhandled option in banking plugin: ' + details.option);\n    }\n\n    const playerInventory = details.player.inventory;\n    const playerBank = details.player.bank;\n    const slotsWithItem = playerInventory.findAll(details.itemId);\n\n    let itemAmount = 0;\n    slotsWithItem.forEach(slot => {\n        const item = playerInventory.items[slot];\n\n        if (!item) {\n            throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        if (item.itemId !== details.itemId) {\n            throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        itemAmount += item.amount;\n    });\n    if (countToRemove === -1 || countToRemove > itemAmount) {\n        countToRemove = itemAmount;\n    }\n\n    if (!playerBank.canFit({ itemId: itemIdToAdd, amount: countToRemove }, true)) {\n        details.player.sendMessage('Your bank is full.');\n        return;\n    }\n\n    const itemToAdd: Item = {\n        itemId: itemIdToAdd,\n        amount: removeFromContainer(playerInventory, details.itemId, countToRemove),\n    };\n\n    playerBank.addStacking(itemToAdd);\n    updateBankingInterface(details.player);\n};\n\nexport const withdrawItem: itemInteractionActionHandler = details => {\n    // Check if player might be spawning widget client-side\n    if (!details.player.interfaceState.findWidget(widgets.bank.screenWidget.widgetId)) {\n        return;\n    }\n    // Check if the player has the item\n    if (!details.player.hasItemInBank(details.itemId)) {\n        return;\n    }\n\n    let itemIdToAdd: number = details.itemId;\n    let stackable: boolean = details.itemDetails.stackable;\n    if (details.player.settings.bankWithdrawNoteMode) {\n        const toNoteId: number = toNote(details.itemId);\n        if (toNoteId > -1) {\n            itemIdToAdd = toNoteId;\n            stackable = true;\n        } else {\n            details.player.sendMessage('This item can not be withdrawn as a note.');\n        }\n    }\n\n    let countToRemove: number;\n    switch (details.option) {\n        case 'withdraw-1':\n            // Withdraw 1\n            countToRemove = 1;\n            break;\n        case 'withdraw-5':\n            // Withdraw 5\n            countToRemove = 5;\n            break;\n        case 'withdraw-10':\n            // Withdraw 10\n            countToRemove = 10;\n            break;\n        case 'withdraw-all':\n            // Withdraw all\n            countToRemove = -1;\n            break;\n        default:\n            // Should never happen\n            throw new Error('Unhandled option in banking plugin: ' + details.option);\n    }\n\n    const playerBank = details.player.bank;\n    const playerInventory = details.player.inventory;\n    const slotWithItem = playerBank.findIndex(details.itemId);\n    const itemInBank = playerBank.items[slotWithItem];\n\n    if (!itemInBank) {\n        logger.error(`Container item was not present, for item id ${details.itemId} in bank, while trying to withdraw`);\n        return;\n    }\n\n    const itemAmount = itemInBank.amount;\n    if (countToRemove === -1 || countToRemove > itemAmount) {\n        countToRemove = itemAmount;\n    }\n\n    if (!stackable) {\n        const slots = playerInventory.getOpenSlotCount();\n        if (slots < countToRemove) {\n            countToRemove = slots;\n        }\n    }\n    if (!playerInventory.canFit({ itemId: itemIdToAdd, amount: countToRemove }) || countToRemove === 0) {\n        details.player.sendMessage('Your inventory is full.');\n        return;\n    }\n\n    const itemToAdd: Item = {\n        itemId: itemIdToAdd,\n        amount: removeFromContainer(playerBank, details.itemId, countToRemove),\n    };\n\n    if (stackable) {\n        playerInventory.add({ itemId: itemToAdd.itemId, amount: itemToAdd.amount });\n    } else {\n        for (let count = 0; count < itemToAdd.amount; count++) {\n            playerInventory.add({ itemId: itemToAdd.itemId, amount: 1 });\n        }\n    }\n\n    updateBankingInterface(details.player);\n};\n\nexport const updateBankingInterface = (player: Player) => {\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.tabWidget, player.inventory);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.screenWidget, player.bank);\n};\n\n/**\n * Removes an item from a container (e.g. bank or inventory) and returns the amount of items it removed.\n * @param from - The container to remove from\n * @param itemId - The item to remove\n * @param amount - The amount to remove\n * @returns The amount of items it removed\n */\nexport const removeFromContainer = (from: ItemContainer, itemId: number, amount: number) => {\n    let resultingAmount = 0;\n    let removeAmount = amount;\n\n    while (removeAmount > 0 && from.has(itemId)) {\n        const containerIndex = from.findIndex(itemId);\n        const containerItem = from.items[containerIndex];\n\n        if (!containerItem) {\n            throw new Error(`Container item was not present, for item id ${itemId} in bank, while trying to remove`);\n        }\n\n        if (removeAmount >= containerItem.amount) {\n            resultingAmount += containerItem.amount;\n            removeAmount -= containerItem.amount;\n            from.remove(containerIndex);\n        } else {\n            resultingAmount += removeAmount;\n            containerItem.amount -= removeAmount;\n            removeAmount = 0;\n        }\n    }\n\n    return resultingAmount;\n};\n\nexport const btnAction: buttonActionHandler = details => {\n    const { player, buttonId } = details;\n    player.settingChanged(buttonId);\n\n    const settingsMappings = {\n        92: { setting: 'bankWithdrawNoteMode', value: 1 },\n        93: { setting: 'bankWithdrawNoteMode', value: 0 },\n        98: { setting: 'bankInsertMode', value: 0 },\n        99: { setting: 'bankInsertMode', value: 1 },\n    };\n    if (!settingsMappings[buttonId]) {\n        return;\n    }\n\n    const config = settingsMappings[buttonId];\n    player.settings[config.setting] = config.value;\n};\n\nconst useBankBoothAction: objectInteractionActionHandler = async details => {\n    const { player } = details;\n\n    let openBank = false;\n    let openPin = false;\n    await dialogue(\n        [player, { npc: 'rs:generic_banker', key: 'banker' }],\n        [\n            banker => [Emote.HAPPY, `Good day, how can I help you?`],\n            options => [\n                `I'd Like to access my bank account, please.`,\n                [\n                    execute(() => {\n                        openBank = true;\n                    }),\n                ],\n                `I'd like to check my PIN settings.`,\n                [\n                    execute(() => {\n                        openPin = true;\n                    }),\n                ],\n                `What is this place?`,\n                [\n                    player => [Emote.WONDERING, `What is this place?`],\n                    banker => [Emote.HAPPY, `This is a branch of the Bank of Gielinor. We have branches in many towns.`],\n                    player => [Emote.WONDERING, `And what do you do?`],\n                    banker => [Emote.GENERIC, `We will look after your items and money for you.`],\n                    banker => [Emote.GENERIC, `Leave your valuables with us if you want to keep them safe.`],\n                ],\n            ],\n        ],\n    );\n\n    if (openBank) {\n        openBankInterface(details as any);\n    } else if (openPin) {\n        openPinSettings(details);\n    }\n};\n\nexport default {\n    pluginId: 'rs:banking',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.bankBooth,\n            options: ['use'],\n            walkTo: true,\n            handler: useBankBoothAction,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.bankBooth,\n            options: ['use-quickly'],\n            walkTo: true,\n            handler: openBankInterface,\n        },\n        {\n            type: 'item_interaction',\n            widgets: widgets.bank.tabWidget,\n            options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'],\n            handler: depositItem,\n        },\n        {\n            type: 'item_interaction',\n            widgets: widgets.bank.screenWidget,\n            options: ['withdraw-1', 'withdraw-5', 'withdraw-10', 'withdraw-all'],\n            handler: withdrawItem,\n        },\n        {\n            type: 'button',\n            widgetId: widgets.bank.screenWidget.widgetId,\n            buttonIds: buttonIds,\n            handler: btnAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/bank/deposit-box.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport type { Item } from '@engine/world/items/item';\nimport { fromNote } from '@engine/world/items/item';\n\nexport const openDepositBoxInterface: objectInteractionActionHandler = ({ player }) => {\n    player.interfaceState.openWidget(widgets.bank.depositBoxWidget.widgetId, {\n        slot: 'screen',\n        multi: true,\n    });\n    player.interfaceState.openWidget(widgets.disabledTab, {\n        slot: 'tabarea',\n        multi: true,\n    });\n\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, player.inventory);\n};\n\nexport const depositItem: itemInteractionActionHandler = details => {\n    // Check if player might be spawning widget clientside\n    if (!details.player.interfaceState.findWidget(widgets.bank.depositBoxWidget.widgetId)) {\n        return;\n    }\n\n    // Check if the player has the item\n    if (!details.player.hasItemInInventory(details.itemId)) {\n        return;\n    }\n\n    // If the item is a noted item, we need to de-note it\n    let itemIdToAdd: number = details.itemId;\n    const fromNoteId: number = fromNote(details.itemId);\n    if (fromNoteId > -1) {\n        itemIdToAdd = fromNoteId;\n    }\n\n    let countToRemove: number;\n    if (details.option.endsWith('all')) {\n        countToRemove = -1;\n    } else {\n        countToRemove = +details.option.replace('deposit-', '');\n    }\n\n    const playerInventory = details.player.inventory;\n    const playerBank = details.player.bank;\n    const slotsWithItem = playerInventory.findAll(details.itemId);\n\n    let itemAmount: number = 0;\n    slotsWithItem.forEach(slot => {\n        const item = playerInventory.items[slot];\n\n        if (!item) {\n            throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        if (item.itemId !== details.itemId) {\n            throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`);\n        }\n\n        itemAmount += item.amount;\n    });\n    if (countToRemove == -1 || countToRemove > itemAmount) {\n        countToRemove = itemAmount;\n    }\n\n    if (!playerBank.canFit({ itemId: itemIdToAdd, amount: countToRemove }, true)) {\n        details.player.sendMessage('Your bank is full.');\n        return;\n    }\n\n    const itemToAdd: Item = { itemId: itemIdToAdd, amount: 0 };\n    while (countToRemove > 0 && playerInventory.has(details.itemId)) {\n        const invIndex = playerInventory.findIndex(details.itemId);\n        const invItem = playerInventory.items[invIndex];\n\n        if (!invItem) {\n            throw new Error(`Inventory item was not present, for item id ${details.itemId} in bank, while trying to deposit`);\n        }\n\n        if (countToRemove >= invItem.amount) {\n            itemToAdd.amount += invItem.amount;\n            countToRemove -= invItem.amount;\n            playerInventory.remove(invIndex);\n        } else {\n            itemToAdd.amount += countToRemove;\n            invItem.amount -= countToRemove;\n            countToRemove = 0;\n        }\n    }\n\n    playerBank.addStacking(itemToAdd);\n\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, details.player.inventory);\n};\n\nexport default {\n    pluginId: 'rs:bank_deposit_box',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.depositBox,\n            options: ['deposit'],\n            walkTo: true,\n            handler: openDepositBoxInterface,\n        },\n        {\n            type: 'item_interaction',\n            widgets: widgets.bank.depositBoxWidget,\n            options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'],\n            handler: depositItem,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/cows/cow.plugin.ts",
    "content": "import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem, findNpc } from '@engine/config/config-handler';\nimport { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { ObjectConfig } from '@runejs/filestore';\n\nfunction milkCow(details: { objectConfig: ObjectConfig; player: Player }): void {\n    const { player, objectConfig } = details;\n    const emptyBucketItem = findItem(itemIds.bucket);\n    // TODO: `findItem` should probably throw this error internally.\n    if (emptyBucketItem === null) {\n        throw new Error('Failed to milk cow as no item matching bucket was found.');\n    }\n\n    if (player.hasItemInInventory(itemIds.bucket)) {\n        player.playAnimation(animationIds.milkCow);\n        player.playSound(soundIds.milkCow, 7);\n        player.removeFirstItem(itemIds.bucket);\n        player.giveItem(itemIds.bucketOfMilk);\n        player.sendMessage(`You milk the ${objectConfig.name} and receive some milk.`);\n    } else {\n        const gilleGroats = findNpc('rs:gillie_groats');\n        const gillieId = gilleGroats.gameId;\n\n        dialogueAction(player)\n            .then(async d => d.npc(gillieId, DialogueEmote.LAUGH_1, [`Tee hee! You've never milked a cow before, have you?`]))\n            .then(async d => d.player(DialogueEmote.CALM_TALK_1, ['Erm... No. How could you tell?']))\n            .then(async d =>\n                d.npc(gillieId, DialogueEmote.LAUGH_2, [\n                    `Because you're spilling milk all over the floor. What a`,\n                    'waste! You need something to hold the milk.',\n                ]),\n            )\n            .then(async d => d.player(DialogueEmote.CONSIDERING, [`Ah yes, I really should have guessed that one, shouldn't`, 'I?']))\n            .then(async d =>\n                d.npc(gillieId, DialogueEmote.LAUGH_2, [\n                    `You're from the city aren't you... Try it again with a`,\n                    `${emptyBucketItem.name.toLowerCase()}.`,\n                ]),\n            )\n            .then(async d => d.player(DialogueEmote.CALM_TALK_2, [`Right, I'll do that.`]))\n            .then(d => {\n                d.close();\n            });\n    }\n}\n\nexport const actionItem: itemOnObjectActionHandler = details => milkCow(details);\n\nexport const actionInteract: objectInteractionActionHandler = details => milkCow(details);\n\nexport default {\n    pluginId: 'rs:cow_milking',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.milkableCow,\n            options: 'milk',\n            walkTo: true,\n            handler: actionInteract,\n        },\n        {\n            type: 'item_on_object',\n            objectIds: objectIds.milkableCow,\n            itemIds: itemIds.bucket,\n            walkTo: true,\n            handler: actionItem,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/crates/crates.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nexport const action: objectInteractionActionHandler = details => {\n    const veggies = [itemIds.onion, itemIds.grain, itemIds.cabbage];\n    details.player.busy = true;\n    details.player.playAnimation(827);\n\n    const random = Math.floor(Math.random() * veggies.length);\n    const pickedItem = findItem(veggies[random])!;\n\n    details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     details.player.sendMessage(`You found a ${pickedItem.name.toLowerCase()} chest!.`);\n    //     details.player.playSound(2581, 7);\n    //     details.player.instance.hideGameObjectTemporarily(details.object, 60);\n    //     details.player.giveItem(pickedItem.gameId);\n    //     details.player.busy = false;\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:crates',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [366, 357, 355],\n            options: ['loot', 'search', 'examine'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/doors/door.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { WNES, directionData } from '@engine/world/direction';\nimport type { LandscapeObject } from '@runejs/filestore';\n\n// @TODO move to yaml config\nconst doors = [\n    {\n        closed: 1530,\n        open: 1531,\n        hinge: 'RIGHT',\n    },\n    {\n        closed: 11707,\n        open: 11708,\n        hinge: 'RIGHT',\n    },\n    {\n        closed: 1533,\n        open: 1534,\n        hinge: 'RIGHT',\n    },\n    {\n        closed: 1516,\n        open: 1517,\n        hinge: 'LEFT',\n    },\n    {\n        closed: 1519,\n        open: 1520,\n        hinge: 'RIGHT',\n    },\n    {\n        closed: 1536,\n        open: 1537,\n        hinge: 'LEFT',\n    },\n    {\n        closed: 11993,\n        open: 11994,\n        hinge: 'RIGHT',\n    },\n    {\n        closed: 13001,\n        open: 13002,\n        hinge: 'RIGHT',\n    },\n];\n\nconst leftHingeDir: { [key: string]: string } = {\n    NORTH: 'WEST',\n    SOUTH: 'EAST',\n    WEST: 'SOUTH',\n    EAST: 'NORTH',\n};\nconst rightHingeDir: { [key: string]: string } = {\n    NORTH: 'EAST',\n    SOUTH: 'WEST',\n    WEST: 'NORTH',\n    EAST: 'SOUTH',\n};\n\nexport const action: objectInteractionActionHandler = ({ player, object: door, position, cacheOriginal }): void => {\n    let opening = true;\n    let doorConfig = doors.find(d => d.closed === door.objectId);\n    let hingeConfig;\n    let replacementDoorId: number;\n    if (!doorConfig) {\n        doorConfig = doors.find(d => d.open === door.objectId);\n        if (!doorConfig) {\n            return;\n        }\n\n        opening = false;\n        hingeConfig = doorConfig.hinge === 'LEFT' ? rightHingeDir : leftHingeDir;\n        replacementDoorId = doorConfig.closed;\n    } else {\n        hingeConfig = doorConfig.hinge === 'LEFT' ? leftHingeDir : rightHingeDir;\n        replacementDoorId = doorConfig.open;\n    }\n\n    const startDir = WNES[door.orientation];\n    const endDir = hingeConfig[startDir];\n    const endPosition = position.step(opening ? 1 : -1, opening ? startDir : endDir);\n\n    const replacementDoor: LandscapeObject = {\n        objectId: replacementDoorId,\n        x: endPosition.x,\n        y: endPosition.y,\n        level: position.level,\n        type: door.type,\n        orientation: directionData[endDir].rotation,\n    };\n\n    player.instance.toggleGameObjects(replacementDoor, door, !cacheOriginal);\n    // 70 = close gate, 71 = open gate, 62 = open door, 60 = close door\n    player.playSound(opening ? soundIds.openDoor : soundIds.closeDoor, 7);\n};\n\nexport default {\n    pluginId: 'rs:standard_doors',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [1530, 4465, 4467, 3014, 3017, 3018, 3019, 1536, 1537, 1533, 1531, 1534, 12348, 11993, 11994, 13001, 13002],\n            options: ['open', 'close'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/doors/double-door.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { activeWorld } from '@engine/world';\nimport { WNES } from '@engine/world/direction';\nimport { Position } from '@engine/world/position';\nimport { action as doorAction } from '@plugins/objects/doors/door.plugin';\nimport { logger } from '@runejs/common';\n\nconst doubleDoors = [\n    {\n        closed: [1516, 1519],\n        open: [1517, 1520],\n    },\n];\n\nconst closingDelta = {\n    WEST: { x: 1, y: 0 },\n    EAST: { x: -1, y: 0 },\n    NORTH: { x: 0, y: -1 },\n    SOUTH: { x: 0, y: 1 },\n};\n\nconst openingDelta = {\n    LEFT: {\n        WEST: { x: 0, y: 1 },\n        EAST: { x: 0, y: -1 },\n        NORTH: { x: 1, y: 0 },\n        SOUTH: { x: -1, y: 0 },\n    },\n    RIGHT: {\n        WEST: { x: 0, y: -1 },\n        EAST: { x: 0, y: 1 },\n        NORTH: { x: -1, y: 0 },\n        SOUTH: { x: 1, y: 0 },\n    },\n};\n\nconst action: objectInteractionActionHandler = details => {\n    const { player, object: door, position, cacheOriginal } = details;\n    let doorConfig = doubleDoors.find(d => d.closed.indexOf(door.objectId) !== -1);\n    let doorIds: number[];\n    let opening = true;\n    if (!doorConfig) {\n        doorConfig = doubleDoors.find(d => d.open.indexOf(door.objectId) !== -1);\n        if (!doorConfig) {\n            return;\n        }\n\n        opening = false;\n        doorIds = doorConfig.open;\n    } else {\n        doorIds = doorConfig.closed;\n    }\n\n    const leftDoorId = doorIds[0];\n    const rightDoorId = doorIds[1];\n    const hinge = leftDoorId === door.objectId ? 'LEFT' : 'RIGHT';\n    const direction = WNES[door.orientation];\n    let deltaX = 0;\n    let deltaY = 0;\n    const otherDoorId = hinge === 'LEFT' ? rightDoorId : leftDoorId;\n\n    if (!opening) {\n        deltaX += closingDelta[direction].x;\n        deltaY += closingDelta[direction].y;\n    } else {\n        deltaX += openingDelta[hinge][direction].x;\n        deltaY += openingDelta[hinge][direction].y;\n    }\n\n    if (!otherDoorId || (deltaX === 0 && deltaY === 0)) {\n        logger.error('Improperly handled double door at ' + door.x + ',' + door.y + ',' + door.level);\n        return;\n    }\n\n    const otherDoorPosition = new Position(door.x + deltaX, door.y + deltaY, door.level);\n\n    const { object: otherDoor } = activeWorld.findObjectAtLocation(player, otherDoorId, otherDoorPosition);\n    if (!otherDoor) {\n        return;\n    }\n\n    // TODO (Jameskmonger) fix the 'as any' here, I used it to satisfy TypeScript strict checks\n    doorAction({\n        player,\n        object: door,\n        objectConfig: null as any,\n        position,\n        cacheOriginal,\n        option: opening ? 'open' : 'close',\n    });\n    doorAction({\n        player,\n        object: otherDoor,\n        objectConfig: null as any,\n        position: otherDoorPosition,\n        cacheOriginal,\n        option: opening ? 'open' : 'close',\n    });\n};\n\nexport default {\n    pluginId: 'rs:double_doors',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [1519, 1516, 1517, 1520],\n            options: ['open', 'close'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/doors/gate.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { activeWorld } from '@engine/world';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { WNES, directionData } from '@engine/world/direction';\nimport type { Chunk } from '@engine/world/map/chunk';\nimport type { ModifiedLandscapeObject } from '@engine/world/map/landscape-object';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\n\nconst gates = [\n    {\n        main: 1551,\n        mainOpen: 1552,\n        hinge: 'LEFT',\n        secondary: 1553,\n        secondaryOpen: 1556,\n    },\n    {\n        main: 12986,\n        mainOpen: 12988,\n        hinge: 'LEFT',\n        secondary: 12987,\n        secondaryOpen: 12989,\n    },\n];\n\n// @TODO clean up this disgusting code\nconst action: objectInteractionActionHandler = details => {\n    const { player, cacheOriginal } = details;\n    let { object: gate, position } = details;\n\n    if ((gate as ModifiedLandscapeObject).metadata) {\n        const metadata = (gate as ModifiedLandscapeObject).metadata;\n\n        if (!metadata) {\n            logger.error(`Could not find metadata for gate with id ${gate.objectId}`);\n            player.sendMessage('Oops, something went wrong. Please report this to a developer.');\n            return;\n        }\n\n        player.instance.toggleGameObjects(metadata.originalMain, metadata.main, true);\n        player.instance.toggleGameObjects(metadata.originalSecond, metadata.second, true);\n        player.playSound(soundIds.closeGate, 7);\n    } else {\n        let details = gates.find(g => g.main === gate.objectId);\n        let clickedSecondary = false;\n        let secondGate;\n        let hinge;\n        let direction = WNES[gate.orientation];\n        let hingeChunk: Chunk;\n        let gateSecondPosition: Position | null = null;\n\n        if (!details) {\n            details = gates.find(g => g.secondary === gate.objectId);\n\n            if (!details) {\n                logger.error(`Could not find gate details for gate with id ${gate.objectId} on second pass.`);\n                player.sendMessage('Oops, something went wrong. Please report this to a developer.');\n                return;\n            }\n\n            secondGate = gate;\n            gateSecondPosition = position;\n            clickedSecondary = true;\n\n            hinge = details.hinge;\n            let deltaX = 0;\n            let deltaY = 0;\n\n            if (hinge === 'LEFT') {\n                switch (direction) {\n                    case 'WEST':\n                        deltaY--;\n                        break;\n                    case 'EAST':\n                        deltaY++;\n                        break;\n                    case 'NORTH':\n                        deltaX--;\n                        break;\n                    case 'SOUTH':\n                        deltaX++;\n                        break;\n                }\n            } else if (hinge === 'RIGHT') {\n                switch (direction) {\n                    case 'WEST':\n                        deltaY++;\n                        break;\n                    case 'EAST':\n                        deltaY--;\n                        break;\n                    case 'NORTH':\n                        deltaX++;\n                        break;\n                    case 'SOUTH':\n                        deltaX--;\n                        break;\n                }\n            }\n\n            const pos = new Position(gate.x + deltaX, gate.y + deltaY, gate.level);\n            hingeChunk = activeWorld.chunkManager.getChunkForWorldPosition(pos);\n\n            const mainGate = hingeChunk.getFilestoreLandscapeObject(details.main, pos);\n\n            if (mainGate) {\n                gate = mainGate;\n                direction = WNES[gate.orientation];\n                position = pos;\n            } else {\n                logger.error('Could not find main gate for secondary gate at ' + gate.x + ',' + gate.y + ',' + gate.level);\n                player.sendMessage('Oops, something went wrong. Please report this to a developer.');\n            }\n        } else {\n            hinge = details.hinge;\n        }\n\n        let deltaX = 0;\n        let deltaY = 0;\n        let newX = 0;\n        let newY = 0;\n\n        if (hinge === 'LEFT') {\n            switch (direction) {\n                case 'WEST':\n                    deltaY++;\n                    newX--;\n                    break;\n                case 'EAST':\n                    deltaY--;\n                    newX++;\n                    break;\n                case 'NORTH':\n                    deltaX++;\n                    newY++;\n                    break;\n                case 'SOUTH':\n                    deltaX--;\n                    newY--;\n                    break;\n            }\n        } else if (hinge === 'RIGHT') {\n            switch (direction) {\n                case 'WEST':\n                    deltaY--;\n                    newX++;\n                    break;\n                case 'EAST':\n                    deltaY++;\n                    newX--;\n                    break;\n                case 'NORTH':\n                    deltaX--;\n                    newY--;\n                    break;\n                case 'SOUTH':\n                    deltaX++;\n                    newY++;\n                    break;\n            }\n        }\n\n        const leftHingeDirections: { [key: string]: string } = {\n            NORTH: 'WEST',\n            SOUTH: 'EAST',\n            WEST: 'SOUTH',\n            EAST: 'NORTH',\n        };\n        const rightHingeDirections: { [key: string]: string } = {\n            NORTH: 'EAST',\n            SOUTH: 'WEST',\n            WEST: 'NORTH',\n            EAST: 'SOUTH',\n        };\n\n        if (deltaX === 0 && deltaY === 0) {\n            logger.error('Improperly handled gate at ' + gate.x + ',' + gate.y + ',' + gate.level);\n            return;\n        }\n\n        const newDirection = hinge === 'LEFT' ? leftHingeDirections[direction] : rightHingeDirections[direction];\n\n        if (!clickedSecondary) {\n            gateSecondPosition = new Position(gate.x + deltaX, gate.y + deltaY, gate.level);\n        }\n\n        if (!gateSecondPosition) {\n            logger.error('Improperly handled gate at ' + gate.x + ',' + gate.y + ',' + gate.level);\n            player.sendMessage('Oops, something went wrong. Please report this to a developer.');\n            return;\n        }\n\n        const gateSecondChunk = activeWorld.chunkManager.getChunkForWorldPosition(gateSecondPosition);\n\n        if (!clickedSecondary) {\n            secondGate = gateSecondChunk.getFilestoreLandscapeObject(details.secondary, gateSecondPosition);\n        }\n\n        const newPosition = position.step(1, direction);\n        const newSecondPosition = new Position(newPosition.x + newX, newPosition.y + newY, gate.level);\n\n        const newHinge = {\n            objectId: details.mainOpen,\n            x: newPosition.x,\n            y: newPosition.y,\n            level: newPosition.level,\n            type: gate.type,\n            orientation: directionData[newDirection].rotation,\n        } as ModifiedLandscapeObject;\n        const newSecond = {\n            objectId: details.secondaryOpen,\n            x: newSecondPosition.x,\n            y: newSecondPosition.y,\n            level: newSecondPosition.level,\n            type: gate.type,\n            orientation: directionData[newDirection].rotation,\n        } as ModifiedLandscapeObject;\n\n        const metadata = {\n            second: JSON.parse(JSON.stringify(newSecond)),\n            originalSecond: secondGate,\n            main: JSON.parse(JSON.stringify(newHinge)),\n            originalMain: gate,\n        };\n\n        newHinge.metadata = metadata;\n        newSecond.metadata = metadata;\n\n        player.instance.toggleGameObjects(newHinge, gate, !cacheOriginal);\n        player.instance.toggleGameObjects(newSecond, secondGate, !cacheOriginal);\n        player.playSound(soundIds.openGate, 7);\n    }\n};\n\nexport default {\n    pluginId: 'rs:gates',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [1551, 1552, 1553, 1556, 12986, 12987, 12988, 12989],\n            options: ['open', 'close'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/dungeon-entrances/taverly-dungeon-ladder.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { objectIds } from '@engine/world/config/object-ids';\n\nexport const enterDungeon: objectInteractionActionHandler = details => {\n    const loc = details.player.position.clone();\n    loc.y += 6400;\n    details.player.playAnimation(animationIds.climbLadder);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     details.player.teleport(loc);\n    // }, World.TICK_LENGTH);\n};\n\nexport const exitDungeon: objectInteractionActionHandler = details => {\n    const loc = details.player.position.clone();\n    loc.y -= 6400;\n    details.player.playAnimation(animationIds.climbLadder);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     details.player.teleport(loc);\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:taverly_dungeon_ladder',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.ladders.taverlyDungeonOverworld,\n            options: ['climb-down'],\n            walkTo: true,\n            handler: enterDungeon,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.ladders.taverlyDungeonUnderground,\n            options: ['climb-up'],\n            walkTo: true,\n            handler: exitDungeon,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/item-spawns/take-axe.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { logger } from '@runejs/common';\n\nconst itemMappings: Record<number, number> = {\n    [objectIds.lumbridgeAxeInLogs]: itemIds.axes.bronze,\n};\n\nexport const action: objectInteractionActionHandler = details => {\n    const { player, option } = details;\n\n    const name = details.objectConfig.name || '';\n    if (!name) {\n        logger.warn(`Object ${details.object.objectId} has no name.`);\n    }\n\n    switch (option) {\n        case 'take-axe':\n            player.playAnimation(827);\n            player.sendMessage(`You take the axe.`);\n            player.playSound(2581, 7);\n            player.giveItem(itemMappings[details.object.objectId]);\n            return;\n        default:\n            player.sendMessage(`This has not been implemented.`);\n            return;\n    }\n};\n\nexport default {\n    pluginId: 'rs:take_axe',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [objectIds.lumbridgeAxeInLogs],\n            options: ['take-axe'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/ladders/ladder.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { ActorTask } from '@engine/task/impl/actor-task';\nimport { ActorTeleportTask } from '@engine/task/impl/actor-teleport-task';\nimport type { Actor } from '@engine/world/actor/actor';\nimport { dialogueAction } from '@engine/world/actor/player/dialogue-action';\nimport { Position } from '@engine/world/position';\nimport { logger } from '@runejs/common';\n\nconst planes = { min: 0, max: 3 };\nconst validate: (level: number) => boolean = level => {\n    return planes.min <= level && level <= planes.max;\n}; //TODO: prevent no-clipping.\n\nexport const action: objectInteractionActionHandler = details => {\n    const { player, option } = details;\n\n    const ladderObjectName = details.objectConfig.name || '';\n    if (!ladderObjectName) {\n        logger.warn(`Ladder object ${details.object.objectId} has no name.`);\n    }\n\n    if (option === 'climb') {\n        dialogueAction(player)\n            .then(async d =>\n                d.options(`Climb up or down the ${ladderObjectName.toLowerCase()}?`, [\n                    `Climb up the ${ladderObjectName.toLowerCase()}.`,\n                    `Climb down the ${ladderObjectName.toLowerCase()}.`,\n                ]),\n            )\n            .then(d => {\n                d.close();\n                switch (d.action) {\n                    case 1:\n                    case 2:\n                        player.enqueueTask(\n                            class LadderTask extends ActorTask {\n                                constructor(actor: Actor) {\n                                    super(actor, { repeat: false, immediate: false });\n                                }\n\n                                execute() {\n                                    action({ ...details, option: `climb-${d.action === 1 ? 'up' : 'down'}` });\n                                }\n                            },\n                        );\n                        return;\n                }\n            });\n        return;\n    }\n    const up = option === 'climb-up';\n    const { position } = player;\n    const newPosition = new Position(position.x, position.y, position.level);\n    newPosition.level = position.level + (up ? 1 : -1);\n    if (position.level === 0) {\n        if (newPosition.level === 1 && position.y >= 6400) {\n            newPosition.level = 0;\n            newPosition.y -= 6414;\n            newPosition.x++;\n        } else if (newPosition.level === -1) {\n            newPosition.level = 0;\n            newPosition.y += 6414;\n            newPosition.x--;\n        }\n    }\n    if (!validate(newPosition.level)) return;\n    if (!ladderObjectName.startsWith('Stair')) {\n        player.playAnimation(up ? 828 : 827);\n    }\n    player.sendMessage(`You climb ${option.slice(6)} the ${ladderObjectName.toLowerCase()}.`);\n    player.enqueueTask(ActorTeleportTask, [newPosition]);\n};\n\nexport default {\n    pluginId: 'rs:ladders',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [1738, 1739, 1740, 1746, 1747, 1748, 2147, 2148, 12964, 12965, 12966],\n            options: ['climb', 'climb-up', 'climb-down'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/mill/flour-bin.plugin.ts",
    "content": "import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { ObjectConfig } from '@runejs/filestore';\n\nfunction flourBin(details: { objectConfig: ObjectConfig; player: Player }): void {\n    const { player, objectConfig } = details;\n\n    if (!details.player.savedMetadata['mill-flour']) {\n        player.sendMessage(\n            `The ${(objectConfig.name || '').toLowerCase()} is already empty. You need to place wheat in the hopper upstairs `,\n        );\n        player.sendMessage(`first.`);\n    } else {\n        if (player.hasItemInInventory(itemIds.pot)) {\n            player.playSound(soundIds.potContentModified, 7);\n            player.removeFirstItem(itemIds.pot);\n            player.giveItem(itemIds.potOfFlour);\n            details.player.savedMetadata['mill-flour'] -= 1;\n        } else {\n            player.sendMessage(`You need a pot to hold the flour in.`);\n        }\n    }\n\n    updateBin(details);\n}\n\nexport const updateBin: playerInitActionHandler = details => {\n    const count = (details.player.savedMetadata['mill-flour'] || 0) === 0 ? 0 : 1;\n    details.player.outgoingPackets.updateClientConfig(695, count);\n};\n\nconst actionInteract: objectInteractionActionHandler = details => {\n    flourBin(details);\n};\n\nconst actionItem: itemOnObjectActionHandler = details => {\n    flourBin(details);\n};\n\nexport default {\n    pluginId: 'rs:flour_bin',\n    hooks: [\n        {\n            type: 'item_on_object',\n            objectIds: [1781, 5792, 1782],\n            itemIds: [itemIds.pot],\n            walkTo: true,\n            handler: actionItem,\n        },\n        {\n            type: 'player_init',\n            handler: updateBin,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: [1781, 1782],\n            options: ['empty'],\n            walkTo: true,\n            handler: actionInteract,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/mill/hopper-controls.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\n\nexport const action: objectInteractionActionHandler = details => {\n    details.player.busy = true;\n    details.player.playAnimation(3571);\n    details.player.playSound(2400, 5);\n    details.player.personalInstance.replaceGameObject(2722, details.object, 1);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     if (details.player.savedMetadata['mill-grain'] && details.player.savedMetadata['mill-grain'] >= 1) {\n    //         details.player.sendMessage(`You operate the hopper. The grain slide down the chute.`);\n    //         if (!details.player.savedMetadata['mill-flour']) {\n    //             details.player.savedMetadata['mill-flour'] = 0;\n    //         }\n    //         details.player.savedMetadata['mill-flour'] += details.player.savedMetadata['mill-grain'];\n    //         details.player.savedMetadata['mill-grain'] = 0;\n    //         details.player.outgoingPackets.updateClientConfig(695, 1);\n    //     } else {\n    //         details.player.sendMessage(`You operate the hopper. Nothing interesting happens.`);\n    //     }\n    //     details.player.busy = false;\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:grain_hopper_controls',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [2718, 2721],\n            options: ['operate'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/mill/hopper.plugin.ts",
    "content": "import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nexport const action: itemOnObjectActionHandler = details => {\n    if (details.player.savedMetadata['mill-grain'] && details.player.savedMetadata['mill-grain'] === 1) {\n        details.player.sendMessage(`There is already grain in the hopper.`);\n        return;\n    }\n    details.player.busy = true;\n    details.player.playAnimation(3572);\n    details.player.playSound(2576, 5);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     details.player.removeFirstItem(itemIds.grain);\n    //     details.player.sendMessage(`You put the grain in the hopper. You should now pull the lever nearby to operate`);\n    //     details.player.sendMessage(`the hopper.`);\n    //     details.player.savedMetadata['mill-grain'] = 1;\n    //     details.player.busy = false;\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:grain_hopper',\n    hooks: [\n        {\n            type: 'item_on_object',\n            objectIds: [2714, 2717],\n            itemIds: [itemIds.grain],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/objects/pickables/pickables.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { logger } from '@runejs/common';\n\nexport const action: objectInteractionActionHandler = details => {\n    details.player.busy = true;\n    details.player.playAnimation(827);\n    let itemId: number;\n    let prefix = 'some';\n    switch (details.objectConfig.name) {\n        case 'Wheat':\n            itemId = itemIds.grain;\n            break;\n        case 'Onion':\n            itemId = itemIds.onion;\n            prefix = 'an';\n            break;\n        case 'Potato':\n            prefix = 'a';\n            itemId = itemIds.potato;\n            break;\n        case 'Flax':\n            itemId = itemIds.flax;\n            break;\n        case 'Cabbage':\n        default:\n            itemId = itemIds.cabbage;\n            break;\n    }\n    const pickedItem = findItem(itemId);\n\n    if (!pickedItem) {\n        logger.warn(`Could not find item for pickable with id ${itemId}`);\n        details.player.busy = false;\n        return;\n    }\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     details.player.sendMessage(`You ${details.option} the ${(details.objectConfig.name || '').toLowerCase()} and receive ${prefix} ${pickedItem.name.toLowerCase()}.`);\n    //     details.player.playSound(2581, 7);\n    //     if (details.objectConfig.name !== 'Flax' || Math.floor(Math.random() * 10) === 1) {\n    //         details.player.instance.hideGameObjectTemporarily(details.object, 30);\n    //     }\n    //     details.player.giveItem(pickedItem.gameId);\n    //     details.player.busy = false;\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:pickables',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [313, 5583, 5584, 5585, 1161, 3366, 312, 2646],\n            options: ['pick'],\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/player/follow-player.plugin.js",
    "content": "module.exports = {\n    pluginId: 'rs:follow_player',\n    hooks: [\n        {\n            type: 'player_interaction',\n            options: 'follow',\n            handler: details => details.player.follow(details.otherPlayer),\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/player/login-unlock-emotes.plugin.ts",
    "content": "import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { unlockEmotes } from '@plugins/buttons/player-emotes.plugin';\n\nexport const handler: playerInitActionHandler = ({ player }) => unlockEmotes(player);\n\nexport default {\n    pluginId: 'rs:unlock_player_emotes',\n    hooks: [{ type: 'player_init', handler }],\n};\n"
  },
  {
    "path": "src/plugins/player/login-update-settings.plugin.ts",
    "content": "import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { validateSettings } from '@engine/world/actor/player/player-data';\nimport { widgetScripts } from '@engine/world/config/widget';\n\nexport const handler: playerInitActionHandler = ({ player }) => {\n    validateSettings(player);\n\n    const settings = player.settings;\n    player.outgoingPackets.updateClientConfig(widgetScripts.brightness, settings.screenBrightness);\n    player.outgoingPackets.updateClientConfig(widgetScripts.mouseButtons, settings.twoMouseButtonsEnabled ? 0 : 1);\n    player.outgoingPackets.updateClientConfig(widgetScripts.splitPrivateChat, settings.splitPrivateChatEnabled ? 1 : 0);\n    player.outgoingPackets.updateClientConfig(widgetScripts.chatEffects, settings.chatEffectsEnabled ? 0 : 1);\n    player.outgoingPackets.updateClientConfig(widgetScripts.acceptAid, settings.acceptAidEnabled ? 1 : 0);\n    player.outgoingPackets.updateClientConfig(widgetScripts.musicVolume, settings.musicVolume);\n    player.outgoingPackets.updateClientConfig(widgetScripts.soundEffectVolume, settings.soundEffectVolume);\n    player.outgoingPackets.updateClientConfig(widgetScripts.areaEffectVolume, settings.areaEffectVolume);\n    player.outgoingPackets.updateClientConfig(widgetScripts.runMode, settings.runEnabled ? 1 : 0);\n    player.outgoingPackets.updateClientConfig(widgetScripts.autoRetaliate, settings.autoRetaliateEnabled ? 0 : 1);\n    player.outgoingPackets.updateClientConfig(widgetScripts.attackStyle, settings.attackStyle);\n    player.outgoingPackets.updateClientConfig(widgetScripts.bankInsertMode, settings.bankInsertMode);\n    player.outgoingPackets.updateClientConfig(widgetScripts.bankWithdrawNoteMode, settings.bankWithdrawNoteMode);\n    player.outgoingPackets.updateSocialSettings();\n};\n\nexport default {\n    pluginId: 'rs:update_player_settings',\n    hooks: [{ type: 'player_init', handler }],\n};\n"
  },
  {
    "path": "src/plugins/player/update-friends-list.plugin.ts",
    "content": "import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { PrivateMessaging } from '@engine/world/actor/player/private-messaging';\n\nexport const handler: playerInitActionHandler = ({ player }) => {\n    PrivateMessaging.playerLoggedIn(player);\n    player.outgoingPackets.sendFriendServerStatus(2);\n};\n\nexport default {\n    pluginId: 'rs:update_friends_list',\n    hooks: [{ type: 'player_init', handler }],\n};\n"
  },
  {
    "path": "src/plugins/quests/cooks-assistant-quest.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport type { PlayerQuest, QuestJournalHandler } from '@engine/config/quest-config';\nimport type { DialogueTree } from '@engine/world/actor/dialogue';\nimport { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Quest } from '@engine/world/actor/player/quest';\nimport { itemIds } from '@engine/world/config/item-ids';\n\nconst journalHandler: QuestJournalHandler = {\n    0: `I can start this quest by speaking to the <col=800000>Cook</col> in the\n        <col=800000>Kitchen</col> on the ground floor of <col=800000>Lumbridge Castle</col>.`,\n\n    50: player => {\n        let questLog =\n            `It's the <col=800000>Duke of Lumbridge's</col> birthday and I have to help ` +\n            `his <col=800000>Cook</col> make him a <col=800000>birthday cake.</col> To do this I need to ` +\n            `bring him the following ingredients:\\n`;\n\n        const quest = player.getQuest('rs:cooks_assistant');\n\n        if (player.hasItemInInventory(itemIds.bucketOfMilk) || quest.metadata.givenMilk) {\n            questLog += `I have found a <col=800000>bucket of milk</col> to give to the cook.\\n`;\n        } else {\n            questLog +=\n                `I need to find a <col=800000>bucket of milk.</col> There's a cattle field east ` +\n                `of Lumbridge, I should make sure I take an empty bucket with me.\\n`;\n        }\n\n        if (player.hasItemInInventory(itemIds.potOfFlour) || quest.metadata.givenFlour) {\n            questLog += `I have found a <col=800000>pot of flour</col> to give to the cook.\\n`;\n        } else {\n            questLog +=\n                `I need to find a <col=800000>pot of flour.</col> There's a mill found north-` +\n                `west of Lumbridge, I should take an empty pot with me.\\n`;\n        }\n\n        if (player.hasItemInInventory(itemIds.egg) || quest.metadata.givenEgg) {\n            questLog += `I have found an <col=800000>egg</col> to give to the cook.\\n`;\n        } else {\n            questLog +=\n                `I need to find an <col=800000>egg.</col> The cook normally gets his eggs from ` +\n                `the Groats' farm, found just to the west of the cattle field.`;\n        }\n\n        return questLog;\n    },\n\n    complete:\n        `It was the Duke of Lumbridge's birthday, but his cook had ` +\n        `forgotten to buy the ingredients he needed to make him a ` +\n        `cake. I brought the cook an egg, some flour and some milk ` +\n        `and the cook made a delicious looking cake with them.\\n\\n` +\n        `As a reward he now lets me use his high quality range ` +\n        `which lets me burn things less whenever I wish to cook ` +\n        `there.\\n\\n` +\n        `<col=ff0000>QUEST COMPLETE!</col>`,\n};\n\nfunction dialogueIngredientQuestions(): Function {\n    return (options, tag_INGREDIENT_QUESTIONS) => [\n        `Where do I find some flour?`,\n        [\n            player => [Emote.GENERIC, `Where do I find some flour?`],\n            cook => [\n                Emote.GENERIC,\n                `There is a Mill fairly close, go North and then West. Mill Lane Mill ` +\n                    `is just off the road to Draynor. I usually get my flour from there.`,\n            ],\n            cook => [Emote.HAPPY, `Talk to Millie, she'll help, she's a lovely girl and a fine Miller.`],\n            goto('tag_INGREDIENT_QUESTIONS'),\n        ],\n        `How about milk?`,\n        [\n            player => [Emote.GENERIC, `How about milk?`],\n            cook => [Emote.GENERIC, `There is a cattle field on the other side of the river, just across ` + `the road from Groats' Farm.`],\n            cook => [\n                Emote.HAPPY,\n                `Talk to Gillie Groats, she look after the Dairy Cows - ` +\n                    `she'll tell you everything you need to know about milking cows!`,\n            ],\n            goto('tag_INGREDIENT_QUESTIONS'),\n        ],\n        `And eggs? Where are they found?`,\n        [\n            player => [Emote.GENERIC, `And eggs? Where are they found?`],\n            cook => [Emote.GENERIC, `I normally get my eggs from the Groats' farm, on the other side of ` + `the river.`],\n            cook => [Emote.GENERIC, `But any chicken should lay eggs.`],\n            goto('tag_INGREDIENT_QUESTIONS'),\n        ],\n        `Actually, I know where to find this stuff.`,\n        [player => [Emote.GENERIC, `I've got all the information I need. Thanks.`]],\n    ];\n}\n\nconst startQuestAction: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    dialogue(\n        [player, { npc, key: 'cook' }],\n        [\n            cook => [Emote.WORRIED, `What am I to do?`],\n            options => [\n                `What's wrong?`,\n                [],\n                `Can you make me a cake?`,\n                [\n                    player => [Emote.HAPPY, `You're a cook, why don't you bake me a cake?`],\n                    cook => [Emote.SAD, `*sniff* Don't talk to me about cakes...`],\n                ],\n                `You don't look very happy.`,\n                [\n                    player => [Emote.WORRIED, `You don't look very happy.`],\n                    cook => [\n                        Emote.SAD,\n                        `No, I'm not. The world is caving in around me - I am overcome by dark feelings ` + `of impending doom.`,\n                    ],\n                    options => [\n                        `What's wrong?`,\n                        [],\n                        `I'd take the rest of the day off if I were you.`,\n                        [\n                            player => [Emote.GENERIC, `I'd take the rest of the day off if I were you.`],\n                            cook => [Emote.WORRIED, `No, that's the worst thing I could do. I'd get in terrible trouble.`],\n                            player => [Emote.SKEPTICAL, `Well maybe you need to take a holiday...`],\n                            cook => [Emote.SAD, `That would be nice, but the Duke doesn't allow holidays for core staff.`],\n                            player => [Emote.LAUGH, `Hmm, why not run away to the sea and start a new life as a Pirate?`],\n                            cook => [\n                                Emote.SKEPTICAL,\n                                `My wife gets sea sick, and I have an irrational fear of eyepatches. ` + `I don't see it working myself.`,\n                            ],\n                            player => [Emote.WORRIED, `I'm afraid I've run out of ideas.`],\n                            cook => [Emote.SAD, `I know I'm doomed.`],\n                        ],\n                    ],\n                ],\n                `Nice hat!`,\n                [\n                    player => [Emote.HAPPY, `Nice hat!`],\n                    cook => [Emote.SKEPTICAL, `Err thank you. It's a pretty ordinary cooks hat really.`],\n                    player => [Emote.HAPPY, `Still, suits you. The trousers are pretty special too.`],\n                    cook => [Emote.SKEPTICAL, `It's all standard cook's issue uniform...`],\n                    player => [\n                        Emote.POMPOUS,\n                        `The whole hat, apron, striped trousers ensemble - it works. It makes you ` + `look like a real cook.`,\n                    ],\n                    cook => [\n                        Emote.ANGRY,\n                        `I am a real cook! I haven't got time to be chatting about Culinary Fashion. ` + `I am in desperate need of help!`,\n                    ],\n                ],\n            ],\n            player => [Emote.HAPPY, `What's wrong?`],\n            cook => [\n                Emote.WORRIED,\n                `Oh dear, oh dear, oh dear, I'm in a terrible terrible ` +\n                    ` mess! It's the Duke's birthday today, and I should be making him a lovely big birthday cake.`,\n            ],\n            cook => [\n                Emote.WORRIED,\n                `I've forgotten to buy the ingredients. I'll never get ` +\n                    `them in time now. He'll sack me! What will I do? I have four children and a goat to ` +\n                    `look after. Would you help me? Please?`,\n            ],\n            options => [\n                `I'm always happy to help a cook in distress.`,\n                [\n                    execute(() => {\n                        player.setQuestProgress('rs:cooks_assistant', 50);\n                    }),\n                    player => [Emote.GENERIC, `Yes, I'll help you.`],\n                    cook => [\n                        Emote.HAPPY,\n                        `Oh thank you, thank you. I need milk, an egg and flour. I'd be very grateful ` + `if you can get them for me.`,\n                    ],\n                    player => [Emote.GENERIC, `So where do I find these ingredients then?`],\n                    dialogueIngredientQuestions(),\n                ],\n                `I can't right now, maybe later.`,\n                [\n                    player => [Emote.GENERIC, `No, I don't feel like it. Maybe later.`],\n                    cook => [Emote.ANGRY, `Fine. I always knew you Adventurer types were callous beasts. ` + `Go on your merry way!`],\n                ],\n            ],\n        ],\n    );\n};\n\nfunction youStillNeed(quest: PlayerQuest): DialogueTree {\n    return [\n        text =>\n            `You still need to get\\n` +\n            `${!quest.metadata.givenMilk ? `A bucket of milk. ` : ``}${!quest.metadata.givenFlour ? `A pot of flour. ` : ``}${!quest.metadata.givenEgg ? `An egg.` : ``}`,\n        options => [\n            `I'll get right on it.`,\n            [player => [Emote.GENERIC, `I'll get right on it.`]],\n            `Can you remind me how to find these things again?`,\n            [player => [Emote.GENERIC, `So where do I find these ingredients then?`], dialogueIngredientQuestions()],\n        ],\n    ];\n}\n\nconst handInIngredientsAction: npcInteractionActionHandler = async details => {\n    const { player, npc } = details;\n\n    const dialogueTree: DialogueTree = [cook => [Emote.GENERIC, `How are you getting on with finding the ingredients?`]];\n\n    const quest = player.getQuest('rs:cooks_assistant');\n\n    const ingredients = [\n        { itemId: itemIds.bucketOfMilk, text: `Here's a bucket of milk.`, attr: 'givenMilk' },\n        { itemId: itemIds.potOfFlour, text: `Here's a pot of flour.`, attr: 'givenFlour' },\n        { itemId: itemIds.egg, text: `Here's a fresh egg.`, attr: 'givenEgg' },\n    ];\n\n    for (const ingredient of ingredients) {\n        if (quest.metadata[ingredient.attr]) {\n            quest.metadata.ingredientCount++;\n            continue;\n        }\n\n        if (!player.hasItemInInventory(ingredient.itemId)) {\n            continue;\n        }\n\n        dialogueTree.push(\n            player => [Emote.GENERIC, ingredient.text],\n            execute(() => {\n                const quest = player.getQuest('rs:cooks_assistant');\n\n                if (player.removeFirstItem(ingredient.itemId) !== -1) {\n                    quest.metadata[ingredient.attr] = true;\n                }\n            }),\n        );\n    }\n\n    let questComplete: boolean = false;\n\n    dialogueTree.push(\n        goto(() => {\n            const count = [quest.metadata.givenMilk, quest.metadata.givenFlour, quest.metadata.givenEgg].filter(\n                value => value === true,\n            ).length;\n\n            if (count === 3) {\n                return 'tag_ALL_INGREDIENTS';\n            } else if (count === 0) {\n                return 'tag_NO_INGREDIENTS';\n            } else {\n                return 'tag_SOME_INGREDIENTS';\n            }\n        }),\n        (subtree, tag_ALL_INGREDIENTS) => [\n            cook => [Emote.HAPPY, `You've brought me everything I need! I am saved! Thank you!`],\n            player => [Emote.WONDERING, `So do I get to go to the Duke's Party?`],\n            cook => [Emote.SAD, `I'm afraid not, only the big cheeses get to dine with the Duke.`],\n            player => [Emote.GENERIC, `Well, maybe one day I'll be important enough to sit on the Duke's table.`],\n            cook => [Emote.SKEPTICAL, `Maybe, but I won't be holding my breath.`],\n            execute(() => {\n                questComplete = true;\n            }),\n        ],\n        (subtree, tag_NO_INGREDIENTS) => [\n            player => [Emote.GENERIC, `I haven't got any of them yet, I'm still looking.`],\n            cook => [\n                Emote.SAD,\n                `Please get the ingredients quickly. I'm running out of time! ` + `The Duke will throw me into the streets!`,\n            ],\n            ...youStillNeed(quest),\n        ],\n        (subtree, tag_SOME_INGREDIENTS) => [\n            cook => [\n                Emote.SAD,\n                `Thanks for the ingredients you have got so far, please get the rest quickly. ` +\n                    `I'm running out of time! The Duke will throw me into the streets!`,\n            ],\n            ...youStillNeed(quest),\n        ],\n    );\n\n    await dialogue([player, { npc, key: 'cook' }], dialogueTree);\n\n    if (questComplete) {\n        player.setQuestProgress('rs:cooks_assistant', 'complete');\n    }\n};\n\nexport default {\n    pluginId: 'rs:cooks_assistant_quest',\n    quests: [\n        new Quest({\n            id: 'rs:cooks_assistant',\n            questTabId: 27,\n            name: `Cook's Assistant`,\n            points: 1,\n            journalHandler,\n            onComplete: {\n                questCompleteWidget: {\n                    rewardText: ['300 Cooking XP'],\n                    itemId: 1891,\n                    modelZoom: 240,\n                    modelRotationX: 180,\n                    modelRotationY: 180,\n                },\n                giveRewards: (player: Player): void => player.skills.cooking.addExp(300),\n            },\n        }),\n    ],\n    hooks: [\n        {\n            type: 'npc_interaction',\n            questRequirement: {\n                questId: 'rs:cooks_assistant',\n                stage: 0,\n            },\n            npcs: 'rs:lumbridge_castle_cook',\n            options: 'talk-to',\n            walkTo: true,\n            handler: startQuestAction,\n        },\n        {\n            type: 'npc_interaction',\n            questRequirement: {\n                questId: 'rs:cooks_assistant',\n                stage: 50,\n            },\n            npcs: 'rs:lumbridge_castle_cook',\n            options: 'talk-to',\n            walkTo: true,\n            handler: handInIngredientsAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/quests/goblin-diplomacy-tutorial/goblin-diplomacy-quest.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action';\nimport type { playerInitActionHandler } from '@engine/action/pipe/player-init.action';\nimport { findNpc, widgets } from '@engine/config/config-handler';\nimport type { QuestJournalHandler } from '@engine/config/quest-config';\nimport { questDialogueActionFactory } from '@engine/config/quest-config';\nimport { tabIndex } from '@engine/interface/interface-state';\nimport { activeWorld } from '@engine/world';\nimport { dialogue } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { defaultPlayerTabWidgets } from '@engine/world/actor/player/player';\nimport { Quest } from '@engine/world/actor/player/quest';\nimport { WorldInstance } from '@engine/world/instances';\nimport { Position } from '@engine/world/position';\nimport { updateCombatStyleWidget } from '@plugins/combat/combat-styles.plugin';\nimport { serverConfig } from '@server/game/game-server';\nimport { Subject } from 'rxjs';\nimport { take } from 'rxjs/operators';\nimport { v4 } from 'uuid';\nimport { harlanDialogueHandler } from './melee-tutor-dialogue';\nimport { runescapeGuideDialogueHandler } from './runescape-guide-dialogue';\nimport { goblinDiplomacyStageHandler } from './stage-handler';\n\nexport const tutorialTabWidgetOrder = [\n    [tabIndex['settings'], widgets.settingsTab],\n    [tabIndex['friends'], widgets.friendsList],\n    [tabIndex['ignores'], widgets.ignoreList],\n    [tabIndex['emotes'], widgets.emotesTab],\n    [tabIndex['music'], widgets.musicPlayerTab],\n    [tabIndex['inventory'], widgets.inventory.widgetId],\n    [tabIndex['skills'], widgets.skillsTab],\n    [tabIndex['equipment'], widgets.equipment.widgetId],\n    [tabIndex['combat'], -1],\n    // @TODO prayer, magic,\n];\n\nexport function showTabWidgetHint(\n    player: Player,\n    tabIndex: number,\n    availableTabs: number,\n    finalProgress: number,\n    helpTitle: string,\n    helpText: string,\n): void {\n    const tabClickEvent = {\n        tabIndex,\n        event: new Subject<boolean>(),\n    };\n    player.metadata.tabClickEvent = tabClickEvent;\n\n    dialogue([player], [titled => [helpTitle, helpText]], {\n        permanent: true,\n    });\n\n    unlockAvailableTabs(player, availableTabs);\n    player.outgoingPackets.blinkTabIcon(tabIndex);\n\n    tabClickEvent.event.pipe(take(1)).subscribe(async () => {\n        player.setQuestProgress('tyn:goblin_diplomacy', finalProgress);\n        tabClickEvent.event.complete();\n        delete player.metadata.tabClickEvent;\n        await tutorialHandler(player);\n    });\n}\n\nexport function unlockAvailableTabs(player: Player, availableTabs?: number): void {\n    let doCombatStyleTab = false;\n\n    if (availableTabs === undefined) {\n        availableTabs = tutorialTabWidgetOrder.length;\n    }\n    for (let i = 0; i < availableTabs; i++) {\n        if (tutorialTabWidgetOrder[i][1] === -1) {\n            doCombatStyleTab = true;\n        }\n        player.setSidebarWidget(tutorialTabWidgetOrder[i][0], tutorialTabWidgetOrder[i][1]);\n    }\n\n    if (doCombatStyleTab) {\n        updateCombatStyleWidget(player);\n    }\n}\n\nexport function npcHint(player: Player, npcKey: string | number): void {\n    if (typeof npcKey === 'string') {\n        const npc = findNpc(npcKey);\n        npcKey = npc.gameId;\n    }\n\n    const npc = activeWorld.findNpcsById(npcKey, player.instance.instanceId)[0] || null;\n\n    if (npc) {\n        player.outgoingPackets.showNpcHintIcon(npc);\n    }\n}\n\nexport const startTutorial = async (player: Player): Promise<void> => {\n    player.setQuestProgress('tyn:goblin_diplomacy', 0);\n\n    defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => {\n        if (widgetId !== -1) {\n            player.outgoingPackets.sendTabWidget(tabIndex, widgetId === widgets.logoutTab ? widgetId : null);\n        }\n    });\n\n    player.inventory.add('rs:pot');\n    player.inventory.add('rs:logs');\n    player.inventory.add('rs:bones');\n    player.inventory.add('rs:coins');\n    player.inventory.add('rs:coins');\n    player.inventory.add('rs:coins');\n    player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n\n    await dialogue([player], [titled => [`Getting Started`, `\\nCreate your character!`]], {\n        permanent: true,\n    });\n\n    player.interfaceState.openWidget(widgets.characterDesign, {\n        slot: 'screen',\n    });\n\n    await player.interfaceState.widgetClosed('screen');\n};\n\nexport async function spawnGoblinBoi(player: Player, spawnPoint: 'beginning' | 'end'): Promise<void> {\n    const nearbyGoblins = activeWorld.findNpcsByKey('rs:goblin', player.instance.instanceId);\n    if (nearbyGoblins && nearbyGoblins.length > 0) {\n        // Goblin is already spawned, do nothing\n        return;\n    }\n\n    // Spawn the goblin where it needs to be :)\n    if (spawnPoint === 'beginning') {\n        //const goblin = await world.spawnNpc('rs:goblin', new Position(3219, 3246), 'SOUTH',\n        //    0, player.instance.instanceId);\n        const goblin = await activeWorld.spawnNpc('rs:goblin', new Position(3221, 3257), 'SOUTH', 0, player.instance.instanceId);\n\n        goblin.pathfinding.walkTo(new Position(3219, 3246), {\n            pathingSearchRadius: 16,\n            ignoreDestination: false,\n        });\n    } else {\n        await activeWorld.spawnNpc('rs:goblin', new Position(3219, 3246), 'SOUTH', 0, player.instance.instanceId);\n    }\n}\n\nexport async function tutorialHandler(player: Player): Promise<void> {\n    const progress = player.getQuest('tyn:goblin_diplomacy').progress;\n    const handler = goblinDiplomacyStageHandler[progress];\n\n    defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => {\n        if (widgetId !== -1) {\n            player.setSidebarWidget(tabIndex, widgetId === widgets.logoutTab ? widgetId : null);\n        }\n    });\n\n    if (handler) {\n        player.outgoingPackets.resetNpcHintIcon();\n        await handler(player);\n    }\n}\n\nfunction spawnQuestNpcs(player: Player): void {\n    activeWorld.spawnNpc('rs:runescape_guide', new Position(3230, 3238), 'SOUTH', 2, player.instance.instanceId);\n    activeWorld.spawnNpc('rs:melee_combat_tutor', new Position(3219, 3238), 'EAST', 1, player.instance.instanceId);\n}\n\nconst tutorialInitAction: playerInitActionHandler = async ({ player }) => {\n    if (serverConfig.tutorialEnabled && !player.savedMetadata.tutorialComplete) {\n        player.instance = new WorldInstance(v4());\n        player.metadata.blockObjectInteractions = true;\n        spawnQuestNpcs(player);\n        await tutorialHandler(player);\n    } else {\n        defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => {\n            if (widgetId !== -1) {\n                player.setSidebarWidget(tabIndex, widgetId);\n            }\n        });\n    }\n};\n\nconst trainingSwordEquipAction: equipmentChangeActionHandler = async ({ player, itemDetails }) => {\n    const progress = player.getQuest('tyn:goblin_diplomacy').progress;\n\n    if (progress === 85) {\n        const swordEquipped = player.isItemEquipped('rs:training_sword');\n        const shieldEquipped = player.isItemEquipped('rs:training_shield');\n\n        if ((itemDetails.key === 'rs:training_sword' && shieldEquipped) || (itemDetails.key === 'rs:training_shield' && swordEquipped)) {\n            player.setQuestProgress('tyn:goblin_diplomacy', 90);\n            await tutorialHandler(player);\n        }\n    }\n};\n\nconst createCharacterAction: buttonActionHandler = ({ player }): void => {\n    player.interfaceState.closeAllSlots();\n};\n\nconst journalHandler: QuestJournalHandler = {\n    0: `stinkyu hoomsn HAHA\\n\\n\\nf1nglewuRt`,\n};\n\nconst QUEST_ID = 'tyn:goblin_diplomacy';\n\nconst QUEST = new Quest({\n    id: QUEST_ID,\n    questTabId: 28,\n    name: `Goblin Diplomacy`,\n    points: 1,\n    journalHandler,\n    onComplete: {\n        questCompleteWidget: {\n            rewardText: ['A training sword & shield'],\n            itemId: 9703,\n            modelZoom: 200,\n            modelRotationX: 0,\n            modelRotationY: 180,\n        },\n    },\n});\n\n/**\n * Custom Goblin Diplomacy tutorial quest!\n */\nexport default {\n    pluginId: 'tyn:goblin_diplomacy_quest',\n    quests: [QUEST],\n    hooks: [\n        {\n            type: 'player_init',\n            handler: tutorialInitAction,\n        },\n        {\n            type: 'npc_interaction',\n            handler: questDialogueActionFactory(QUEST_ID, runescapeGuideDialogueHandler, tutorialHandler),\n            npcs: 'rs:runescape_guide',\n            options: 'talk-to',\n            walkTo: true,\n        },\n        {\n            type: 'npc_interaction',\n            handler: questDialogueActionFactory(QUEST_ID, harlanDialogueHandler, tutorialHandler),\n            npcs: 'rs:melee_combat_tutor',\n            options: 'talk-to',\n            walkTo: true,\n        },\n        {\n            type: 'equipment_change',\n            eventType: 'equip',\n            handler: trainingSwordEquipAction,\n            itemIds: [9703, 9704],\n        },\n        {\n            type: 'button',\n            widgetId: widgets.characterDesign,\n            handler: createCharacterAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/quests/goblin-diplomacy-tutorial/melee-tutor-dialogue.ts",
    "content": "import type { QuestDialogueHandler } from '@engine/config/quest-config';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\n\nexport const harlanDialogueHandler: QuestDialogueHandler = {\n    50: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [\n                harlan => [Emote.GENERIC, `Greetings, adventurer. How can I assist you?`],\n                player => [Emote.WONDERING, `The guide in there said you could unlock my inventory and stuff.`],\n                harlan => [Emote.LAUGH, `I suppose I could, yes. But where's the fun in that?`],\n                player => [Emote.SAD, `I just want to fight something.`],\n                harlan => [\n                    Emote.GENERIC,\n                    `I'm sure you'll get the chance, what with all the recent goblin attacks on this side of the River Lum.`,\n                ],\n                harlan => [Emote.GENERIC, `To that end, let me show you your inventory.`],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 55);\n                }),\n            ],\n        );\n    },\n    55: async (player, npc) => {\n        await dialogue([player, { npc, key: 'harlan' }], [harlan => [Emote.GENERIC, `Speak with me once you've opened your inventory.`]]);\n    },\n    60: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [\n                player => [Emote.SAD, `Doesn't look like I had much on me...`],\n                harlan => [Emote.GENERIC, `I would say the goblins likely ran through your pockets before the Guard hauled you in.`],\n                harlan => [Emote.GENERIC, `Lets check out your hitpoints and make sure you're in proper shape after that.`],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 65);\n                }),\n            ],\n        );\n    },\n    65: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [\n                harlan => [\n                    Emote.GENERIC,\n                    `Click on your skills tab to view your hitpoints stat. It should be blinking over near your inventory.`,\n                ],\n            ],\n        );\n    },\n    70: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [\n                harlan => [\n                    Emote.GENERIC,\n                    `You appear to be in good shape. Though with a backpack like that, ` +\n                        `I don't think you'll make much headway against those goblins...`,\n                ],\n                harlan => [Emote.GENERIC, `I'll provide you with some starter equipment - but from there, you're on your own.`],\n                harlan => [Emote.GENERIC, `But before I can do that, you'll need to open your Equipment tab.`],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 75);\n                }),\n            ],\n        );\n    },\n    75: async (player, npc) => {\n        await dialogue([player, { npc, key: 'harlan' }], [harlan => [Emote.GENERIC, `Speak with me once you've opened your equipment.`]]);\n    },\n    80: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [\n                harlan => [Emote.GENERIC, `Have this training equipment and try it on.`],\n                execute(() => {\n                    player.inventory.add('rs:training_sword');\n                    player.inventory.add('rs:training_shield');\n                    player.setQuestProgress('tyn:goblin_diplomacy', 85);\n                }),\n                text => `Harlan hands you a Training sword and shield.`,\n            ],\n        );\n    },\n    85: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'harlan' }],\n            [harlan => [Emote.GENERIC, `Try on the Training sword and shield and we can continue.`]],\n        );\n    },\n};\n"
  },
  {
    "path": "src/plugins/quests/goblin-diplomacy-tutorial/runescape-guide-dialogue.ts",
    "content": "import type { QuestDialogueHandler } from '@engine/config/quest-config';\nimport { Emote, dialogue, execute } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { defaultPlayerTabWidgets } from '@engine/world/actor/player/player';\nimport { updateCombatStyleWidget } from '@plugins/combat/combat-styles.plugin';\n\nexport const runescapeGuideDialogueHandler: QuestDialogueHandler = {\n    5: async (player: Player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [\n                guide => [Emote.GENERIC, `Greetings adventurer, welcome to RuneScape.`],\n                player => [Emote.SKEPTICAL, `How did I get here?...`],\n                guide => [Emote.GENERIC, `Seems like a goblin smacked you over the head on your way in. Nasty little things.`],\n                player => [Emote.DROWZY, `I guess that explains the headache.`],\n                guide => [Emote.GENERIC, `I would imagine so. Now, it's my job here is to show new players around.`],\n                options => [\n                    `Go on.`,\n                    [\n                        player => [Emote.GENERIC, `Carry on then.`],\n                        guide => [\n                            Emote.GENERIC,\n                            `We'll start with the Options menu. Click on the blinking spanner icon at the bottom right of your game screen.`,\n                        ],\n                        execute(() => {\n                            player.setQuestProgress('tyn:goblin_diplomacy', 10);\n                        }),\n                    ],\n                    `I know how the game works already.`,\n                    [\n                        player => [Emote.HAPPY, `This isn't my first time here, I'm good.`],\n                        guide => [Emote.HAPPY, `Oh good, I won't tell you what you already know then.`],\n                        execute(() => {\n                            player.savedMetadata.tutorialComplete = true;\n                            player.setQuestProgress('tyn:goblin_diplomacy', 'complete');\n                            player.instance = null;\n                            defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => {\n                                if (widgetId !== -1) {\n                                    player.setSidebarWidget(tabIndex, widgetId);\n                                }\n                            });\n                            updateCombatStyleWidget(player);\n                            player.metadata.blockObjectInteractions = false;\n                        }),\n                    ],\n                ],\n            ],\n        );\n    },\n    10: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [guide => [Emote.GENERIC, 'Please click on the blinking spanner icon, then we can continue.']],\n        );\n    },\n    15: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [\n                guide => [\n                    Emote.HAPPY,\n                    `Next we'll move on to the more social side of things. Click on the blinking icon to learn about the Friends List.`,\n                ],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 20);\n                }),\n            ],\n        );\n    },\n    20: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]],\n        );\n    },\n    25: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]],\n        );\n    },\n    30: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]],\n        );\n    },\n    35: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [\n                player => [Emote.HAPPY, `I've gone through the Friends List and everything. When do I get to kill things?`],\n                guide => [Emote.LAUGH, `All in good time, ${player.username}. We've a little more to discuss yet - like music!`],\n                player => [Emote.SKEPTICAL, `Music? Doesn't everyone turn that off?`],\n                guide => [Emote.SAD, `Some people find it nostalgic.`],\n                player => [Emote.SAD, `Sorry, go on...`],\n                guide => [Emote.GENERIC, `Yes... As I was saying... Music can be accessed from the music tab.`],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 40);\n                }),\n            ],\n        );\n    },\n    45: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [\n                player => [Emote.HAPPY, `That music tab is pretty nostalgic, I'll give ya that.`],\n                guide => [Emote.LAUGH, `Isn't it? Sometimes I can hear Harmony in my sleep.`],\n                player => [Emote.WONDERING, `So what's next?`],\n                guide => [Emote.GENERIC, `Next you'll be moving on to my friend Harlan, to learn about your inventory and skills.`],\n                player => [Emote.HAPPY, `So I finally get to kill something?`],\n                guide => [Emote.SAD, `That's all you adventurers ever want to do...`],\n                guide => [\n                    Emote.GENERIC,\n                    `Oh well. Head on over to Harlan, the melee combat tutor, and I'm sure he'll show you how to kill something.`,\n                ],\n                execute(() => {\n                    player.setQuestProgress('tyn:goblin_diplomacy', 50);\n                }),\n            ],\n        );\n    },\n    50: async (player, npc) => {\n        await dialogue(\n            [player, { npc, key: 'guide' }],\n            [guide => [Emote.GENERIC, `Please speak to my friend Harlan, the melee combat tutor, to continue.`]],\n        );\n    },\n};\n"
  },
  {
    "path": "src/plugins/quests/goblin-diplomacy-tutorial/stage-handler.ts",
    "content": "import { findNpc } from '@engine/config/config-handler';\nimport type { QuestStageHandler } from '@engine/config/quest-config';\nimport { tabIndex } from '@engine/interface/interface-state';\nimport { activeWorld } from '@engine/world';\nimport { dialogue } from '@engine/world/actor/dialogue';\nimport { Cutscene } from '@engine/world/actor/player/cutscenes';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { schedule } from '@engine/world/task';\nimport {\n    npcHint,\n    showTabWidgetHint,\n    spawnGoblinBoi,\n    startTutorial,\n    tutorialHandler,\n    unlockAvailableTabs,\n} from '@plugins/quests/goblin-diplomacy-tutorial/goblin-diplomacy-quest.plugin';\n\nexport const goblinDiplomacyStageHandler: QuestStageHandler = {\n    0: async player => {\n        await startTutorial(player);\n        player.setQuestProgress('tyn:goblin_diplomacy', 5);\n        await tutorialHandler(player);\n    },\n    5: async player => {\n        npcHint(player, 'rs:runescape_guide');\n\n        await dialogue([player], [titled => [`Getting Started`, `\\nWelcome to RuneScape!\\nSpeak with the Guide to begin your journey.`]], {\n            permanent: true,\n        });\n    },\n    10: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['settings'],\n            1,\n            15,\n            `Game Options`,\n            `The Options menu can be used to modify various game settings.\\n` +\n                `Click the blinking icon to open the Options menu.\\n\\n` +\n                `When you're finished, speak with the Guide to continue.`,\n        );\n    },\n    15: player => {\n        npcHint(player, 'rs:runescape_guide');\n\n        unlockAvailableTabs(player, 1);\n\n        dialogue([player], [titled => [`Getting Started`, `\\nSpeak with the Guide to continue.`]], {\n            permanent: true,\n        });\n    },\n    20: player => {\n        showTabWidgetHint(player, tabIndex['friends'], 2, 25, `Friends List`, `\\nKeep track of your friends via the Friends List.`);\n    },\n    25: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['ignores'],\n            3,\n            30,\n            `Ignore List`,\n            `\\nThe Ignore List allows you to block messages from other users.\\n` +\n                `Check it out by clicking the blinking icon at the bottom right.`,\n        );\n    },\n    30: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['emotes'],\n            4,\n            35,\n            `Emotes`,\n            `Perform emotes for other players via the Emotes tab.\\n\\n` +\n                `Click on the blinking Emotes tab to see the list of emotes you can perform, then speak with the Guide to continue.`,\n        );\n    },\n    35: player => {\n        npcHint(player, 'rs:runescape_guide');\n\n        unlockAvailableTabs(player, 4);\n\n        dialogue([player], [titled => [`Continue`, `\\nSpeak with the Guide to continue.`]], {\n            permanent: true,\n        });\n    },\n    40: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['music'],\n            5,\n            45,\n            `Music`,\n            `Check out the music tab to view and play all of your favorite RuneScape tracks!\\n` + `Once you've unlocked them, of course.`,\n        );\n    },\n    45: player => {\n        npcHint(player, 'rs:runescape_guide');\n        unlockAvailableTabs(player, 5);\n\n        dialogue([player], [titled => [`Continue`, `\\nSpeak with the Guide to continue.`]], {\n            permanent: true,\n        });\n    },\n    50: player => {\n        player.metadata.blockObjectInteractions = false;\n        npcHint(player, 'rs:melee_combat_tutor');\n        unlockAvailableTabs(player, 5);\n\n        dialogue([player], [titled => [`Continue`, `\\nSpeak with the Melee Combat Tutor to continue.`]], {\n            permanent: true,\n        });\n    },\n    55: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['inventory'],\n            6,\n            60,\n            `Inventory`,\n            `Your inventory contains any items held on your person that aren't equipped. ` +\n                `Click the blinking backpack icon to open your inventory.`,\n        );\n    },\n    60: player => {\n        npcHint(player, 'rs:melee_combat_tutor');\n        unlockAvailableTabs(player, 6);\n\n        dialogue([player], [titled => [`Continue`, `\\nTalk-to the Melee Combat Tutor to continue.`]], {\n            permanent: true,\n        });\n    },\n    65: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['skills'],\n            7,\n            70,\n            `Skills`,\n            `You can see your character's skill levels on the Skills tab, including your current number of hitpoints. ` +\n                `If your hitpoints ever reach zero, you'll die - so be careful!`,\n        );\n    },\n    70: player => {\n        npcHint(player, 'rs:melee_combat_tutor');\n        unlockAvailableTabs(player, 7);\n\n        dialogue([player], [titled => [`Continue`, `\\nTalk-to the Melee Combat Tutor to continue.`]], {\n            permanent: true,\n        });\n    },\n    75: player => {\n        showTabWidgetHint(\n            player,\n            tabIndex['equipment'],\n            8,\n            80,\n            `Equipment`,\n            `The equipment tab contains details on everything you have equipped, as well as any stat ` +\n                `bonuses received from your equipment.`,\n        );\n    },\n    80: player => {\n        npcHint(player, 'rs:melee_combat_tutor');\n        unlockAvailableTabs(player, 8);\n\n        dialogue([player], [titled => [`Continue`, `\\nTalk-to the Melee Combat Tutor to continue.`]], {\n            permanent: true,\n        });\n    },\n    85: player => {\n        unlockAvailableTabs(player, 8);\n\n        dialogue([player], [titled => [`Continue`, `\\nEquip the Training sword and shield.`]], {\n            permanent: true,\n        });\n    },\n    90: async player => {\n        npcHint(player, 'rs:melee_combat_tutor');\n        unlockAvailableTabs(player, 8);\n\n        dialogue([player], [titled => [`Continue`, `\\nTalk-to the Melee Combat Tutor to continue.`]], {\n            permanent: true,\n        });\n\n        // @TODO vvv this is all placeholder code for the cutscene that will be needed later :)\n        await spawnGoblinBoi(player, 'beginning');\n\n        await schedule(10);\n\n        const cameraX = 3219;\n        const cameraY = 3240;\n        const cameraHeight = 320;\n        const lookX = 3219;\n        const lookY = 3246;\n        const lookHeight = 300;\n        const speed = 0;\n        const acceleration = 64;\n\n        player.cutscene = new Cutscene(player);\n        player.cutscene.snapCameraTo(cameraX, cameraY, cameraHeight, speed, acceleration);\n        player.cutscene.lookAt(lookX, lookY, lookHeight, speed, acceleration);\n\n        await schedule(3);\n\n        function getAnim() {\n            const goblinDetails = findNpc('rs:goblin');\n            const anims = goblinDetails.combatAnimations;\n\n            if (!anims) {\n                return null;\n            }\n\n            if (!anims.attack) {\n                return null;\n            }\n\n            if (Array.isArray(anims.attack)) {\n                return anims.attack[0];\n            }\n\n            return anims.attack;\n        }\n\n        const goblinAnim = getAnim();\n\n        if (goblinAnim !== null) {\n            activeWorld.findNpcsByKey('rs:goblin', player.instance.instanceId)[0].playAnimation(goblinAnim);\n        }\n\n        player.playSound(soundIds.npc.human.maleDefence, 5);\n    },\n};\n"
  },
  {
    "path": "src/plugins/quests/quest-journal.plugin.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { QuestKey } from '@engine/config/quest-config';\nimport { questMap } from '@engine/plugins/loader';\nimport { wrapText } from '@engine/util/strings';\nimport type { Quest } from '@engine/world/actor/player/quest';\n\nexport const handler: buttonActionHandler = async ({ player, buttonId }) => {\n    const quest = Object.values<Quest>(questMap).find(quest => quest.questTabId === buttonId);\n    if (!quest) {\n        return;\n    }\n\n    const [playerQuest] = player.quests.filter(playerQuest => playerQuest.questId === quest.id);\n\n    let playerStage: QuestKey = 0;\n    if (playerQuest && playerQuest.progress !== undefined) {\n        playerStage = playerQuest.progress;\n    }\n\n    const journalHandler = quest.journalHandler[playerStage];\n    if (journalHandler === undefined) {\n        const questJournalStages = Object.keys(quest.journalHandler);\n        let journalEntry;\n        for (const stage of questJournalStages) {\n            const stageNum = parseInt(stage, 10);\n            if (isNaN(stageNum) || playerStage === 'complete') {\n                continue;\n            }\n\n            if (stageNum <= playerStage) {\n                journalEntry = stage;\n            } else {\n                break;\n            }\n        }\n    }\n\n    const color = 128;\n    let text: string = '';\n\n    if (typeof journalHandler === 'function') {\n        text = await Promise.resolve(journalHandler(player));\n    } else if (typeof journalHandler === 'string') {\n        text = journalHandler;\n    }\n\n    let lines: string[];\n    if (text) {\n        lines = wrapText(text as string, 395);\n    } else {\n        lines = ['Invalid Quest Stage'];\n    }\n\n    player.modifyWidget(widgets.questJournal, { childId: 2, text: '@dre@' + quest.name });\n\n    for (let i = 0; i <= 100; i++) {\n        if (i === 0) {\n            player.modifyWidget(widgets.questJournal, { childId: 3, text: `<col=${color}>${lines[0]}</col>` });\n            continue;\n        }\n\n        if (lines.length > i) {\n            player.modifyWidget(widgets.questJournal, { childId: i + 4, text: `<col=${color}>${lines[i]}</col>` });\n        } else {\n            player.modifyWidget(widgets.questJournal, { childId: i + 4, text: '' });\n        }\n    }\n\n    player.interfaceState.openWidget(widgets.questJournal, {\n        slot: 'screen',\n        multi: false,\n    });\n};\n\nexport default {\n    pluginId: 'rs:quest_journal',\n    hooks: [{ type: 'button', widgetId: widgets.questTab, handler }],\n};\n"
  },
  {
    "path": "src/plugins/quests/witchs-potion-quest.plugin.ts",
    "content": "import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport type { QuestJournalHandler } from '@engine/config/quest-config';\nimport { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Quest } from '@engine/world/actor/player/quest';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { Position } from '@engine/world/position';\n\nconst journalHandler: QuestJournalHandler = {\n    0: `<col=000080>I can start this quest by speaking to <col=800000>Hetty<col=000080> in her house in\n<col=800000>Rimmington<col=000080>, West of <col=800000>Port Sarim`,\n\n    50: player => {\n        let questLog = `<str><col=000000>I spoke to Hetty in her house at Rimmington. Hetty told me</str>\n        <str>she could increase my magic power if I can bring her</str>\n        <str>certain ingredients for a potion.</str>\n        <col=000080>Hetty needs me to bring her the following:`;\n\n        const questLogIngredientData = [\n            {\n                itemId: itemIds.witchesPotion.ratsTail,\n                haveText: `<str><col=000000>I have a rat's tail with me,</str>`,\n                dontHaveText: `<col=800000>A rat's tail<col=000080>, I could get one from a small rat.`,\n            },\n            {\n                itemId: itemIds.witchesPotion.burntMeat,\n                haveText: `<str><col=000000>I have a piece of burnt meat with me,</str>`,\n                dontHaveText: `<col=800000>A piece of burnt meat<col=000080>, I could thoroughly cook a piece of\n        <col=000080>raw beef.`,\n            },\n            {\n                itemId: itemIds.witchesPotion.onion,\n                haveText: `<str><col=000000>I have an onion with me,</str>`,\n                dontHaveText: `<col=800000>An onion<col=000080>, I could probably find one at a farm.`,\n            },\n            {\n                itemId: itemIds.witchesPotion.eyeOfNewt,\n                haveText: `<str><col=000000>I have an eye of newt with me,</str>`,\n                dontHaveText: `<col=800000>An eye of newt<col=000080>, maybe the <col=800000>Magic shop<col=000080> in <col=800000>Port Sarim<col=000080> would\n        <col=000080>sell me this?`,\n            },\n        ];\n\n        let ingredientsObtained = 0;\n        for (const ingredient of questLogIngredientData) {\n            questLog += `\\n`;\n            if (player.hasItemInInventory(ingredient.itemId)) {\n                questLog += ingredient.haveText;\n                ingredientsObtained++;\n            } else {\n                questLog += ingredient.dontHaveText;\n            }\n        }\n\n        if (ingredientsObtained === 4) {\n            questLog += `\\n<col=000080>I should bring these ingredients to <col=800000>Hetty<col=000080>.`;\n        }\n\n        return questLog;\n    },\n\n    75: `<col=000000><str>I brought her an onion, a rat's tail, a piece of burnt meat</str>\n    <str>and an eye of newt which she used to make a potion.</str>\n    <col=000080>I should drink from the <col=800000>cauldron<col=000080> and improve my magic!`,\n\n    complete: `<col=000000><str>I brought her an onion, a rat's tail, a piece of burnt meat\n        <str>and an eye of newt which she used to make a potion.\\n\n        <str>I drank from the cauldron and my magic power increased!\\n\n        <col=ff0000>QUEST COMPLETE!</col>`,\n};\n\nconst drinkThePotionDialogue: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    player.face(npc);\n    dialogue(\n        [player, { npc, key: 'hetty' }],\n        [hetty => [Emote.ANGRY, `Well are you going to drink the potion or not?`], player => [Emote.GENERIC, `Yes, I will.`]],\n    );\n};\n\nconst drinkFromCauldron: objectInteractionActionHandler = async details => {\n    const { player, object } = details;\n\n    let questComplete = false;\n    player.face(new Position(object.x, object.y));\n    await dialogue(\n        [player],\n        [\n            text => `You drink from the cauldron. It tastes horrible!\\nYou feel yourself imbued with power.`,\n            execute(() => {\n                questComplete = true;\n            }),\n        ],\n    );\n\n    if (questComplete) {\n        player.setQuestProgress('rs:witchs_potion', `complete`);\n    }\n};\n\nconst attemptToDrinkBeforeAllowed: objectInteractionActionHandler = async details => {\n    const { player, object } = details;\n    player.face(new Position(object.x, object.y));\n    await dialogue([player], [player => [Emote.GENERIC, `As nice as that looks I think I'll give it a miss for now.`]]);\n};\n\nconst dialogueIngredientQuestions: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n\n    const hasRatsTail = player.hasItemInInventory(itemIds.witchesPotion.ratsTail);\n    const hasBurntMeat = player.hasItemInInventory(itemIds.witchesPotion.burntMeat);\n    const hasOnion = player.hasItemInInventory(itemIds.witchesPotion.onion);\n    const hasEyeOfNewt = player.hasItemInInventory(itemIds.witchesPotion.eyeOfNewt);\n\n    const ingredients = [\n        {\n            itemId: itemIds.witchesPotion.ratsTail,\n            haveText: `I have the rat's tail (ewww), `,\n            dontHaveText: `I don't have a rat's tail, `,\n        },\n        {\n            itemId: itemIds.witchesPotion.burntMeat,\n            haveText: `I have the burnt meat, `,\n            dontHaveText: `I don't have any burnt meat, `,\n        },\n        {\n            itemId: itemIds.witchesPotion.onion,\n            haveText: `I have an onion, and `,\n            dontHaveText: `I don't have an onion, and `,\n        },\n        {\n            itemId: itemIds.witchesPotion.eyeOfNewt,\n            haveText: `I have the eye of newt, yum!`,\n            dontHaveText: `I don't have an eye of newt.`,\n        },\n    ];\n\n    let requirementsDialogue = ``;\n    for (const ingredient of ingredients) {\n        if (player.hasItemInInventory(ingredient.itemId)) {\n            requirementsDialogue += ingredient.haveText;\n        } else {\n            requirementsDialogue += ingredient.dontHaveText;\n        }\n    }\n\n    dialogue(\n        [player, { npc, key: 'hetty' }],\n        [\n            goto(() => {\n                const count = [hasRatsTail, hasEyeOfNewt, hasOnion, hasBurntMeat].filter(value => value === true).length;\n                if (count === 4) {\n                    return 'tag_ALL_INGREDIENTS';\n                } else if (count === 0) {\n                    return 'tag_NO_INGREDIENTS';\n                } else {\n                    return 'tag_SOME_INGREDIENTS';\n                }\n            }),\n            (subtree, tag_ALL_INGREDIENTS) => [\n                hetty => [Emote.HAPPY, `So have you found the things for the potion?`],\n                player => [Emote.HAPPY, `Yes I have everything!`],\n                hetty => [Emote.HAPPY, `Excellent, can I have them then?`],\n                (text, tag_has_ingredients) =>\n                    `You pass the ingredients to Hetty and she puts them all into her cauldron. Hetty closes her eyes and begins to chant. The cauldron bubbles mysteriously.`,\n                player => [Emote.GENERIC, `Well, is it ready?`],\n                execute(() => {\n                    player.removeFirstItem(itemIds.witchesPotion.ratsTail);\n                    player.removeFirstItem(itemIds.witchesPotion.onion);\n                    player.removeFirstItem(itemIds.witchesPotion.burntMeat);\n                    player.removeFirstItem(itemIds.witchesPotion.eyeOfNewt);\n                    player.setQuestProgress(`rs:witchs_potion`, 75);\n                }),\n                hetty => [Emote.HAPPY, `Ok, now drink from the cauldron.`],\n            ],\n            (subtree, tag_NO_INGREDIENTS) => [\n                player => [Emote.HAPPY, `I've been looking for those ingredients.`],\n                hetty => [Emote.HAPPY, `So what have you found so far?`],\n                player => [Emote.GENERIC, `I'm afraid I don't have any of them yet.`],\n                hetty => [\n                    Emote.SAD,\n                    `Well I can't make the potion without them! Remember... You need an eye of newt, a rat's tail, an onion, and a piece of burnt meat. Off you go dear!`,\n                ],\n            ],\n            (subtree, tag_SOME_INGREDIENTS) => [\n                player => [Emote.HAPPY, `I've been looking for those ingredients.`],\n                hetty => [Emote.HAPPY, `So what have you found so far?`],\n                player => [Emote.GENERIC, requirementsDialogue],\n                hetty => [Emote.GENERIC, `Great, but I'll need the other ingredients as well.`],\n            ],\n        ],\n    );\n};\n\nconst afterQuestDialogue: npcInteractionActionHandler = details => {\n    const { player, npc } = details;\n    player.face(npc);\n\n    dialogue(\n        [player, { npc, key: 'hetty' }],\n        [\n            hetty => [Emote.HAPPY, `How's your magic coming along?`],\n            player => [Emote.HAPPY, `I'm practicing and slowly getting better.`],\n            hetty => [Emote.HAPPY, `Good, good.`],\n        ],\n    );\n};\n\nconst startQuestAction: npcInteractionActionHandler = async details => {\n    const { player, npc } = details;\n    player.face(npc);\n    npc.face(player);\n    let beginQuest = false;\n    await dialogue(\n        [player, { npc, key: 'hetty' }],\n        [\n            hetty => [Emote.WONDERING, 'What could you want with an old woman like me?'],\n            options => [\n                `I am in search of a quest.`,\n                [\n                    (player, tag_search_of_quest) => [Emote.GENERIC, `I am in search of a quest.`],\n                    hetty => [Emote.HAPPY, `Hmmm... Maybe I can think of something for you.`],\n                    hetty => [Emote.HAPPY, `Would you like to become more proficient in the dark arts?`],\n\n                    options => [\n                        `Yes, help me become one with my darker side.`,\n                        [player => [Emote.HAPPY, `Yes, help me become one with my darker side.`], goto(`tag_darker_side`)],\n                        `No, I have my principles and honour.`,\n                        [\n                            player => [Emote.GENERIC, `No, I have my principles and honour.`],\n                            hetty => [Emote.SAD, `Suit yourself, but you're missing out.`],\n                        ],\n                        `What, you mean improve my magic?`,\n                        [\n                            player => [Emote.SAD, 'What, you mean improve my magic?'],\n                            text => `The witch sighs.`,\n                            hetty => [Emote.GENERIC, 'Yes, improve your magic...'],\n                            hetty => [Emote.SAD, 'Do you have no sense of drama?'],\n                            options => [\n                                `Yes, I'd like to improve my magic.`,\n                                [\n                                    player => [Emote.GENERIC, `Yes, I'd like to improve my magic.`],\n                                    (hetty, tag_darker_side) => [\n                                        Emote.HAPPY,\n                                        `Okay, I'm going to make a potion to help bring out your darker self.`,\n                                    ],\n                                    hetty => [Emote.GENERIC, `You will need certain ingredients.`],\n                                    player => [Emote.GENERIC, `What do I need?`],\n                                    execute(() => {\n                                        beginQuest = true;\n                                    }),\n                                    hetty => [\n                                        Emote.WONDERING,\n                                        `You need an eye of newt, a rat's tail, an onion... Oh and a piece of burnt meat.`,\n                                    ],\n                                    player => [Emote.HAPPY, `Great, I'll go and get them.`],\n                                ],\n                                `No, I'm not interested.`,\n                                [\n                                    player => [Emote.SAD, `No, I'm not interested.`],\n                                    hetty => [Emote.SAD, `Many aren't to start off with.`],\n                                    text => `The witch smiles mysteriously.`,\n                                    hetty => [Emote.GENERIC, `But I think you'll be drawn back to this place.`],\n                                ],\n                                `Show me the mysteries of the dark arts...`,\n                                [player => [Emote.GENERIC, `Show me the mysteries of the dark arts...`], goto(`tag_darker_side`)],\n                            ],\n                        ],\n                    ],\n                ],\n                `I've heard that you are a witch.`,\n                [\n                    player => [Emote.HAPPY, `I've heard that you are a witch.`],\n                    hetty => [Emote.HAPPY, `Yes it does seem to be getting fairly common knowledge.`],\n                    hetty => [Emote.WORRIED, `I fear I may get a visit from the witch hunters of Falador before long.`],\n                    options => [\n                        `I am in search of a quest.`,\n                        [goto('tag_search_of_quest')],\n                        `Goodbye.`,\n                        [player => [Emote.VERY_SAD, `Goodbye.`]],\n                    ],\n                ],\n            ],\n        ],\n    );\n    if (beginQuest) {\n        player.setQuestProgress('rs:witchs_potion', 50);\n    }\n};\n\nexport default {\n    pluginId: 'rs:witchs_potion_quest',\n    quests: [\n        new Quest({\n            id: 'rs:witchs_potion',\n            questTabId: 42,\n            name: `Witch's Potion`,\n            points: 1,\n            journalHandler,\n            onComplete: {\n                questCompleteWidget: {\n                    rewardText: ['325 Magic XP'],\n                    itemId: 221,\n                    modelZoom: 240,\n                    modelRotationX: 180,\n                    modelRotationY: 180,\n                },\n                giveRewards: (player: Player): void => player.skills.magic.addExp(325),\n            },\n        }),\n    ],\n    hooks: [\n        {\n            type: 'npc_interaction',\n            npcs: 'rs:hetty',\n            options: 'talk-to',\n            walkTo: true,\n            handler: startQuestAction,\n        },\n        {\n            type: 'npc_interaction',\n            questRequirement: {\n                questId: 'rs:witchs_potion',\n                stage: 50,\n            },\n            npcs: 'rs:hetty',\n            options: 'talk-to',\n            walkTo: true,\n            handler: dialogueIngredientQuestions,\n        },\n        {\n            type: 'npc_interaction',\n            questRequirement: {\n                questId: 'rs:witchs_potion',\n                stage: 75,\n            },\n            npcs: 'rs:hetty',\n            options: 'talk-to',\n            walkTo: true,\n            handler: drinkThePotionDialogue,\n        },\n        {\n            type: 'npc_interaction',\n            questRequirement: {\n                questId: 'rs:witchs_potion',\n                stage: 'complete',\n            },\n            npcs: 'rs:hetty',\n            options: 'talk-to',\n            walkTo: true,\n            handler: afterQuestDialogue,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: 2024,\n            questRequirement: {\n                questId: 'rs:witchs_potion',\n                stage: 75,\n            },\n            options: 'drink from',\n            walkTo: true,\n            handler: drinkFromCauldron,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: 2024,\n            options: 'drink from',\n            walkTo: true,\n            handler: attemptToDrinkBeforeAllowed,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/construction/con-constants.ts",
    "content": "import { Position } from '@engine/world/position';\n\nexport const MAP_SIZE = 13;\n\nexport type RoomType =\n    | 'empty'\n    | 'empty_grass'\n    | 'garden'\n    | 'formal_garden'\n    | 'parlor'\n    | 'kitchen'\n    | 'dining_room'\n    | 'bedroom'\n    | 'skill_hall'\n    | 'quest_hall'\n    | 'portal_chamber'\n    | 'combat_room'\n    | 'games_room'\n    | 'treasure_room'\n    | 'chapel'\n    | 'study'\n    | 'throne_room'\n    | 'workshop'\n    | 'oubliette'\n    | 'costume_room';\n\nexport const RoomStyle = {\n    basic_wood: 0,\n    basic_stone: 1,\n    whitewashed_stone: 2,\n    fremennik_wood: 3,\n    tropical_wood: 4,\n    fancy_stone: 5,\n};\n\n/**\n * A map of room types to their respective world map template positions within the game.\n */\nexport const roomTemplates: { [key in RoomType]: Position } = {\n    empty: new Position(1856, 5056),\n    empty_grass: new Position(1864, 5056),\n    garden: new Position(1856, 5064),\n    formal_garden: new Position(1872, 5064),\n    parlor: new Position(1856, 5112),\n    kitchen: new Position(1872, 5112),\n    dining_room: new Position(1888, 5112),\n    bedroom: new Position(1904, 5112),\n    skill_hall: new Position(1864, 5104),\n    quest_hall: new Position(1912, 5104),\n    portal_chamber: new Position(1864, 5088),\n    combat_room: new Position(1880, 5088),\n    games_room: new Position(1896, 5088),\n    treasure_room: new Position(1912, 5088),\n    chapel: new Position(1872, 5096),\n    study: new Position(1888, 5096),\n    throne_room: new Position(1904, 5096),\n    workshop: new Position(1856, 5096),\n    oubliette: new Position(1904, 5080),\n    costume_room: new Position(1904, 5064, 0),\n};\n\n/**\n * A map of room builder widget button ids to their respective room types.\n */\nexport const roomBuilderButtonMap: { [key: number]: RoomType } = {\n    160: 'parlor',\n    161: 'garden',\n    162: 'kitchen',\n    163: 'dining_room',\n    164: 'workshop',\n    165: 'bedroom',\n    166: 'skill_hall',\n    167: 'games_room',\n    168: 'combat_room',\n    169: 'quest_hall',\n    170: 'study',\n    171: 'costume_room',\n    172: 'chapel',\n    173: 'portal_chamber',\n    174: 'formal_garden',\n    175: 'throne_room',\n    176: 'oubliette',\n    177: 'treasure_room', // @TODO dungeon corridor\n    178: 'treasure_room', // @TODO dungeon junction\n    179: 'treasure_room', // @TODO dungeon stair\n    180: 'treasure_room',\n};\n\nexport const instance1 = new Position(6400, 6400);\nexport const instance1PohSpawn = new Position(6400 + 36, 6400 + 36);\nexport const instance1Max = new Position(6400 + 64, 6400 + 64);\nexport const instance2 = new Position(6400, 6464);\nexport const instance2PohSpawn = new Position(6400 + 36, 6464 + 36); // for reference\nexport const instance2Max = new Position(6400 + 64, 6464 + 64);\n\n// Standard home outer door ids: closed[13100, 13101], open[13102, 13103]\n"
  },
  {
    "path": "src/plugins/skills/construction/home-saver.ts",
    "content": "import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ConstructedRegion } from '@engine/world/map/region';\nimport type { Room } from '@plugins/skills/construction/house';\nimport { House } from '@plugins/skills/construction/house';\nimport { logger } from '@runejs/common';\nimport JSON5 from 'json5';\n\n/**\n * Gets the PoH save file name for the given player.\n * @param player\n */\nconst getSaveFileName = (player: Player): string => `${player.username.toLowerCase().replace(/ /g, '_')}.json5`;\n\n/**\n * Loads and returns the given player's PoH as a ConstructedRegion object.\n * Returns null if the player does not have a house.\n * @param player\n */\nexport const loadHouse = (player: Player): House | null => {\n    const houseSaveDir = join('data', 'houses');\n\n    if (!existsSync(houseSaveDir)) {\n        mkdirSync(houseSaveDir);\n        return null;\n    }\n\n    const filePath = join(houseSaveDir, getSaveFileName(player));\n\n    try {\n        const customMapFile = readFileSync(filePath, 'utf-8');\n        if (!customMapFile) {\n            return null;\n        }\n\n        const customMap = JSON5.parse(customMapFile);\n        if (!customMap) {\n            return null;\n        }\n\n        const loadedHouse = customMap as House;\n        const house = new House();\n        house.copyRooms(loadedHouse.rooms);\n        return house;\n    } catch (error) {\n        logger.error(`Error loading player house for ${player.username}.`);\n        logger.error(error);\n        return null;\n    }\n};\n\n/**\n * Saves the given player's house as a JSON5 file.\n * @param player\n */\nexport const saveHouse = (player: Player): void => {\n    const customMap = player.metadata.customMap as ConstructedRegion;\n    if (!customMap) {\n        return;\n    }\n\n    const houseSaveDir = join('data', 'houses');\n\n    if (!existsSync(houseSaveDir)) {\n        mkdirSync(houseSaveDir);\n    }\n\n    const filePath = join(houseSaveDir, getSaveFileName(player));\n\n    const house = new House();\n    house.rooms = customMap.chunks as Room[][][];\n\n    try {\n        writeFileSync(filePath, JSON5.stringify(house, null, 4));\n    } catch (error) {\n        logger.error(`Error saving player house for ${player.username}.`);\n        logger.error(error);\n    }\n};\n"
  },
  {
    "path": "src/plugins/skills/construction/house.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { ConstructedRegion } from '@engine/world/map/region';\nimport { ConstructedChunk } from '@engine/world/map/region';\nimport type { Position } from '@engine/world/position';\nimport type { RoomType } from '@plugins/skills/construction/con-constants';\nimport {\n    MAP_SIZE,\n    instance1,\n    instance1Max,\n    instance1PohSpawn,\n    instance2,\n    instance2Max,\n    roomTemplates,\n} from '@plugins/skills/construction/con-constants';\nimport { loadHouse } from '@plugins/skills/construction/home-saver';\n\nexport const openHouse = (player: Player): void => {\n    let pohPosition: Position = instance1;\n    let playerSpawn: Position = instance1PohSpawn;\n\n    if (player.position.within(instance1, instance1Max, false)) {\n        playerSpawn = player.position.copy().setY(player.position.y + 64);\n        pohPosition = instance2;\n    } else if (player.position.within(instance2, instance2Max, false)) {\n        playerSpawn = player.position.copy().setY(player.position.y - 64);\n    }\n\n    const playerHouse = loadHouse(player);\n\n    if (playerHouse) {\n        player.metadata.customMap = {\n            renderPosition: pohPosition,\n            chunks: playerHouse.rooms,\n        } as ConstructedRegion;\n    }\n\n    player.teleport(playerSpawn);\n\n    if (!player.metadata.customMap) {\n        const house = new House();\n        house.rooms[0][6][6] = new Room('garden');\n\n        player.metadata.customMap = {\n            renderPosition: pohPosition,\n            chunks: house.rooms,\n        } as ConstructedRegion;\n    } else {\n        player.metadata.customMap.renderPosition = pohPosition;\n    }\n\n    for (let plane = 0; plane < 3; plane++) {\n        for (let chunkX = 0; chunkX < 13; chunkX++) {\n            for (let chunkY = 0; chunkY < 13; chunkY++) {\n                const room = player.metadata.customMap.chunks[plane][chunkX][chunkY];\n                if (!room) {\n                    continue;\n                }\n\n                const templatePosition = room.templatePosition;\n\n                // load all the PoH template maps into memory so that their collision maps are generated\n                activeWorld.chunkManager.getChunk(templatePosition);\n            }\n        }\n    }\n\n    player.sendMessage(`Welcome home.`);\n};\n\nexport class House {\n    public rooms: (Room | null)[][][];\n\n    public constructor() {\n        this.rooms = new Array(4);\n        for (let level = 0; level < 4; level++) {\n            this.rooms[level] = new Array(MAP_SIZE);\n            for (let x = 0; x < MAP_SIZE; x++) {\n                this.rooms[level][x] = new Array(MAP_SIZE).fill(null);\n\n                if (level === 0) {\n                    for (let y = 0; y < MAP_SIZE; y++) {\n                        this.rooms[level][x][y] = new Room('empty_grass');\n                    }\n                }\n            }\n        }\n    }\n\n    public copyRooms(rooms: (Room | null)[][][]): void {\n        for (let level = 0; level < 4; level++) {\n            for (let x = 0; x < MAP_SIZE; x++) {\n                for (let y = 0; y < MAP_SIZE; y++) {\n                    const existingRoom = rooms[level][x][y] ?? null;\n\n                    this.rooms[level][x][y] = existingRoom ? new Room(existingRoom.type, existingRoom.orientation) : null;\n                }\n            }\n        }\n    }\n}\n\nexport class Room extends ConstructedChunk {\n    public readonly type: RoomType;\n\n    public constructor(type: RoomType, orientation: number = 0) {\n        super(orientation);\n        this.type = type;\n    }\n\n    public getTemplatePosition(): Position {\n        return roomTemplates[this.type];\n    }\n}\n"
  },
  {
    "path": "src/plugins/skills/construction/index.ts",
    "content": "import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action';\nimport type { PlayerInitAction } from '@engine/action/pipe/player-init.action';\nimport { saveHouse } from '@plugins/skills/construction/home-saver';\nimport { openHouse } from '@plugins/skills/construction/house';\nimport { doorHotspotHandler, roomBuilderWidgetHandler } from '@plugins/skills/construction/room-builder';\nimport { instance1, instance1Max, instance2, instance2Max, roomBuilderButtonMap } from './con-constants';\n\nexport default {\n    pluginId: 'rs:construction',\n    hooks: [\n        {\n            type: 'button',\n            widgetIds: 402,\n            buttonIds: Object.keys(roomBuilderButtonMap).map(key => parseInt(key, 10)),\n            handler: roomBuilderWidgetHandler,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: [15313, 15314],\n            options: 'build',\n            walkTo: true,\n            handler: doorHotspotHandler,\n        },\n        {\n            type: 'player_command',\n            commands: ['con', 'poh', 'house'],\n            handler: ({ player }: PlayerCommandAction): void => openHouse(player),\n        },\n        {\n            type: 'player_command',\n            commands: ['savepoh', 'savehouse'],\n            handler: ({ player }: PlayerCommandAction): void => {\n                player.sendMessage(`Saving house data...`);\n                saveHouse(player);\n            },\n        },\n        {\n            type: 'player_init',\n            handler: ({ player }: PlayerInitAction): void => {\n                if (player.position.within(instance1, instance1Max, false) || player.position.within(instance2, instance2Max, false)) {\n                    openHouse(player);\n                }\n            },\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/construction/room-builder.ts",
    "content": "import type { buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { dialogue, execute, goto } from '@engine/world/actor/dialogue';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Coords } from '@engine/world/position';\nimport { MAP_SIZE, roomBuilderButtonMap } from '@plugins/skills/construction/con-constants';\nimport { Room, openHouse } from '@plugins/skills/construction/house';\nimport { getCurrentRoom } from '@plugins/skills/construction/util';\nimport { logger } from '@runejs/common';\n\nconst newRoomOriention = (player: Player): number => {\n    const currentRoom = getCurrentRoom(player);\n\n    if (!currentRoom) {\n        return 0;\n    }\n\n    const playerLocalX = player.position.localX;\n    const playerLocalY = player.position.localY;\n\n    let deltaX = 0;\n    let deltaY = 0;\n\n    let orientation = 0;\n\n    if (playerLocalX === 7) {\n        // build east\n        deltaX = 1;\n        orientation = 1;\n    } else if (playerLocalX === 0) {\n        // build west\n        deltaX = -1;\n        orientation = 3;\n    } else if (playerLocalY === 7) {\n        // build north\n        deltaY = 1;\n        orientation = 0;\n    } else if (playerLocalY === 0) {\n        // build south\n        deltaY = -1;\n        orientation = 2;\n    }\n\n    return orientation;\n};\n\nexport const canBuildNewRoom = (player: Player): Coords | null => {\n    const currentRoom = getCurrentRoom(player);\n\n    if (!currentRoom) {\n        return null;\n    }\n\n    const playerLocalX = player.position.localX;\n    const playerLocalY = player.position.localY;\n\n    let buildX = currentRoom.x;\n    let buildY = currentRoom.y;\n\n    if (playerLocalX === 7) {\n        // build east\n        if (currentRoom.x < MAP_SIZE - 3) {\n            buildX = currentRoom.x + 1;\n        }\n    } else if (playerLocalX === 0) {\n        // build west\n        if (currentRoom.x > 2) {\n            buildX = currentRoom.x - 1;\n        }\n    } else if (playerLocalY === 7) {\n        // build north\n        if (currentRoom.y < MAP_SIZE - 3) {\n            buildY = currentRoom.y + 1;\n        }\n    } else if (playerLocalY === 0) {\n        // build south\n        if (currentRoom.y > 2) {\n            buildY = currentRoom.y - 1;\n        }\n    }\n\n    if (buildX === currentRoom.x && buildY === currentRoom.y) {\n        player.sendMessage(`You can not build there.`);\n        return null;\n    }\n\n    const playerCustomMap = player.metadata.customMap;\n    if (!playerCustomMap) {\n        logger.error(`Player ${player.username} does not have a custom map.`);\n        return null;\n    }\n    const rooms = playerCustomMap.chunks as Room[][][];\n    const existingRoom = rooms[player.position.level][buildX][buildY];\n\n    if (existingRoom && existingRoom.type !== 'empty_grass' && existingRoom.type !== 'empty') {\n        player.sendMessage(`${existingRoom.type} already exists there`); // @TODO\n        return null;\n    }\n\n    return {\n        x: buildX,\n        y: buildY,\n        level: player.position.level,\n    };\n};\n\nexport const roomBuilderWidgetHandler: buttonActionHandler = async ({ player, buttonId }) => {\n    const newRoomCoords = canBuildNewRoom(player);\n    if (!newRoomCoords) {\n        return;\n    }\n\n    const chosenRoomType = roomBuilderButtonMap[buttonId];\n    if (!chosenRoomType) {\n        return;\n    }\n\n    const playerCustomMap = player.metadata.customMap;\n    if (!playerCustomMap) {\n        logger.error(`Player ${player.username} does not have a custom map.`);\n        return;\n    }\n\n    const createdRoom = new Room(chosenRoomType, newRoomOriention(player));\n    playerCustomMap.chunks[newRoomCoords.level][newRoomCoords.x][newRoomCoords.y] = createdRoom;\n\n    player.interfaceState.closeAllSlots();\n\n    openHouse(player);\n\n    await dialogue(\n        [player],\n        [\n            (options, tag_Home) => [\n                'Rotate Counter-Clockwise',\n                [\n                    execute(() => {\n                        createdRoom.orientation = createdRoom.orientation > 0 ? createdRoom.orientation - 1 : 3;\n                        openHouse(player);\n                    }),\n                    goto('tag_Home'),\n                ],\n                'Rotate Clockwise',\n                [\n                    execute(() => {\n                        createdRoom.orientation = createdRoom.orientation < 3 ? createdRoom.orientation + 1 : 0;\n                        openHouse(player);\n                    }),\n                    goto('tag_Home'),\n                ],\n                'Accept',\n                [execute(() => {})],\n            ],\n        ],\n    );\n};\n\nexport const doorHotspotHandler: objectInteractionActionHandler = ({ player }) => {\n    if (!canBuildNewRoom(player)) {\n        return;\n    }\n\n    player.interfaceState.openWidget(402, { slot: 'screen' });\n};\n"
  },
  {
    "path": "src/plugins/skills/construction/util.ts",
    "content": "import { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport type { Coords } from '@engine/world/position';\nimport { Position } from '@engine/world/position';\n\n/**\n * Finds the local coordinates of the room that the player is currently in within their PoH.\n * Returns null if the player is not currently in a custom map region.\n * @param player The player to find the room for.\n */\nexport const getCurrentRoom = (player: Player): Coords | null => {\n    const customMap = player.metadata?.customMap;\n\n    if (!customMap) {\n        return null;\n    }\n\n    const mapWorldX = customMap.renderPosition.x;\n    const mapWorldY = customMap.renderPosition.y;\n\n    const topCornerMapChunk = activeWorld.chunkManager.getChunkForWorldPosition(new Position(mapWorldX, mapWorldY, player.position.level));\n    const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(player.position);\n\n    const currentRoomX = playerChunk.position.x - (topCornerMapChunk.position.x - 2);\n    const currentRoomY = playerChunk.position.y - (topCornerMapChunk.position.y - 2);\n\n    return {\n        x: currentRoomX,\n        y: currentRoomY,\n        level: player.position.level,\n    };\n};\n"
  },
  {
    "path": "src/plugins/skills/crafting/sheep-plugin.plugin.ts",
    "content": "import type { itemOnNpcActionHandler } from '@engine/action/pipe/item-on-npc.action';\nimport type { npcInitActionHandler } from '@engine/action/pipe/npc-init.action';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\n\nconst initAction: npcInitActionHandler = ({ npc }) => {\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    // setInterval(() => {\n    //     if(Math.random() >= 0.66) {\n    //         npc.say(`Baa!`);\n    //         npc.playSound(soundIds.sheepBaa, 4);\n    //     }\n    // }, (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH);\n};\n\nexport const shearAction: itemOnNpcActionHandler = ({ player, npc }) => {\n    player.busy = true;\n    player.playAnimation(animationIds.shearSheep);\n    player.playSound(soundIds.shearSheep, 5);\n    // set to face position, so it does not look weird when the player walk away\n    npc.face(player.position);\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     if(Math.random() >= 0.66) {\n    //         player.sendMessage('The sheep manages to get away from you!');\n    //         npc.forceMovement(player.faceDirection, 5);\n    //     } else {\n    //         player.sendMessage('You get some wool.');\n    //         player.giveItem(itemIds.wool);\n    //         npc.say('Baa!');\n    //         npc.playSound(soundIds.sheepBaa, 4);\n    //         npc.transformInto('rs:naked_sheep');\n\n    //         setTimeout(() => {\n    //             npc.transformInto('rs:sheep');\n    //         }, (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH);\n    //     }\n    //     player.busy = false;\n    // }, World.TICK_LENGTH);\n};\n\nexport default {\n    pluginId: 'rs:sheep_shearing',\n    hooks: [\n        {\n            type: 'npc_init',\n            npcs: 'rs:sheep',\n            handler: initAction,\n        },\n        {\n            type: 'item_on_npc',\n            npcs: 'rs:sheep',\n            itemIds: [itemIds.shears, itemIds.recruitmentDrive.shears],\n            walkTo: true,\n            handler: shearAction,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/crafting/spinning-wheel.plugin.ts",
    "content": "import type { ButtonAction, buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport { ActorTask } from '@engine/task/impl/actor-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { logger } from '@runejs/common';\n\ninterface Spinnable {\n    input: number | number[];\n    output: number;\n    experience: number;\n    requiredLevel: number;\n}\n\ninterface SpinnableButton {\n    shouldTakeInput: boolean;\n    count: number;\n    spinnable: Spinnable;\n}\n\nconst ballOfWool: Spinnable = { input: itemIds.wool, output: itemIds.ballOfWool, experience: 2.5, requiredLevel: 1 };\nconst bowString: Spinnable = { input: itemIds.flax, output: itemIds.bowstring, experience: 15, requiredLevel: 10 };\nconst rootsCbowString: Spinnable = {\n    input: [itemIds.roots.oak, itemIds.roots.willow, itemIds.roots.maple, itemIds.roots.yew],\n    output: itemIds.crossbowString,\n    experience: 15,\n    requiredLevel: 10,\n};\nconst sinewCbowString: Spinnable = {\n    input: itemIds.sinew,\n    output: itemIds.crossbowString,\n    experience: 15,\n    requiredLevel: 10,\n};\nconst magicAmuletString: Spinnable = {\n    input: itemIds.roots.magic,\n    output: itemIds.magicString,\n    experience: 30,\n    requiredLevel: 19,\n};\nconst widgetButtonIds: Map<number, SpinnableButton> = new Map<number, SpinnableButton>([\n    [100, { shouldTakeInput: false, count: 1, spinnable: ballOfWool }],\n    [99, { shouldTakeInput: false, count: 5, spinnable: ballOfWool }],\n    [98, { shouldTakeInput: false, count: 10, spinnable: ballOfWool }],\n    [97, { shouldTakeInput: true, count: 0, spinnable: ballOfWool }],\n    [95, { shouldTakeInput: false, count: 1, spinnable: bowString }],\n    [94, { shouldTakeInput: false, count: 5, spinnable: bowString }],\n    [93, { shouldTakeInput: false, count: 10, spinnable: bowString }],\n    [91, { shouldTakeInput: true, count: 0, spinnable: bowString }],\n    [107, { shouldTakeInput: false, count: 1, spinnable: magicAmuletString }],\n    [106, { shouldTakeInput: false, count: 5, spinnable: magicAmuletString }],\n    [105, { shouldTakeInput: false, count: 10, spinnable: magicAmuletString }],\n    [104, { shouldTakeInput: true, count: 0, spinnable: magicAmuletString }],\n    [121, { shouldTakeInput: false, count: 1, spinnable: rootsCbowString }],\n    [120, { shouldTakeInput: false, count: 5, spinnable: rootsCbowString }],\n    [119, { shouldTakeInput: false, count: 10, spinnable: rootsCbowString }],\n    [118, { shouldTakeInput: true, count: 0, spinnable: rootsCbowString }],\n    [114, { shouldTakeInput: false, count: 1, spinnable: sinewCbowString }],\n    [113, { shouldTakeInput: false, count: 5, spinnable: sinewCbowString }],\n    [112, { shouldTakeInput: false, count: 10, spinnable: sinewCbowString }],\n    [111, { shouldTakeInput: true, count: 0, spinnable: sinewCbowString }],\n]);\n\nexport const openSpinningInterface: objectInteractionActionHandler = details => {\n    details.player.interfaceState.openWidget(widgets.whatWouldYouLikeToSpin, {\n        slot: 'screen',\n    });\n};\n\n/**\n * A task to (repeatedly if needed) spin a product from a spinnable.\n */\nclass SpinProductTask extends ActorTask<Player> {\n    /**\n     * The number of ticks that `execute` has been called inside this task.\n     */\n    private elapsedTicks = 0;\n\n    /**\n     * The number of items that should be spun.\n     */\n    private count: number;\n\n    /**\n     * The number of items that have been spun.\n     */\n    private created = 0;\n\n    /**\n     * The spinnable that is being used.\n     */\n    private spinnable: Spinnable;\n\n    /**\n     * The currently being spun input.\n     */\n    private currentItem: number;\n\n    /**\n     * The index of the current input being spun.\n     */\n    private currentItemIndex = 0;\n\n    constructor(player: Player, spinnable: Spinnable, count: number) {\n        super(player);\n        this.spinnable = spinnable;\n        this.count = count;\n    }\n\n    public execute(): void {\n        if (this.created === this.count) {\n            this.stop();\n            return;\n        }\n\n        // As an multiple items can be used for one of the recipes, check if its an array\n        let isArray = false;\n        if (Array.isArray(this.spinnable.input)) {\n            isArray = true;\n            this.currentItem = this.spinnable.input[0];\n        } else {\n            this.currentItem = this.spinnable.input;\n        }\n\n        // Check if out of input material\n        if (!this.actor.hasItemInInventory(this.currentItem)) {\n            let cancel = false;\n            if (isArray) {\n                if (this.currentItemIndex < (<number[]>this.spinnable.input).length) {\n                    this.currentItemIndex++;\n                    this.currentItem = (<number[]>this.spinnable.input)[this.currentItemIndex];\n                } else {\n                    cancel = true;\n                }\n            } else {\n                cancel = true;\n            }\n            if (cancel) {\n                const itemName = findItem(this.currentItem)?.name || '';\n                this.actor.sendMessage(`You don't have any ${itemName.toLowerCase()}.`);\n                this.stop();\n                return;\n            }\n        }\n\n        // Spinning takes 3 ticks for each item\n        if (this.elapsedTicks % 3 === 0) {\n            this.actor.removeFirstItem(this.currentItem);\n            this.actor.giveItem(this.spinnable.output);\n            this.actor.skills.addExp(Skill.CRAFTING, this.spinnable.experience);\n            this.created++;\n        }\n\n        // animation plays once every two items\n        if (this.elapsedTicks % 6 === 0) {\n            this.actor.playAnimation(animationIds.spinSpinningWheel);\n            this.actor.outgoingPackets.playSound(soundIds.spinWool, 5);\n        }\n\n        this.elapsedTicks++;\n    }\n}\n\nconst spinProduct: any = (details: ButtonAction, spinnable: Spinnable, count: number) => {\n    details.player.enqueueTask(SpinProductTask, [spinnable, count]);\n};\n\nexport const buttonClicked: buttonActionHandler = details => {\n    // Check if player might be spawning widget clientside\n    if (!details.player.interfaceState.findWidget(459)) {\n        return;\n    }\n    const product = widgetButtonIds.get(details.buttonId);\n\n    if (!product) {\n        logger.error(`Unhandled button id ${details.buttonId} for buttonClicked in spinning wheel.`);\n        return;\n    }\n\n    // Close the widget as it is no longer needed\n    details.player.interfaceState.closeAllSlots();\n\n    if (!details.player.skills.hasLevel(Skill.CRAFTING, product.spinnable.requiredLevel)) {\n        const outputName = findItem(product.spinnable.output)?.name || '';\n\n        details.player.sendMessage(\n            `You need a crafting level of ${product.spinnable.requiredLevel} to craft ${outputName.toLowerCase()}.`,\n            true,\n        );\n        return;\n    }\n\n    if (!product.shouldTakeInput) {\n        // If the player has not chosen make X, we dont need to get input and can just start the crafting\n        spinProduct(details, product.spinnable, product.count);\n    } else {\n        // We should prepare for a number to be sent from the client\n        const numericInputSpinSub = details.player.numericInputEvent.subscribe(number => {\n            actionCancelledSpinSub?.unsubscribe();\n            numericInputSpinSub?.unsubscribe();\n            // When a number is recieved we can start crafting the product\n            spinProduct(details, product.spinnable, number);\n        });\n        // If the player moves or cancels the number input, we do not want to wait for input, as they could be depositing\n        // items into their bank.\n        const actionCancelledSpinSub = details.player.actionsCancelled.subscribe(() => {\n            actionCancelledSpinSub?.unsubscribe();\n            numericInputSpinSub?.unsubscribe();\n        });\n        // Ask the player to enter how many they want to create\n        details.player.outgoingPackets.showNumberInputDialogue();\n    }\n};\n\nexport default {\n    pluginId: 'rs:spinning_wheel',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: objectIds.spinningWheel,\n            options: ['spin'],\n            walkTo: true,\n            handler: openSpinningInterface,\n        },\n        {\n            type: 'button',\n            widgetId: widgets.whatWouldYouLikeToSpin,\n            buttonIds: Array.from(widgetButtonIds.keys()),\n            handler: buttonClicked,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/firemaking/chance.ts",
    "content": "/**\n * Roll a chance to light a fire.\n *\n * TODO (jameskmonger) this was ported from the old codebase and needs to be documented.\n *\n * @param logLevel The firemaking level required to light the log.\n * @param playerLevel The player's current firemaking level.\n * @returns `true` if the player successfully lights the fire, `false` otherwise.\n */\nexport const canLight = (logLevel: number, playerLevel: number): boolean => {\n    if (playerLevel < logLevel) {\n        return false;\n    }\n\n    playerLevel++;\n    const hostRatio = Math.random() * logLevel;\n    const clientRatio = Math.random() * ((playerLevel - logLevel) * (1 + logLevel * 0.01));\n    return hostRatio < clientRatio;\n};\n\n/**\n * Roll a chance to 'chain' a fire.\n *\n * TODO (jameskmonger) this was ported from the old codebase and needs to be documented.\n *                      what is \"chain\"?\n *\n * @param logLevel The firemaking level required to light the log.\n * @param playerLevel The player's current firemaking level.\n * @returns `true` if the player successfully lights the fire, `false` otherwise.\n */\nexport const canChain = (logLevel: number, playerLevel: number): boolean => {\n    if (playerLevel < logLevel) {\n        return false;\n    }\n\n    playerLevel++;\n    const hostRatio = Math.random() * logLevel;\n    const clientRatio = Math.random() * ((playerLevel - logLevel) * (1 + logLevel * 0.01));\n    return clientRatio - hostRatio < 3.5;\n};\n"
  },
  {
    "path": "src/plugins/skills/firemaking/data.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport type { Burnable } from './types';\n\n// using ! here because we know the items exist\nexport const FIREMAKING_LOGS: Burnable[] = [\n    {\n        logItem: findItem('rs:logs')!,\n        requiredLevel: 1,\n        experienceGained: 40,\n    },\n    {\n        logItem: findItem('rs:oak_logs')!,\n        requiredLevel: 15,\n        experienceGained: 60,\n    },\n    {\n        logItem: findItem('rs:willow_logs')!,\n        requiredLevel: 30,\n        experienceGained: 90,\n    },\n    {\n        logItem: findItem('rs:teak_logs')!,\n        requiredLevel: 35,\n        experienceGained: 105,\n    },\n    {\n        logItem: findItem('rs:maple_logs')!,\n        requiredLevel: 45,\n        experienceGained: 135,\n    },\n    {\n        logItem: findItem('rs:mahogany_logs')!,\n        requiredLevel: 50,\n        experienceGained: 157.5,\n    },\n    {\n        logItem: findItem('rs:yew_logs')!,\n        requiredLevel: 60,\n        experienceGained: 202.5,\n    },\n    {\n        logItem: findItem('rs:magic_logs')!,\n        requiredLevel: 75,\n        experienceGained: 303.8,\n    },\n];\n"
  },
  {
    "path": "src/plugins/skills/firemaking/firemaking-task.ts",
    "content": "import { ActorWorldItemInteractionTask } from '@engine/task/impl/actor-world-item-interaction-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport { canLight } from './chance';\nimport { FIREMAKING_LOGS } from './data';\nimport { lightFire } from './light-fire';\nimport type { Burnable } from './types';\n\n/**\n * A firemaking task on a {@link WorldItem} log.\n *\n * This task extends {@link ActorWorldItemInteractionTask} which is a task that\n * handles the interaction between an {@link Actor} and a {@link WorldItem}.\n *\n * The {@link ActorWalkToTask} (which our parent class extends) automatically\n * sets a {@link TaskBreakType} of {@link TaskBreakType.ON_MOVE} which will\n * cancel this task if the player clicks to move.\n *\n * By default, the {@link ActorWalkToTask} also sets a {@link TaskStackType} of\n * {@link TaskStackType.NEVER} and a {@link TaskStackGroup} of {@link TaskStackGroup.ACTION}\n * which means that this other actions will cancel the firemaking attempts.\n *\n * @author jameskmonger\n */\nclass FiremakingTask extends ActorWorldItemInteractionTask<Player> {\n    /**\n     * The log being lit.\n     */\n    private logInfo: Burnable;\n\n    /**\n     * The number of ticks that `execute` has been called inside this task.\n     */\n    private elapsedTicks = 0;\n    private canLightFire = false;\n\n    /**\n     * Create a new firemaking task.\n     *\n     * @param player The player that is attempting to light the fire.\n     * @param logWorldItem The world item that represents the log.\n     */\n    constructor(player: Player, logWorldItem: WorldItem) {\n        super(player, logWorldItem);\n\n        const logInfo = FIREMAKING_LOGS.find(l => l.logItem.gameId === logWorldItem.itemId);\n\n        if (!logInfo) {\n            throw new Error(`Invalid firemaking log item id: ${logWorldItem.itemId}`);\n        }\n\n        this.logInfo = logInfo;\n    }\n\n    /**\n     * Execute the main firemaking task loop. This method is called every game tick until the task is completed.\n     *\n     * As this task extends {@link ActorWorldItemInteractionTask}, it's important that the\n     * {@link super.execute} method is called at the start of this method.\n     *\n     * The base `execute` performs a number of checks that allow this task to function healthily.\n     */\n    public execute() {\n        super.execute();\n\n        /**\n         * As this task extends {@link ActorWorldItemInteractionTask}, the base classes {@link ActorWorldItemInteractionTask[\"worldItem\"]}\n         * property will be null if the item isn't valid anymore or the player isn't in the right position.\n         *\n         * Therefore if `worldItem` is null, we can return early here, as our player is likely walking to the task.\n         */\n        if (!this.worldItem) {\n            return;\n        }\n\n        // store the tick count before incrementing so we don't need to keep track of it in all the separate branches\n        const tickCount = this.elapsedTicks++;\n\n        if (this.canLightFire) {\n            if (tickCount === 2) {\n                lightFire(this.actor, this.actor.position, this.worldItem, this.logInfo.experienceGained);\n                this.stop();\n            }\n\n            // the rest of the function is for *attempting* to light the fire\n            // so we can return early here\n            return;\n        }\n\n        // play animation every 12 ticks\n        if (tickCount % 12 === 0) {\n            this.actor.playAnimation(animationIds.lightingFire);\n        }\n\n        // TODO (jameskmonger) reconsider this, is there a minimum tick count?\n        //              OSRS wiki implies that there isn't\n        //              https://oldschool.runescape.wiki/w/Firemaking#Success_chance\n        const passedMinimumThreshold = tickCount > 10;\n        this.canLightFire = passedMinimumThreshold && canLight(this.logInfo.requiredLevel, this.actor.skills.firemaking.level);\n\n        // if we can now light the fire, reset the timer so that on the next tick we can begin lighting the fire\n        if (this.canLightFire) {\n            this.elapsedTicks = 0;\n            this.actor.busy = true;\n            this.actor.playSound(soundIds.fireLit, 7);\n\n            return;\n        }\n\n        // play lighting sound every 4th tick\n        if (tickCount % 4 === 0) {\n            this.actor.playSound(soundIds.lightingFire, 10, 0);\n        }\n    }\n}\n\n/**\n * Run the firemaking task for a player.\n *\n * @param player The player that is attempting to light the fire.\n * @param worldItemLog The WorldItem that represents the log.\n */\nexport function runFiremakingTask(player: Player, worldItemLog: WorldItem) {\n    player.enqueueTask(FiremakingTask, [worldItemLog]);\n}\n"
  },
  {
    "path": "src/plugins/skills/firemaking/index.ts",
    "content": "import type { ItemOnItemActionHook, itemOnItemActionHandler } from '@engine/action/pipe/item-on-item.action';\nimport type { ItemOnWorldItemActionHook } from '@engine/action/pipe/item-on-world-item.action';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { canChain } from './chance';\nimport { FIREMAKING_LOGS } from './data';\nimport { runFiremakingTask } from './firemaking-task';\nimport { canLightFireAtCurrentPosition, lightFire } from './light-fire';\n\n/**\n * Action hook for lighting a log with a tinderbox in the player's inventory.\n */\nconst tinderboxOnLogHandler: itemOnItemActionHandler = details => {\n    const { player, usedItem, usedWithItem, usedSlot, usedWithSlot } = details;\n\n    if (player.metadata.lastFire && Date.now() - player.metadata.lastFire < 600) {\n        return;\n    }\n\n    const log = usedItem.itemId !== itemIds.tinderbox ? usedItem : usedWithItem;\n    const removeFromSlot = usedItem.itemId !== itemIds.tinderbox ? usedSlot : usedWithSlot;\n    const skillInfo = FIREMAKING_LOGS.find(l => l.logItem.gameId === log.itemId);\n\n    if (!skillInfo) {\n        player.sendMessage(`Mishandled firemaking log ${log.itemId}.`);\n        return;\n    }\n\n    if (player.skills.firemaking.level < skillInfo.requiredLevel) {\n        player.sendMessage(`You need a Firemaking level of ${skillInfo.requiredLevel} to light this log.`);\n        return;\n    }\n\n    if (!canLightFireAtCurrentPosition(player)) {\n        player.sendMessage('You cannot light a fire here.');\n        return;\n    }\n\n    player.removeItem(removeFromSlot);\n    const worldItemLog = player.instance.spawnWorldItem(log, player.position, { owner: player, expires: 300 });\n\n    // TODO (jameskmonger) chaining functionality needs documentation, I can't find anything about it online\n    if (\n        player.metadata.lastFire &&\n        Date.now() - player.metadata.lastFire < 1200 &&\n        canChain(skillInfo.requiredLevel, player.skills.firemaking.level)\n    ) {\n        lightFire(player, player.position, worldItemLog, skillInfo.experienceGained);\n    } else {\n        player.sendMessage('You attempt to light the logs.');\n\n        runFiremakingTask(player, worldItemLog);\n    }\n};\n\n/**\n * Firemaking plugin\n *\n * TODO:\n * - Document/remove `canChain` functionality - this is not documented anywhere online (RS wiki etc)\n */\nexport default {\n    pluginId: 'rs:firemaking',\n    hooks: [\n        {\n            type: 'item_on_item',\n            items: FIREMAKING_LOGS.map(log => ({ item1: itemIds.tinderbox, item2: log.logItem.gameId })),\n            handler: tinderboxOnLogHandler,\n        } as ItemOnItemActionHook,\n        {\n            type: 'item_on_world_item',\n            items: FIREMAKING_LOGS.map(log => ({ item: itemIds.tinderbox, worldItem: log.logItem.gameId })),\n            handler: ({ player, usedWithItem }) => {\n                runFiremakingTask(player, usedWithItem);\n            },\n        } as ItemOnWorldItemActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/firemaking/light-fire.ts",
    "content": "import { randomBetween } from '@engine/util/num';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport type { WorldItem } from '@engine/world/items/world-item';\nimport type { Position } from '@engine/world/position';\nimport type { LandscapeObject } from '@runejs/filestore';\n\nconst fireDurationTicks = (): number => {\n    return randomBetween(100, 200); // 1-2 minutes\n};\n\n/**\n * Whether or not a fire can be lit at the player's position.\n *\n * This is `true` if there are no spawned objects at the specified position of type 10.\n *\n * Probably should be moved to a more generic location (maybe on WorldInstance)\n *\n * @param player The player attempting to light the fire.\n * @returns `true` if a fire can be lit at the specified position, `false` otherwise.\n *\n * @author jameskmonger\n */\nexport const canLightFireAtCurrentPosition = (player: Player): boolean => {\n    const existingFire = player.instance\n        .getTileModifications(player.position)\n        .mods.spawnedObjects.find(\n            o => o.x === player.position.x && o.y === player.position.y && o.level === player.position.level && o.type === 10,\n        );\n\n    return existingFire === undefined;\n};\n\n/**\n * Light a fire at the specified position.\n *\n * @param player The player lighting the fire.\n * @param position The position to light the fire at.\n * @param worldItemLog The world item representing the log.\n * @param burnExp The experience gained for lighting the fire.\n */\nexport const lightFire = (player: Player, position: Position, worldItemLog: WorldItem, burnExp: number): void => {\n    if (!canLightFireAtCurrentPosition(player)) {\n        player.sendMessage('You cannot light a fire here.');\n        return;\n    }\n\n    player.instance.despawnWorldItem(worldItemLog);\n    const fireObject: LandscapeObject = {\n        objectId: objectIds.fire,\n        x: position.x,\n        y: position.y,\n        level: position.level,\n        type: 10,\n        orientation: 0,\n    };\n\n    player.playAnimation(null);\n    player.sendMessage('The fire catches and the logs begin to burn.');\n    player.skills.firemaking.addExp(burnExp);\n\n    if (!player.walkingQueue.moveIfAble(-1, 0)) {\n        if (!player.walkingQueue.moveIfAble(1, 0)) {\n            if (!player.walkingQueue.moveIfAble(0, -1)) {\n                player.walkingQueue.moveIfAble(0, 1);\n            }\n        }\n    }\n\n    player.instance.spawnTemporaryGameObject(fireObject, position, fireDurationTicks()).then(() => {\n        player.instance.spawnWorldItem({ itemId: itemIds.ashes, amount: 1 }, position, { expires: 300 });\n    });\n\n    player.face(position, false);\n    player.metadata.lastFire = Date.now();\n    player.busy = false;\n};\n"
  },
  {
    "path": "src/plugins/skills/firemaking/types.ts",
    "content": "import type { ItemDetails } from '@engine/config/item-config';\n\n/**\n * The definition for a burnable log.\n */\nexport type Burnable = {\n    /**\n     * The item details for the log.\n     */\n    logItem: ItemDetails;\n\n    /**\n     * The firemaking level required to light the log.\n     */\n    requiredLevel: number;\n\n    /**\n     * The experience gained for lighting the log.\n     */\n    experienceGained: number;\n};\n"
  },
  {
    "path": "src/plugins/skills/fletching/fletching-constants.ts",
    "content": "import { itemIds } from '@engine/world/config/item-ids';\nimport type { Fletchable } from '@plugins/skills/fletching/fletching-types';\n\nexport const knifeId: number = itemIds.knife;\n\nexport const fletchables: Map<string, Map<string, Fletchable>> = new Map<string, Map<string, Fletchable>>([\n    [\n        'bow(u)',\n        new Map<string, Fletchable>([\n            [\n                'wood short',\n                {\n                    level: 1,\n                    experience: 5,\n                    item: { itemId: itemIds.bowunstrung.woodshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'wood long',\n                {\n                    level: 10,\n                    experience: 10,\n                    item: { itemId: itemIds.bowunstrung.woodlong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'oak short',\n                {\n                    level: 20,\n                    experience: 16.5,\n                    item: { itemId: itemIds.bowunstrung.oakshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'oak long',\n                {\n                    level: 25,\n                    experience: 25,\n                    item: { itemId: itemIds.bowunstrung.oaklong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'comp ogre',\n                {\n                    level: 30,\n                    experience: 45,\n                    item: { itemId: itemIds.bowunstrung.compogre, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'willow short',\n                {\n                    level: 35,\n                    experience: 33.3,\n                    item: { itemId: itemIds.bowunstrung.willowshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'willow long',\n                {\n                    level: 40,\n                    experience: 41.5,\n                    item: { itemId: itemIds.bowunstrung.willowlong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'maple short',\n                {\n                    level: 50,\n                    experience: 50,\n                    item: { itemId: itemIds.bowunstrung.mapleshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'maple long',\n                {\n                    level: 55,\n                    experience: 58.3,\n                    item: { itemId: itemIds.bowunstrung.maplelong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'yew short',\n                {\n                    level: 65,\n                    experience: 67.5,\n                    item: { itemId: itemIds.bowunstrung.yewshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'yew long',\n                {\n                    level: 70,\n                    experience: 75,\n                    item: { itemId: itemIds.bowunstrung.yewlong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'magic short',\n                {\n                    level: 80,\n                    experience: 83.3,\n                    item: { itemId: itemIds.bowunstrung.magicshort, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n            [\n                'magic long',\n                {\n                    level: 85,\n                    experience: 91.5,\n                    item: { itemId: itemIds.bowunstrung.magiclong, amount: 1 },\n                    ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }],\n                },\n            ],\n        ]),\n    ],\n    [\n        'bow',\n        new Map<string, Fletchable>([\n            [\n                'wood short',\n                {\n                    level: 1,\n                    experience: 5,\n                    item: { itemId: itemIds.bowstrung.woodshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.woodshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'wood long',\n                {\n                    level: 10,\n                    experience: 10,\n                    item: { itemId: itemIds.bowstrung.woodlong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.woodlong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'oak short',\n                {\n                    level: 20,\n                    experience: 16.5,\n                    item: { itemId: itemIds.bowunstrung.oakshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.oakshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'oak long',\n                {\n                    level: 25,\n                    experience: 25,\n                    item: { itemId: itemIds.bowstrung.oaklong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.oaklong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'comp ogre',\n                {\n                    level: 30,\n                    experience: 45,\n                    item: { itemId: itemIds.bowstrung.compogre, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.compogre, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'willow short',\n                {\n                    level: 35,\n                    experience: 33.3,\n                    item: { itemId: itemIds.bowstrung.willowshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.willowshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'willow long',\n                {\n                    level: 40,\n                    experience: 41.5,\n                    item: { itemId: itemIds.bowstrung.willowlong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.willowlong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'maple short',\n                {\n                    level: 50,\n                    experience: 50,\n                    item: { itemId: itemIds.bowstrung.mapleshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.mapleshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'maple long',\n                {\n                    level: 55,\n                    experience: 58.3,\n                    item: { itemId: itemIds.bowstrung.maplelong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.maplelong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'yew short',\n                {\n                    level: 65,\n                    experience: 67.5,\n                    item: { itemId: itemIds.bowstrung.yewshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.yewshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'yew long',\n                {\n                    level: 70,\n                    experience: 75,\n                    item: { itemId: itemIds.bowstrung.yewlong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.yewlong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'magic short',\n                {\n                    level: 80,\n                    experience: 83.3,\n                    item: { itemId: itemIds.bowstrung.magicshort, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.magicshort, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n            [\n                'magic long',\n                {\n                    level: 85,\n                    experience: 91.5,\n                    item: { itemId: itemIds.bowstrung.magiclong, amount: 1 },\n                    ingredient: [\n                        { itemId: itemIds.bowunstrung.magiclong, amount: 1 },\n                        { itemId: itemIds.bowstring, amount: 1 },\n                    ],\n                },\n            ],\n        ]),\n    ],\n]);\n"
  },
  {
    "path": "src/plugins/skills/fletching/fletching-types.ts",
    "content": "import type { Item } from '@engine/world/items/item';\nexport interface Fletchable {\n    item: Item;\n    level: number;\n    experience: number;\n    ingredient: Item[];\n}\n"
  },
  {
    "path": "src/plugins/skills/fletching/fletching.plugin.ts",
    "content": "//fletching stuff goes below this! lets do it!\n\nexport default {\n    pluginId: 'rs:fletching',\n};\n"
  },
  {
    "path": "src/plugins/skills/level-up-dialogue.plugin.ts",
    "content": "import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action';\n\nconst widgetIds = [158, 161, 175, 167, 171, 170, 168, 159, 177, 165, 164, 163, 160, 174, 169, 166, 157, 176, 173, 162, 172];\n\n/**\n * Handles a level-up dialogue action.\n */\nexport const handler: widgetInteractionActionHandler = ({ player }) => player.interfaceState.closeChatOverlayWidget();\n\nexport default {\n    pluginId: 'rs:close_level_up_message',\n    hooks: [{ type: 'widget_interaction', widgetIds, handler, cancelActions: false }],\n};\n"
  },
  {
    "path": "src/plugins/skills/mining/chance.ts",
    "content": "import { randomBetween } from '@engine/util/num';\nimport type { IHarvestable } from '@engine/world/config/harvestable-object';\n\n/**\n * Roll a random number between 0 and 255 and compare it to the percent needed to mine the ore.\n *\n * @param ore The ore to mine\n * @param toolLevel The level of the pickaxe being used\n * @param miningLevel The player's mining level\n *\n * @returns True if the tree was successfully cut, false otherwise\n */\nexport const canMine = (ore: IHarvestable, toolLevel: number, miningLevel: number): boolean => {\n    const successChance = randomBetween(0, 255);\n\n    const percentNeeded = ore.baseChance + toolLevel + miningLevel;\n    return successChance <= percentNeeded;\n};\n"
  },
  {
    "path": "src/plugins/skills/mining/mining-task.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport { equipmentIndices } from '@engine/config/item-config';\nimport { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task';\nimport { colors } from '@engine/util/colors';\nimport { randomBetween } from '@engine/util/num';\nimport { colorText } from '@engine/util/strings';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport type { HarvestTool } from '@engine/world/config/harvest-tool';\nimport type { IHarvestable } from '@engine/world/config/harvestable-object';\nimport { selectWeightedItem } from '@engine/world/config/harvestable-object';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { checkForGemBoost } from '@engine/world/skill-util/glory-boost';\nimport { rollGemType } from '@engine/world/skill-util/harvest-roll';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { canMine } from './chance';\n\n/**\n * A task that handles mining. It is a subclass of ActorLandscapeObjectInteractionTask, which means that it will\n * walk to the object, and then execute the task when it is in range.\n *\n * The mining task will repeat until the player's inventory is full, or the rock is depleted, or the task is otherwise\n * stopped.\n *\n * @author jameskmonger\n */\nexport class MiningTask extends ActorLandscapeObjectInteractionTask<Player> {\n    /**\n     * The number of ticks that have elapsed since this task was started.\n     *\n     * We use this to determine when to mine the next ore, or play the next animation.\n     */\n    private elapsedTicks = 0;\n\n    /**\n     * The name of the item that we are mining.\n     */\n    private targetItemName: string;\n\n    constructor(\n        player: Player,\n        landscapeObject: LandscapeObject,\n        private readonly ore: IHarvestable,\n        private readonly tool: HarvestTool,\n    ) {\n        super(player, landscapeObject);\n\n        const itemConfigId = typeof ore.items === 'string' ? ore.items : selectWeightedItem(ore.items);\n        const item = findItem(itemConfigId);\n\n        if (!item) {\n            throw new Error(`Could not find item with ID ${itemConfigId}`);\n        }\n\n        this.targetItemName = item.name.toLowerCase().replace(' ore', '');\n    }\n\n    private isGemRock(): boolean {\n        return this.landscapeObject?.objectId === 2111;\n    }\n\n    private hasChargedGlory(): boolean {\n        const neckSlotIndex = equipmentIndices['neck']; // This is 2\n        const neckItem = this.actor.equipment.items[neckSlotIndex];\n        if (!neckItem) {\n            return false;\n        }\n        const itemConfig = findItem(neckItem.itemId);\n        if (!itemConfig) {\n            return false;\n        }\n        return itemConfig.key.startsWith('rs:amulet_of_glory:charged_');\n    }\n\n    public execute(): void {\n        const taskIteration = this.elapsedTicks++;\n\n        // This will be null if the player is not in range of the object.\n        if (!this.landscapeObject) {\n            return;\n        }\n\n        if (!this.hasLevel()) {\n            this.actor.sendMessage(`You need a Mining level of ${this.ore.level} to mine this rock.`, true);\n            return;\n        }\n\n        if (!this.hasMaterials()) {\n            this.actor.sendMessage('You do not have a pickaxe for which you have the level to use.');\n            return;\n        }\n\n        // Check if the players inventory is full, and notify them if its full.\n        if (!this.actor.inventory.hasSpace()) {\n            this.actor.sendMessage(`Your inventory is too full to hold any more ${this.targetItemName}.`, true);\n            this.actor.playSound(soundIds.inventoryFull);\n            return;\n        }\n\n        // mining in original plugin took 3 ticks to mine a rock, so we'll do the same for now\n        if (taskIteration % 3 !== 0) {\n            return;\n        }\n\n        this.actor.playSound(soundIds.pickaxeSwing, 7, 0);\n        this.actor.playAnimation(this.tool.animation);\n\n        // Get tool level, and set it to 2 if the tool is an iron hatchet or iron pickaxe\n        // TODO why is this set to 2? Was ported from the old code\n        let toolLevel = this.tool.level - 1;\n        if (this.tool.itemId === 1349 || this.tool.itemId === 1267) {\n            toolLevel = 2;\n        }\n\n        // roll for success\n        const succeeds = canMine({ ...this.ore, baseChance: this.getGemMiningChance() }, toolLevel, this.actor.skills.mining.level);\n        if (!succeeds) {\n            return;\n        }\n\n        const findsRareGem = randomBetween(1, checkForGemBoost(this.actor)) === 1;\n        if (findsRareGem) {\n            this.actor.sendMessage(colorText('You found a rare gem.', colors.red));\n            this.actor.giveItem(rollGemType());\n        } else {\n            this.actor.sendMessage(`You manage to mine some ${this.targetItemName}.`);\n            const itemToGive = typeof this.ore.items === 'string' ? this.ore.items : selectWeightedItem(this.ore.items);\n            this.actor.giveItem(itemToGive);\n            // TODO (Jameskmonger) handle Gem rocks and Pure essence rocks\n            // if (itemToAdd === 1436 && details.player.skills.hasLevel(Skill.MINING, 30)) {\n            //     itemToAdd = 7936;\n            // }\n            // if (details.object.objectId === 2111 && details.player.skills.hasLevel(Skill.MINING, 30)) {\n            //     itemToAdd = rollGemRockResult().itemId;\n            // }\n        }\n\n        this.actor.skills.addExp(Skill.MINING, this.ore.experience);\n\n        // check if the rock is depleted\n        if (randomBetween(0, 100) <= this.ore.break) {\n            this.actor.playSound(soundIds.oreDepeleted);\n            this.actor.playAnimation(null);\n\n            const replacementObject = this.ore.objects.get(this.landscapeObject.objectId);\n\n            if (replacementObject) {\n                const respawnTime = randomBetween(this.ore.respawnLow, this.ore.respawnHigh);\n                this.actor.instance.replaceGameObject(replacementObject, this.landscapeObject, respawnTime);\n            }\n\n            this.stop();\n            return;\n        }\n    }\n\n    /**\n     * Checks if the player has the pickaxe they started with.\n     *\n     * @returns true if the player has the pickaxe, false otherwise\n     */\n    private hasMaterials() {\n        return this.actor.inventory.has(this.tool.itemId);\n    }\n\n    private getGemMiningChance(): number {\n        if (!this.isGemRock()) {\n            return this.ore.baseChance;\n        }\n\n        // Base chance scaling from 28 to 70 based on level\n        let chance = this.ore.baseChance + ((this.actor.skills.mining.level - this.ore.level) * (70 - 28)) / (99 - 40);\n\n        // Glory multiplies chance by 3 (from 28-70 to 84-210)\n        if (this.hasChargedGlory()) {\n            chance *= 3;\n        }\n\n        return chance;\n    }\n\n    /**\n     * Check that the player still has the level to mine the ore.\n     *\n     * @returns true if the player has the level, false otherwise\n     */\n    private hasLevel() {\n        return this.actor.skills.hasLevel(Skill.MINING, this.ore.level);\n    }\n}\n"
  },
  {
    "path": "src/plugins/skills/mining/mining.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { Skill } from '@engine/world/actor/skills';\nimport { getBestPickaxe } from '@engine/world/config/harvest-tool';\nimport { getAllOreIds, getOreFromRock } from '@engine/world/config/harvestable-object';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { MiningTask } from './mining-task';\n\nconst action: objectInteractionActionHandler = details => {\n    // Get the mining details for the target rock\n    const ore = getOreFromRock(details.object.objectId);\n\n    if (!ore) {\n        details.player.sendMessage('There is current no ore available in this rock.');\n        details.player.playSound(soundIds.oreEmpty, 7, 0);\n        return;\n    }\n\n    if (!details.player.skills.hasLevel(Skill.MINING, ore.level)) {\n        details.player.sendMessage(`You need a Mining level of ${ore.level} to mine this rock.`, true);\n        return;\n    }\n\n    const tool = getBestPickaxe(details.player);\n\n    if (!tool) {\n        details.player.sendMessage('You do not have a pickaxe for which you have the level to use.');\n        return;\n    }\n\n    if (!tool) {\n        return;\n    }\n\n    details.player.sendMessage('You swing your pick at the rock.');\n    details.player.face(details.position);\n    details.player.playAnimation(tool.animation);\n\n    //handleHarvesting(details, tool, ore, Skill.MINING);\n    details.player.enqueueTask(MiningTask, [details.object, ore, tool]);\n};\n\nexport default {\n    pluginId: 'rs:mining',\n    hooks: [\n        {\n            type: 'object_interaction',\n            options: ['mine'],\n            objectIds: getAllOreIds(),\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/mining/prospecting.plugin.ts",
    "content": "import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem } from '@engine/config/config-handler';\nimport { getAllOreIds, getOreFromRock } from '@engine/world/config/harvestable-object';\nimport { soundIds } from '@engine/world/config/sound-ids';\n\nconst action: objectInteractionActionHandler = details => {\n    details.player.sendMessage('You examine the rock for ores.');\n    details.player.face(details.position);\n    const ore = getOreFromRock(details.object.objectId);\n    details.player.playSound(soundIds.oreEmpty, 7, 0);\n\n    const itemConfigId = typeof ore.items === 'string' ? ore.items : ore.items[0].itemConfigId;\n    const oreItem = findItem(itemConfigId);\n\n    if (!oreItem) {\n        details.player.sendMessage('Sorry, something went wrong. Please report this to a developer.');\n        return;\n    }\n\n    // this used to use `setInterval` but will need rewriting to be synced with ticks\n    // see https://github.com/runejs/server/issues/417\n    details.player.sendMessage('[debug] see issue #417');\n    // setTimeout(() => {\n    //     if (!ore) {\n    //         details.player.sendMessage('There is current no ore available in this rock.');\n    //         return;\n    //     }\n    //     const oreName = oreItem.name.toLowerCase().replace(' ore', '');\n\n    //     details.player.sendMessage(`This rock contains ${oreName}.`);\n    // }, World.TICK_LENGTH * 3);\n};\n\nexport default {\n    pluginId: 'rs:prospecting',\n    hooks: [\n        {\n            type: 'object_interaction',\n            options: ['prospect'],\n            objectIds: getAllOreIds(),\n            walkTo: true,\n            handler: action,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/prayer/bury-bones.plugin.ts",
    "content": "import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport { Achievements, giveAchievement } from '@engine/world/actor/player/achievements';\nimport { Skill } from '@engine/world/actor/skills';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\n\nconst action: itemInteractionActionHandler = details => {\n    const { player, option } = details;\n\n    if (option !== 'bury') return;\n\n    if (!player.canMove()) return;\n\n    // bones can be buried only if prayerBuryXp is defined, but they can also\n    // grant zero xp - this checks for that edge case\n    if (!details.itemDetails.metadata.prayerBuryXp && details.itemDetails.metadata.prayerBuryXp !== 0) {\n        return;\n    }\n\n    player.sendMessage(`You bury the ${details.itemDetails.name}.`);\n\n    player.playAnimation(animationIds.buryBones);\n    player.removeItem(details.itemSlot);\n    player.playSound(soundIds.buryBones);\n    player.skills.addExp(Skill.PRAYER, details.itemDetails.metadata.prayerBuryXp);\n\n    giveAchievement(Achievements.BURY_BONES, player);\n};\n\nconst allBones: number[] = [\n    findItem('rs:bones')?.gameId,\n    findItem('rs:bones_burnt')?.gameId,\n    findItem('rs:bones_wolf')?.gameId,\n    findItem('rs:bones_bat')?.gameId,\n    findItem('rs:bones_big')?.gameId,\n    findItem('rs:bones_dagannoth')?.gameId,\n    findItem('rs:bones_babydragon')?.gameId,\n    findItem('rs:bones_dragon')?.gameId,\n    findItem('rs:bones_wyvern')?.gameId,\n    findItem('rs:bones_monkey_normal')?.gameId,\n    findItem('rs:bones_monkey_small_zombie')?.gameId,\n    findItem('rs:bones_monkey_large_zombie')?.gameId,\n    findItem('rs:bones_monkey_gorilla')?.gameId,\n    findItem('rs:bones_monkey_bearded_gorilla')?.gameId,\n    findItem('rs:bones_monkey_small_ninja')?.gameId,\n    findItem('rs:bones_monkey_medium_ninja')?.gameId,\n    findItem('rs:bones_monkey_skeleton_gorilla')?.gameId,\n    findItem('rs:bones_jogre')?.gameId,\n    findItem('rs:bones_zogre')?.gameId,\n    findItem('rs:bones_fayrg')?.gameId,\n    findItem('rs:bones_raurg')?.gameId,\n    findItem('rs:bones_ourg')?.gameId,\n].filter(id => typeof id === 'number') as number[];\n\nexport default {\n    pluginId: 'rs:prayer_bury_bones',\n    hooks: [\n        {\n            type: 'item_interaction',\n            widgets: widgets.inventory,\n            options: 'bury',\n            itemIds: allBones,\n            handler: action,\n            cancelOtherActions: true,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/runecrafting/runecrafting-altar.plugin.ts",
    "content": "import type { ItemOnObjectAction, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport type { ObjectInteractionAction, objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport type { Item } from '@engine/world/items/item';\n/**\n * @Author NickNick\n */\nimport { altars, getEntityByAttr, getEntityIds, runes, talismans } from '@plugins/skills/runecrafting/runecrafting-constants';\nimport type { RunecraftingAltar } from '@plugins/skills/runecrafting/runecrafting-types';\nimport { logger } from '@runejs/common';\n\nconst enterAltar: itemOnObjectActionHandler = (details: ItemOnObjectAction) => {\n    const { player, object, item } = details;\n    const altar = getEntityByAttr(altars, 'entranceId', object.objectId);\n    const rune = getEntityByAttr(runes, 'altar.entranceId', object.objectId);\n\n    if (!altar) {\n        logger.error(`No altar [entrance] found for runecrafting altar plugin: ${object.objectId}`);\n        return;\n    }\n\n    if (!rune) {\n        logger.error(`No rune found for runecrafting altar plugin: ${object.objectId}`);\n        return;\n    }\n\n    if (item.itemId === itemIds.talismans.elemental) {\n        if (\n            rune.talisman.id === itemIds.talismans.air ||\n            rune.talisman.id === itemIds.talismans.water ||\n            rune.talisman.id === itemIds.talismans.earth ||\n            rune.talisman.id === itemIds.talismans.fire\n        ) {\n            finishEnterAltar(player, item, altar);\n            return;\n        }\n    }\n\n    // Wrong talisman.\n    if (item.itemId !== rune.talisman.id) {\n        player.sendMessage('Nothing interesting happens.');\n        return;\n    }\n\n    // Correct talisman.\n    if (item.itemId === rune.talisman.id) {\n        finishEnterAltar(player, item, altar);\n    }\n};\n\nfunction finishEnterAltar(player: Player, item: Item, altar: RunecraftingAltar): void {\n    const talisman = findItem(item.itemId);\n\n    if (!talisman) {\n        logger.error(`No talisman found for runecrafting altar plugin: ${item.itemId}`);\n        return;\n    }\n\n    player.sendMessage(`You hold the ${talisman.name} towards the mysterious ruins.`);\n    player.sendMessage(`You feel a powerful force take hold of you..`);\n    player.teleport(altar.entrance);\n}\n\nconst exitAltar: objectInteractionActionHandler = (details: ObjectInteractionAction) => {\n    const { player, object } = details;\n    const altar = getEntityByAttr(altars, 'portalId', object.objectId);\n\n    if (!altar) {\n        logger.error(`No altar [exit] found for runecrafting altar plugin: ${object.objectId}`);\n        return;\n    }\n\n    player.teleport(altar.exit);\n};\n\nexport default {\n    pluginId: 'rs:runecrafting_altars',\n    hooks: [\n        {\n            type: 'item_on_object',\n            itemIds: getEntityIds(talismans, 'id'),\n            objectIds: getEntityIds(altars, 'entranceId'),\n            walkTo: true,\n            handler: enterAltar,\n        },\n        {\n            type: 'object_interaction',\n            objectIds: getEntityIds(altars, 'portalId'),\n            walkTo: true,\n            handler: exitAltar,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/runecrafting/runecrafting-constants.ts",
    "content": "/**\n * @Author NickNick\n */\n/*\n    RUNECRAFTING Tiara Configs\n    Air - config 491 1\n    Mind - config 491 2\n    Water - config 491 4\n    Earth - config 491 8\n    Fire - config 491 16\n    Body - config 491 32\n    Cosmic - config 491 64\n    Chaos - config 491 128\n    Nature - config 491 256\n    Law - config 491 512\n    Death - config 491 1024\n */\n\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { Position } from '@engine/world/position';\nimport type {\n    RunecraftingAltar,\n    RunecraftingCombinationRune,\n    RunecraftingRune,\n    RunecraftingTalisman,\n    RunecraftingTiara,\n} from '@plugins/skills/runecrafting/runecrafting-types';\n\nexport const tiaras: Map<string, RunecraftingTiara> = new Map<string, RunecraftingTiara>([\n    [\n        'air',\n        {\n            id: itemIds.tiaras.air,\n            config: 1,\n            level: 1,\n            xp: 25.0,\n            recipe: { ingredients: [itemIds.talismans.air, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'mind',\n        {\n            id: itemIds.tiaras.mind,\n            config: 2,\n            level: 1,\n            xp: 27.5,\n            recipe: { ingredients: [itemIds.talismans.mind, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'water',\n        {\n            id: itemIds.tiaras.water,\n            config: 4,\n            level: 1,\n            xp: 30,\n            recipe: { ingredients: [itemIds.talismans.water, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'body',\n        {\n            id: itemIds.tiaras.body,\n            config: 32,\n            level: 1,\n            xp: 37.5,\n            recipe: { ingredients: [itemIds.talismans.body, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'earth',\n        {\n            id: itemIds.tiaras.earth,\n            config: 8,\n            level: 1,\n            xp: 32.5,\n            recipe: { ingredients: [itemIds.talismans.earth, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'fire',\n        {\n            id: itemIds.tiaras.fire,\n            config: 16,\n            level: 1,\n            xp: 35,\n            recipe: { ingredients: [itemIds.talismans.fire, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'cosmic',\n        {\n            id: itemIds.tiaras.cosmic,\n            config: 64,\n            level: 1,\n            xp: 40,\n            recipe: { ingredients: [itemIds.talismans.cosmic, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'nature',\n        {\n            id: itemIds.tiaras.nature,\n            config: 256,\n            level: 1,\n            xp: 45,\n            recipe: { ingredients: [itemIds.talismans.nature, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'chaos',\n        {\n            id: itemIds.tiaras.chaos,\n            config: 128,\n            level: 1,\n            xp: 42.5,\n            recipe: { ingredients: [itemIds.talismans.chaos, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'law',\n        {\n            id: itemIds.tiaras.law,\n            config: 512,\n            level: 1,\n            xp: 47.5,\n            recipe: { ingredients: [itemIds.talismans.law, itemIds.tiaras.blank] },\n        },\n    ],\n    [\n        'death',\n        {\n            id: itemIds.tiaras.death,\n            config: 1024,\n            level: 1,\n            xp: 50,\n            recipe: { ingredients: [itemIds.talismans.death, itemIds.tiaras.blank] },\n        },\n    ],\n]);\n\nexport const talismans: Map<string, RunecraftingTalisman> = new Map<string, RunecraftingTalisman>([\n    ['air', { id: itemIds.talismans.air }],\n    ['mind', { id: itemIds.talismans.mind }],\n    ['water', { id: itemIds.talismans.water }],\n    ['body', { id: itemIds.talismans.body }],\n    ['earth', { id: itemIds.talismans.earth }],\n    ['fire', { id: itemIds.talismans.fire }],\n    ['cosmic', { id: itemIds.talismans.cosmic }],\n    ['nature', { id: itemIds.talismans.nature }],\n    ['chaos', { id: itemIds.talismans.chaos }],\n    ['law', { id: itemIds.talismans.law }],\n    ['death', { id: itemIds.talismans.death }],\n    ['elemental', { id: itemIds.talismans.elemental }],\n]);\n\nexport const altars: Map<string, RunecraftingAltar> = new Map<string, RunecraftingAltar>([\n    [\n        'air',\n        {\n            entranceId: 2452,\n            craftingId: 2478,\n            portalId: 2465,\n            entrance: new Position(2841, 4829, 0),\n            exit: new Position(2983, 3292, 0),\n        },\n    ],\n    [\n        'mind',\n        {\n            entranceId: 2453,\n            craftingId: 2479,\n            portalId: 2466,\n            entrance: new Position(2793, 4828, 0),\n            exit: new Position(2980, 3514, 0),\n        },\n    ],\n    [\n        'water',\n        {\n            entranceId: 2454,\n            craftingId: 2480,\n            portalId: 2467,\n            entrance: new Position(2726, 4832, 0),\n            exit: new Position(3187, 3166, 0),\n        },\n    ],\n    [\n        'earth',\n        {\n            entranceId: 2455,\n            craftingId: 2481,\n            portalId: 2468,\n            entrance: new Position(2655, 4830, 0),\n            exit: new Position(3304, 3474, 0),\n        },\n    ],\n    [\n        'fire',\n        {\n            entranceId: 2456,\n            craftingId: 2482,\n            portalId: 2469,\n            entrance: new Position(2574, 4849, 0),\n            exit: new Position(3311, 3256, 0),\n        },\n    ],\n    [\n        'body',\n        {\n            entranceId: 2457,\n            craftingId: 2483,\n            portalId: 2470,\n            entrance: new Position(2524, 4825, 0),\n            exit: new Position(3051, 3445, 0),\n        },\n    ],\n    [\n        'cosmic',\n        {\n            entranceId: 2458,\n            craftingId: 2484,\n            portalId: 2471,\n            entrance: new Position(2142, 4813, 0),\n            exit: new Position(2408, 4379, 0),\n        },\n    ],\n    [\n        'law',\n        {\n            entranceId: 2459,\n            craftingId: 2485,\n            portalId: 2472,\n            entrance: new Position(2464, 4818, 0),\n            exit: new Position(2858, 3379, 0),\n        },\n    ],\n    [\n        'nature',\n        {\n            entranceId: 2460,\n            craftingId: 2486,\n            portalId: 2473,\n            entrance: new Position(2400, 4835, 0),\n            exit: new Position(2867, 3019, 0),\n        },\n    ],\n    [\n        'chaos',\n        {\n            entranceId: 2461,\n            craftingId: 2487,\n            portalId: 2474,\n            entrance: new Position(2268, 4842, 0),\n            exit: new Position(3058, 3591, 0),\n        },\n    ],\n    [\n        'death',\n        {\n            entranceId: 2462,\n            craftingId: 2488,\n            portalId: 2475,\n            entrance: new Position(2208, 4830, 0),\n            exit: new Position(3222, 3222, 0),\n        },\n    ],\n]);\n\nexport const runes: Map<string, RunecraftingRune> = new Map<string, RunecraftingRune>([\n    [\n        'air',\n        {\n            id: 556,\n            xp: 5.0,\n            level: 1,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('air'),\n            talisman: talismans.get('air'),\n            tiara: tiaras.get('air'),\n        } as RunecraftingRune,\n    ],\n    [\n        'mind',\n        {\n            id: 558,\n            xp: 5.5,\n            level: 1,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('mind'),\n            talisman: talismans.get('mind'),\n            tiara: tiaras.get('mind'),\n        } as RunecraftingRune,\n    ],\n    [\n        'water',\n        {\n            id: 555,\n            xp: 6,\n            level: 5,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('water'),\n            talisman: talismans.get('water'),\n            tiara: tiaras.get('water'),\n        } as RunecraftingRune,\n    ],\n    [\n        'earth',\n        {\n            id: 557,\n            xp: 6.5,\n            level: 9,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('earth'),\n            talisman: talismans.get('earth'),\n            tiara: tiaras.get('earth'),\n        } as RunecraftingRune,\n    ],\n    [\n        'fire',\n        {\n            id: 554,\n            xp: 7.0,\n            level: 14,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('fire'),\n            talisman: talismans.get('fire'),\n            tiara: tiaras.get('fire'),\n        } as RunecraftingRune,\n    ],\n    [\n        'body',\n        {\n            id: 559,\n            xp: 7.5,\n            level: 20,\n            essence: [itemIds.essence.pure, itemIds.essence.rune],\n            altar: altars.get('body'),\n            talisman: talismans.get('body'),\n            tiara: tiaras.get('body'),\n        } as RunecraftingRune,\n    ],\n    [\n        'cosmic',\n        {\n            id: 564,\n            xp: 8.0,\n            level: 27,\n            essence: [itemIds.essence.pure],\n            altar: altars.get('cosmic'),\n            talisman: talismans.get('cosmic'),\n            tiara: tiaras.get('cosmic'),\n        } as RunecraftingRune,\n    ],\n    [\n        'chaos',\n        {\n            id: 562,\n            xp: 8.5,\n            level: 35,\n            essence: [itemIds.essence.pure],\n            altar: altars.get('chaos'),\n            talisman: talismans.get('chaos'),\n            tiara: tiaras.get('chaos'),\n        } as RunecraftingRune,\n    ],\n    [\n        'nature',\n        {\n            id: 561,\n            xp: 9.0,\n            level: 44,\n            essence: [itemIds.essence.pure],\n            altar: altars.get('nature'),\n            talisman: talismans.get('nature'),\n            tiara: tiaras.get('nature'),\n        } as RunecraftingRune,\n    ],\n    [\n        'law',\n        {\n            id: 563,\n            xp: 9.5,\n            level: 54,\n            essence: [itemIds.essence.pure],\n            altar: altars.get('law'),\n            talisman: talismans.get('law'),\n            tiara: tiaras.get('law'),\n        } as RunecraftingRune,\n    ],\n    [\n        'death',\n        {\n            id: 560,\n            xp: 10.0,\n            level: 65,\n            essence: [itemIds.essence.pure],\n            altar: altars.get('death'),\n            talisman: talismans.get('death'),\n            tiara: tiaras.get('death'),\n        } as RunecraftingRune,\n    ],\n]);\n\nexport const combinationRunes: Map<string, RunecraftingCombinationRune> = new Map<string, RunecraftingCombinationRune>([\n    [\n        'mist',\n        {\n            altar: [altars.get('air'), altars.get('water')],\n            id: 4695,\n            level: 7,\n            talisman: [talismans.get('air'), talismans.get('water')],\n            tiara: [tiaras.get('air'), tiaras.get('water')],\n            runes: [runes.get('air'), runes.get('water')],\n            xp: [8.0, 8.5],\n        } as RunecraftingCombinationRune,\n    ],\n    [\n        'dust',\n        {\n            id: 4696,\n            level: 10,\n            xp: [8.3, 9.0],\n            altar: [altars.get('air'), altars.get('earth')],\n            talisman: [talismans.get('air'), talismans.get('earth')],\n            runes: [runes.get('air'), runes.get('earth')],\n            tiara: [tiaras.get('air'), tiaras.get('earth')],\n        } as RunecraftingCombinationRune,\n    ],\n    [\n        'mud',\n        {\n            id: 4698,\n            level: 13,\n            xp: [9.3, 9.5],\n            altar: [altars.get('water'), altars.get('earth')],\n            talisman: [talismans.get('water'), talismans.get('earth')],\n            runes: [runes.get('water'), runes.get('earth')],\n            tiara: [tiaras.get('water'), tiaras.get('earth')],\n        } as RunecraftingCombinationRune,\n    ],\n    [\n        'smoke',\n        {\n            id: 4697,\n            level: 15,\n            xp: [8.5, 9.5],\n            altar: [altars.get('air'), altars.get('fire')],\n            talisman: [talismans.get('air'), talismans.get('fire')],\n            runes: [runes.get('air'), runes.get('fire')],\n            tiara: [tiaras.get('air'), tiaras.get('fire')],\n        } as RunecraftingCombinationRune,\n    ],\n    [\n        'steam',\n        {\n            id: 4694,\n            level: 19,\n            xp: [9.5, 10.0],\n            altar: [altars.get('water'), altars.get('fire')],\n            talisman: [talismans.get('water'), talismans.get('fire')],\n            runes: [runes.get('water'), runes.get('fire')],\n            tiara: [tiaras.get('water'), tiaras.get('fire')],\n        } as RunecraftingCombinationRune,\n    ],\n    [\n        'lava',\n        {\n            id: 4699,\n            level: 23,\n            xp: [10.0, 10.5],\n            altar: [altars.get('earth'), altars.get('fire')],\n            talisman: [talismans.get('earth'), talismans.get('fire')],\n            runes: [runes.get('earth'), runes.get('fire')],\n            tiara: [tiaras.get('earth'), tiaras.get('fire')],\n        } as RunecraftingCombinationRune,\n    ],\n]);\n\nexport function getEntityByAttr<T>(entities: Map<any, T>, attr: string, value: unknown): T | null {\n    let entity: T | null = null;\n    const splits = attr.split('.');\n\n    // Handles dot seperated attribute names.\n    if (splits.length === 2) {\n        entities.forEach(e => {\n            if (e[splits[0]][splits[1]] === value) {\n                entity = e;\n            }\n        });\n    }\n\n    // Handles single attribute name.\n    if (splits.length === 1) {\n        entities.forEach(e => {\n            if (e[attr] === value) {\n                entity = e;\n            }\n        });\n    }\n\n    return entity;\n}\n\nexport function getEntityIds<T>(entities: Map<any, T>, property: keyof T): number[] {\n    const entityIds: number[] = [];\n    entities.forEach((entity: T) => {\n        if (entity[property] && typeof entity[property] === 'number') {\n            const tempnum: number = entity[property] as any;\n            entityIds.push(tempnum);\n        }\n    });\n    return entityIds;\n}\n\nexport function runeMultiplier(runeId: number, level: number): number {\n    switch (runeId) {\n        case 556:\n            return Math.floor(level / 11.0) + 1;\n        case 558:\n            return Math.floor(level / 14.0) + 1;\n        case 555:\n            return Math.floor(level / 19.0) + 1;\n        case 557:\n            return Math.floor(level / 26.0) + 1;\n        case 554:\n            return Math.floor(level / 35.0) + 1;\n        case 559:\n            return Math.floor(level / 46.0) + 1;\n        case 564:\n            return Math.floor(level / 59.0) + 1;\n        case 562:\n            return Math.floor(level / 74.0) + 1;\n        case 561:\n            return Math.floor(level / 91.0) + 1;\n        case 563:\n            return 1.0;\n        case 560:\n            return 1.0;\n    }\n\n    return 1.0;\n}\n"
  },
  {
    "path": "src/plugins/skills/runecrafting/runecrafting-crafting.plugin.ts",
    "content": "import type { ItemOnObjectAction, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport type { ObjectInteractionAction, objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action';\nimport { findItem, widgets } from '@engine/config/config-handler';\nimport { randomBetween } from '@engine/util/num';\n/**\n * @Author NickNick\n */\nimport { Skill } from '@engine/world/actor/skills';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport {\n    altars,\n    combinationRunes,\n    getEntityByAttr,\n    getEntityIds,\n    runeMultiplier,\n    runes,\n} from '@plugins/skills/runecrafting/runecrafting-constants';\nimport type { RunecraftingCombinationRune } from '@plugins/skills/runecrafting/runecrafting-types';\nimport { logger } from '@runejs/common';\n\nconst craftRune: objectInteractionActionHandler = (details: ObjectInteractionAction) => {\n    const { player, object } = details;\n    const rune = getEntityByAttr(runes, 'altar.craftingId', object.objectId);\n\n    if (!rune) {\n        logger.error(`No rune [crafting] found for runecrafting plugin: ${object.objectId}`);\n        return;\n    }\n\n    const runeDetails = findItem(rune.id);\n\n    if (!runeDetails) {\n        logger.warn(`Could not find rune details for rune id ${rune.id}`);\n        return;\n    }\n\n    const level = player.skills.get(Skill.RUNECRAFTING).level;\n    if (level < rune.level) {\n        player.sendMessage(`You need a runecrafting level of ${rune.level} to craft ${runeDetails.name}.`);\n        return;\n    }\n    let essenceAvailable = 0;\n    rune.essence.forEach(essenceId => {\n        essenceAvailable += player.inventory.findAll(essenceId).length;\n    });\n\n    if (essenceAvailable > 0) {\n        // Remove essence from inventory.\n        rune.essence.forEach(essenceId => {\n            player.inventory.findAll(essenceId).forEach(index => {\n                player.inventory.remove(index);\n            });\n        });\n        // Add crafted runes to inventory.\n        player.inventory.add({ itemId: rune.id, amount: runeMultiplier(rune.id, level) * essenceAvailable });\n        // Add experience\n        player.skills.addExp(Skill.RUNECRAFTING, rune.xp * essenceAvailable);\n        // Update widget items.\n        player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n        return;\n    }\n\n    player.sendMessage(`You do not have any rune essence to bind.`);\n};\n\nfunction getCombinationRuneByAltar(itemId: number, objectId: number): RunecraftingCombinationRune | undefined {\n    for (const combinationRune of combinationRunes.values()) {\n        const altarIndex = combinationRune.altar.findIndex(altar => altar.craftingId === objectId);\n        if (altarIndex > -1 && combinationRune.talisman[altarIndex ^ 1].id === itemId) {\n            return combinationRune;\n        }\n    }\n    return undefined;\n}\n\nconst craftCombinationRune: itemOnObjectActionHandler = (details: ItemOnObjectAction) => {\n    const { player, object, item } = details;\n    const rune = getCombinationRuneByAltar(item.itemId, object.objectId);\n    if (!rune) {\n        player.sendMessage('Nothing interesting happens.');\n        return;\n    }\n\n    const altarIndex = rune.altar.findIndex(altar => object.objectId === altar.craftingId);\n    const shouldBreakTalisman = randomBetween(0, 1) === 1;\n    const requiredRunesIndex = player.inventory.findIndex(rune.runes[altarIndex ^ 1].id);\n    if (requiredRunesIndex < 0) {\n        player.sendMessage(`You don't have any runes to bind.`);\n        return;\n    }\n    const runeDetails = findItem(rune.id);\n\n    if (!runeDetails) {\n        logger.warn(`Could not find rune details for rune id ${rune.id}`);\n        return;\n    }\n\n    const level = player.skills.get(Skill.RUNECRAFTING).level;\n    if (level < rune.level) {\n        player.sendMessage(`You need a runecrafting level of ${rune.level} to craft ${runeDetails.name}.`);\n        return;\n    }\n\n    const essenceAvailable = player.inventory.findAll(itemIds.essence.pure).length;\n    const requiredRunesAvailable = player.inventory.amountInStack(requiredRunesIndex);\n    if (essenceAvailable > 0 && requiredRunesIndex > 0) {\n        const amountToCraft = Math.min(essenceAvailable, requiredRunesAvailable);\n\n        // Remove runes from inventory\n        if (amountToCraft === requiredRunesAvailable) {\n            player.inventory.remove(requiredRunesIndex, false);\n        } else {\n            player.inventory.set(requiredRunesIndex, {\n                itemId: rune.runes[altarIndex ^ 1].id,\n                amount: requiredRunesAvailable - amountToCraft,\n            });\n        }\n        // Remove essence from inventory.\n        for (let i = 0; i < amountToCraft; i++) {\n            player.inventory.removeFirst(itemIds.essence.pure);\n        }\n        // Add crafted runes to inventory.\n        player.inventory.add({ itemId: rune.id, amount: amountToCraft });\n        // Add experience\n        player.skills.addExp(Skill.RUNECRAFTING, rune.xp[altarIndex] * essenceAvailable);\n        if (shouldBreakTalisman) {\n            player.inventory.removeFirst(item.itemId);\n        }\n        // Update widget items.\n        player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory);\n        player.sendMessage(`You craft some ${runeDetails.name}.`);\n        return;\n    }\n    //\n    player.sendMessage(`You do not have any pure essence to bind.`);\n};\n\nexport default {\n    pluginId: 'rs:runecrafting',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: getEntityIds(altars, 'craftingId'),\n            walkTo: true,\n            handler: craftRune,\n        },\n        {\n            type: 'item_on_object',\n            objectIds: getEntityIds(altars, 'craftingId'),\n            walkTo: true,\n            handler: craftCombinationRune,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/runecrafting/runecrafting-tiara.plugin.ts",
    "content": "/**\n * @Author NickNick\n */\n\nimport type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action';\nimport { getEntityByAttr, getEntityIds, tiaras } from '@plugins/skills/runecrafting/runecrafting-constants';\nimport { logger } from '@runejs/common';\n\nconst unequipTiara: equipmentChangeActionHandler = details => {\n    const { player } = details;\n    player.outgoingPackets.updateClientConfig(491, 0);\n};\n\nconst equipTiara: equipmentChangeActionHandler = details => {\n    const { player, itemId } = details;\n    const tiara = getEntityByAttr(tiaras, 'id', itemId);\n\n    if (!tiara) {\n        logger.error(`No tiara [equipping] found for runecrafting plugin: ${itemId}`);\n        return;\n    }\n\n    player.outgoingPackets.updateClientConfig(491, tiara.config);\n};\n\nexport default {\n    pluginId: 'rs:runecrafting_tiaras',\n    hooks: [\n        {\n            type: 'equipment_change',\n            eventType: 'equip',\n            itemIds: getEntityIds(tiaras, 'id'),\n            handler: equipTiara,\n        },\n        {\n            type: 'equipment_change',\n            eventType: 'unequip',\n            itemIds: getEntityIds(tiaras, 'id'),\n            handler: unequipTiara,\n        },\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/runecrafting/runecrafting-types.ts",
    "content": "/**\n * @Author NickNick\n */\n\nimport type { Item } from '@engine/world/items/item';\nimport type { Position } from '@engine/world/position';\n\nexport interface RunecraftingRecipe {\n    ingredients: Item[] | number[];\n}\n\nexport interface RunecraftingTiara {\n    id: number;\n    config: number;\n    recipe: RunecraftingRecipe;\n    level: number;\n    xp: number;\n}\n\nexport interface RunecraftingTalisman {\n    id: number;\n}\n\nexport interface RunecraftingAltar {\n    entranceId: number;\n    craftingId: number;\n    portalId: number;\n    entrance: Position;\n    exit: Position;\n}\n\nexport interface RunecraftingRune {\n    id: number;\n    xp: number;\n    level: number;\n    essence: number[];\n    altar: RunecraftingAltar;\n    tiara: RunecraftingTiara;\n    talisman: RunecraftingTalisman;\n}\nexport interface RunecraftingCombinationRune {\n    id: number;\n    xp: [number, number];\n    level: number;\n    altar: [RunecraftingAltar, RunecraftingAltar];\n    tiara: [RunecraftingTiara, RunecraftingTiara];\n    talisman: [RunecraftingTalisman, RunecraftingTalisman];\n    runes: [RunecraftingRune, RunecraftingRune];\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/Strength.json",
    "content": "{\n    \"id\": 121,\n    \"name\": \"Strength\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Weapons\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:black_halberd\",\n                    \"text\": \"Black halberd\\\\n(10 Attack is needed)\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:white_halberd\",\n                    \"text\": \"White halberd\\\\n(10 Attack is needed)\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:mithril_halberd\",\n                    \"text\": \"Mithril halberd\\\\n(20 Attack is needed)\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:adamant_halberd\",\n                    \"text\": \"Adamant halberd\\\\n(30 Attack is needed)\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:rune_halberd\",\n                    \"text\": \"Rune halberd\\\\n(40 Attack is needed)\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:dragon_halberd\",\n                    \"text\": \"Dragon halberd\\\\n(60 Attack is needed)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:granite_maul\",\n                    \"text\": \"Granite maul\\\\n(50 Attack is needed)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:tzhaar-ket-om\",\n                    \"text\": \"TzHaar-Ket-Om\\\\nobsidian maul\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:dharoks_greataxe\",\n                    \"text\": \"Dharok's greataxe\\\\n(70 Attack is needed)\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:torags_hammers\",\n                    \"text\": \"Torag's hammers\\\\n(70 Attack is needed)\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Armour\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:granite\",\n                    \"text\": \"Granite Armour\",\n                    \"level\": 50\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/agility.json",
    "content": "{\n    \"id\": 122,\n    \"name\": \"Agility\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Courses\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:gnome_stronghold_agility_course\",\n                    \"text\": \"Gnome Stronghold Agility Course\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:gnomeball_game\",\n                    \"text\": \"Gnomeball Game\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:werewolf_skullball_game\",\n                    \"text\": \"Werewolf Skullball game\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:agility_pyramid\",\n                    \"text\": \"Agility Pyramid\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:barbarian_outpost_agility_course\",\n                    \"text\": \"Barbarian Outpost Agility Course\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:ape_atoll_agility_course\",\n                    \"text\": \"Ape Atoll Agility Course\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:wilderness_course\",\n                    \"text\": \"Wilderness Course\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:slayer_staff\",\n                    \"text\": \"Werewolf Agility Course\",\n                    \"level\": 60\n                }\n            ]\n        },\n        {\n            \"name\": \"Areas\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:agility_jump_green\",\n                    \"text\": \"Rope-swing to Moss Giant Island\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:agility_jump_green\",\n                    \"text\": \"Stepping stones in Karamja Dungeon\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:agility_jump_green\",\n                    \"text\": \"Monkey bars under Edgeville\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:agility_contortion_green\",\n                    \"text\": \"Pipe contortion in Karamja Dungeon\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:agility_jump_green\",\n                    \"text\": \"Stepping stones in south-eastern Karamja\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:agility_contortion_green\",\n                    \"text\": \"Pipe contortion in Karamja Dungeon\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:agility_balance_green\",\n                    \"text\": \"Elf area log balance\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:agility_contortion_green\",\n                    \"text\": \"Contortion in Yanille Dungeon small room\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:agility_contortion_green\",\n                    \"text\": \"Access the God Wars Dungeon area via the \\\\nAgility route\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Yanille Dungeon's rubble climb\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Enter the Saradomins area of the \\\\nGod Wars Dungeon\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Shortcuts\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Falador Agility shortcut\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:agility_jump_yellow\",\n                    \"text\": \"Jump fence south of Varrock\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Yanille Agility shortcut\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:agility_balance_yellow\",\n                    \"text\": \"Coal Truck log balance\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Falador Agility shortcut\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:agility_balance_yellow\",\n                    \"text\": \"Draynor Manor stones to Champions' Guild\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:agility_balance_yellow\",\n                    \"text\": \"Ardougne log balance shortcut\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Gnome Stronghold shortcut\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Al Kahrid Mining pit cliffside scramble\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Trollheim easy cliffside scramble\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Dwarven Mine narrow crevice\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Trollheim medium cliffside scramble\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Trollheim advanced cliffside scramble\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Cosmic Temple - narrow walkway\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Deep Wilderness - narrow tunnel\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Trollheim hard cliffside scramble\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Pipe from Edgeville dungeon to Varrock Sewers\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Port Phasmatys ectopool shortcut\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:agility_jump_yellow\",\n                    \"text\": \"Elven overpass easy cliffside scamble\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Slayer Tower medium spiked chain climb\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Taverley dungeon lesser demon fence shortcut\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Trollheim Wilderness route\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Temple on the Salve to Morytania shortcut\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Elven overpass medium cliffside scramble\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Taverley Dungeon short-cuts to blue dragons\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:agility_climb\",\n                    \"text\": \"Slayer Tower advanced spiked chain\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:agility_jump_yellow\",\n                    \"text\": \"Taverley Dungeon spiked blades jump\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:agility_jump_yellow\",\n                    \"text\": \"Fremennik Slayer Dungeon spiked blades jump\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:agility_jump_yellow\",\n                    \"text\": \"Brimhaven Dungeon eastern stepping stones\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:agility_contortion_yellow\",\n                    \"text\": \"Iorwerth southern shortcut\",\n                    \"level\": 84\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/attack.json",
    "content": "{\n    \"id\": 118,\n    \"name\": \"Attack\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Weapons\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_dagger\",\n                    \"text\": \"Bronze\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:iron_dagger\",\n                    \"text\": \"Iron\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:steel_dagger\",\n                    \"text\": \"Steel\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:black_dagger\",\n                    \"text\": \"Black\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:white_dagger\",\n                    \"text\": \"Members: White\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:mithril_dagger\",\n                    \"text\": \"Mithril\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:adamant_dagger\",\n                    \"text\": \"Adamant\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:battlestaff\",\n                    \"text\": \"Members: Battlestaves (with 30 Magic)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:rune_dagger\",\n                    \"text\": \"Rune\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:granite_maul\",\n                    \"text\": \"Members: Granite Maul\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:dragon_dagger\",\n                    \"text\": \"Members: Dragon\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:toktz_xil_ak\",\n                    \"text\": \"Members: Obsidian weapons\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:abyssal_whip\",\n                    \"text\": \"Members: Abyssal Whip\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:dharoks_greataxe\",\n                    \"text\": \"Members: Dharok's greataxe\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:torags_hammers\",\n                    \"text\": \"Members: Torag's hammers\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:veracs_flail\",\n                    \"text\": \"Members: Verac's flail\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:guthans_warspear\",\n                    \"text\": \"Members: Guthan's warspear\",\n                    \"level\": 70\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/construction.json",
    "content": "{\n    \"id\": 143,\n    \"name\": \"Construction\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Rooms\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:garden\",\n                    \"text\": \"Garden\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:parlour\",\n                    \"text\": \"Parlour\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:kitchen\",\n                    \"text\": \"Kitchen\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:dining_room\",\n                    \"text\": \"Dining room\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:workshop\",\n                    \"text\": \"Workshop\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:bedroom\",\n                    \"text\": \"Bedroom\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:hall_skill_trophies\",\n                    \"text\": \"Hall (skill trophies)\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:league_hall\",\n                    \"text\": \"Leauge hall\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:games_room\",\n                    \"text\": \"Games room\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:combat_room\",\n                    \"text\": \"Combat room\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:hall_quest_trophies\",\n                    \"text\": \"Hall (quest trophies)\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:menagerie\",\n                    \"text\": \"Menagerie\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:study\",\n                    \"text\": \"Study\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:costume_room\",\n                    \"text\": \"Costume room\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:chapel\",\n                    \"text\": \"Chapel\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:portal_chamber\",\n                    \"text\": \"Portal chamber\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:formal_garden\",\n                    \"text\": \"Formal garden\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:throne_room\",\n                    \"text\": \"Throne room\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:oubliette\",\n                    \"text\": \"Oubliette\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:superior_garden\",\n                    \"text\": \"Superior garden\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:dungeon\",\n                    \"text\": \"Dungeon\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:treasure_room\",\n                    \"text\": \"Treasure room\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:achievement_gallery\",\n                    \"text\": \"Achievement Gallery\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:league_hall\",\n                    \"text\": \"Garden\",\n                    \"level\": 1\n                }\n            ]\n        },\n        {\n            \"name\": \"Skills\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:clay_fireplace\",\n                    \"text\": \"Clay  fireplace\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:firepit\",\n                    \"text\": \"Firepit\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:pump_and_drain\",\n                    \"text\": \"Pump and drain\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:firepit_with_hook\",\n                    \"text\": \"Firepit with hook\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:repair_bench\",\n                    \"text\": \"Repair bench\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:pluming_stand\",\n                    \"text\": \"Pluming stand\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:crafting_table_1\",\n                    \"text\": \"Crafting table 1\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:firepit_with_pot\",\n                    \"text\": \"Firepit with pot\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:wooden_workbench\",\n                    \"text\": \"Wooden workbench\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:small_oven\",\n                    \"text\": \"Small oven\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:crafting_table_2\",\n                    \"text\": \"Crafting table 2\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:pump_and_tub\",\n                    \"text\": \"Pump and tub\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:large_oven\",\n                    \"text\": \"Large oven\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:oak_workbench\",\n                    \"text\": \"Oak workbench\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:stone_fireplace\",\n                    \"text\": \"Stone fireplace\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:crafting_table_3\",\n                    \"text\": \"Crafting table 3\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:steel_range\",\n                    \"text\": \"Steel range\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:whetstone\",\n                    \"text\": \"Whetstone\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:shield_easel\",\n                    \"text\": \"Shield easel\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:fancy_range\",\n                    \"text\": \"Fancy range\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:crafting_table_4\",\n                    \"text\": \"Crafting table 4\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:steel_framed_workbench\",\n                    \"text\": \"Steel framed workbench\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:sink\",\n                    \"text\": \"Sink\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:armour_stand\",\n                    \"text\": \"Armour stand\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:workbench_with_vice\",\n                    \"text\": \"Workbench with vice\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:marble_fireplace\",\n                    \"text\": \"Marble fireplace\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:banner_easel\",\n                    \"text\": \"Banner easel\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:workbench_with_lathe\",\n                    \"text\": \"Workbench with lathe\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:ancient_altar\",\n                    \"text\": \"Ancient altar\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:lunar_altar\",\n                    \"text\": \"Lunar altar\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:dark_altar\",\n                    \"text\": \"Dark altar\",\n                    \"level\": 80\n                }\n            ]\n        },\n        {\n            \"name\": \"Surfaces\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:crude_wooden_chair\",\n                    \"text\": \"Crude wooden chair\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:wooden_chair\",\n                    \"text\": \"Wooden chair\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:wooden_dining_table\",\n                    \"text\": \"Wooden dining table\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:wooden_kitchen_table\",\n                    \"text\": \"Wooden kitchen table\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:rocking_chair\",\n                    \"text\": \"Rocking chair\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:oak_chair\",\n                    \"text\": \"Oak chair\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:wooden_bed\",\n                    \"text\": \"Wooden bed\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:oak_dining_table\",\n                    \"text\": \"Oak dining table\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:oak_dining_bench\",\n                    \"text\": \"Oak dining bench\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:oak_armchair\",\n                    \"text\": \"Oak armchair\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:oak_bed\",\n                    \"text\": \"Oak bed\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:carved_oak_dining_table\",\n                    \"text\": \"Carved oak dining table\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:carved_oak_dining_bench\",\n                    \"text\": \"Carved oak dining bench\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:oak_kitchen_table\",\n                    \"text\": \"Oak kitchen table\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:large_oak_bed\",\n                    \"text\": \"Large oak bed\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:teak_armchair\",\n                    \"text\": \"Teak armchair\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:teak_dining_table\",\n                    \"text\": \"Teak dining table\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:teak_dining_bench\",\n                    \"text\": \"Teak dining bench\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:teak_bed\",\n                    \"text\": \"Teak bed\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:carved_teak_dining_bench\",\n                    \"text\": \"Carved teak dining bench\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:carved_teak_dining_table\",\n                    \"text\": \"Carved teak dining table\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:large_teak_bed\",\n                    \"text\": \"Large teak bed\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:mahogany_armchair\",\n                    \"text\": \"Mahogany armchair\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:mahogany_dining_table\",\n                    \"text\": \"Mahogany dining table\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:mahogany_dining_bench\",\n                    \"text\": \"Mahogany dining bench\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:teak_kitchen_table\",\n                    \"text\": \"Teak kitchen table\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:mahogany_four_poster_bed\",\n                    \"text\": \"Mahogany four-poster bed\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:oak_throne\",\n                    \"text\": \"Oak throne\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_four_poster_bed\",\n                    \"text\": \"Gilded mahogany four-poster bed\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_dining_bench\",\n                    \"text\": \"Gilded mahogany dining bench\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:teak_throne\",\n                    \"text\": \"Teak throne\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_and_marble_table\",\n                    \"text\": \"Gilded mahogany and marble table\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:mahogany_throne\",\n                    \"text\": \"Mahogany throne\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_throne\",\n                    \"text\": \"Gilded mahogany throne\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:teak_kitchen_table\",\n                    \"text\": \"Teak kitchen table\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:teak_kitchen_table\",\n                    \"text\": \"Teak kitchen table\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:teak_kitchen_table\",\n                    \"text\": \"Teak kitchen table\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:skeleton_throne\",\n                    \"text\": \"Skeleton throne\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:crystal_throne\",\n                    \"text\": \"Crystal throne\",\n                    \"level\": 95\n                },\n                {\n                    \"item\": \"rs:demonic_throne\",\n                    \"text\": \"Demonic throne\",\n                    \"level\": 99\n                }\n            ]\n        },\n        {\n            \"name\": \"Storage\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:wooden_bookcase\",\n                    \"text\": \"Wooden bookcase\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:wooden_shelves_1\",\n                    \"text\": \"Wooden shelves 1\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:beer_barrel\",\n                    \"text\": \"Beer barrel\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:wooden_larder\",\n                    \"text\": \"Wooden larder\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:cider_barrel\",\n                    \"text\": \"Cider barrel\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:wooden_shelves_2\",\n                    \"text\": \"Wooden shelves 2\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:tool_store_1\",\n                    \"text\": \"Tool store 1\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:asgarnian_ale_barrel\",\n                    \"text\": \"Asgarnian ale barrel\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:shoe_box\",\n                    \"text\": \"Shoe box\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:wooden_shaving_stand\",\n                    \"text\": \"Wooden shaving stand\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:wooden_shelves_3\",\n                    \"text\": \"Wooden shelves 3\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:tool_store_2\",\n                    \"text\": \"Tool store 2\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:greenmans_ale_barrel\",\n                    \"text\": \"Greenman's Ale barrel\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:oak_chest_of_drawers\",\n                    \"text\": \"oak chest of drawers\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:oak_bookcase\",\n                    \"text\": \"Oak bookcase\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:oak_shaving_stand\",\n                    \"text\": \"Oak shaving stand\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:oak_larder\",\n                    \"text\": \"Oak larder\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:oak_shelves_1\",\n                    \"text\": \"Oak shelves 1\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:tool_store_3\",\n                    \"text\": \"Tool store 3\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:dragon_bitter_barrel\",\n                    \"text\": \"Dragon Bitter barrel\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:oak_dresser\",\n                    \"text\": \"Oak dresser\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:oak_Wardrobe_bedroom\",\n                    \"text\": \"Oak wardrobe (bedroom)\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:mahogany_bookcase\",\n                    \"text\": \"Mahogany bookcase\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:oak_wardrobe_costume_room\",\n                    \"text\": \"Oak wardrobe (costtume room)\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:teak_larder\",\n                    \"text\": \"Teak larder\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:oak_fancy_dress_box\",\n                    \"text\": \"Oak fancy dress box\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:tool_store_4\",\n                    \"text\": \"Tool store 4\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:oak_shelves_2\",\n                    \"text\": \"Oak shelves 2\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:oak_armour_case\",\n                    \"text\": \"Oak armour case\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:teak_dresser\",\n                    \"text\": \"Teak dresser\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:chefs_delight_barrel\",\n                    \"text\": \"Chef's Delight barrel\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:oak_treasure_chest\",\n                    \"text\": \"Oak treasure chest\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:oak_toy_box\",\n                    \"text\": \"Oak toy box\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:carved_oak_wardrobe_costume_room\",\n                    \"text\": \"Carved oak wardrobe (costume room)\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:teak_chest_of_drawers\",\n                    \"text\": \"Teak chest of drawers\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:oak_cape_rack\",\n                    \"text\": \"Oak cape rack\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:tool_store_5\",\n                    \"text\": \"Tool store 5\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:teak_shelves_1\",\n                    \"text\": \"Teak shelves 1\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:fancy_teak_dresser\",\n                    \"text\": \"Fancy teak dresser\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:servants_moneybag\",\n                    \"text\": \"Servant's moneybag\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:teak_wardrobe_costume_room\",\n                    \"text\": \"Teak wardrobe (costume room)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spice_rack\",\n                    \"text\": \"Spice Rack\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:teak_fancy_dress_box\",\n                    \"text\": \"Teak fancy dress box\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:teak_cape_rack\",\n                    \"text\": \"Teak cape rack\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:teak_wardrobe_bedroom\",\n                    \"text\": \"Teak wardrobe (bedroom)\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:teak_armour_case\",\n                    \"text\": \"Teak armour case\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:mahogany_dresser\",\n                    \"text\": \"Mahogany dresser\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:teak_treasure_chest\",\n                    \"text\": \"Teak treasure chest\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:teak_toy_box\",\n                    \"text\": \"Teak toyo boy\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:carved_teak_wardrobe_costume_room\",\n                    \"text\": \"Carved teak wardrobe (costume room)\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:mahogany_cape_rack\",\n                    \"text\": \"Mahogany cape rack\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_dresser\",\n                    \"text\": \"Gilded mahogany dresser\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:mahogany_wardrobe_bedroom\",\n                    \"text\": \"Mahogany wardrobe (bedroom)\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:mahogany_wardrobe_costume_room\",\n                    \"text\": \"Mahogany wardrobe (costume room)\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:mahogany_fancy_dress_box\",\n                    \"text\": \"Mahogany fancy dress box\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:basic_jewellery_box\",\n                    \"text\": \"Basic jewellery box\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_cape_rack\",\n                    \"text\": \"Gilded mahogany cape rack\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:mahogany_armour_case\",\n                    \"text\": \"Mahogany armour case\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:mahogany_treasure_chest\",\n                    \"text\": \"Mahogany treasure chest\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:fancy_jewellery_box\",\n                    \"text\": \"Fancy jewellery box\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:mahogany_toy_box\",\n                    \"text\": \"Mahogany toy box\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_wardrobe_costume_room\",\n                    \"text\": \"Gilded mahogany wardrobe (costume room)\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_wardrobe_bedroom\",\n                    \"text\": \"Gilded mahogany wardrobe (Bedroom)\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:marble_cape_rack\",\n                    \"text\": \"Marble cape rack\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:marble_wardrobe_costume_room\",\n                    \"text\": \"Marble wardrobe (costume room)\",\n                    \"level\": 96\n                },\n                {\n                    \"item\": \"rs:magic_stone_cape_rack\",\n                    \"text\": \"Magic stone cape rack\",\n                    \"level\": 99\n                }\n            ]\n        },\n        {\n            \"name\": \"Decorative\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:redberry_bushes\",\n                    \"text\": \"Redberry bushes\\\\nPayment: Cabbages(10) x4\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:brown_rug\",\n                    \"text\": \"Brown rug\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:torn_curtains\",\n                    \"text\": \"Torn curtains\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:rug\",\n                    \"text\": \"Rug\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:curtains\",\n                    \"text\": \"Curtains\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:oak_clock\",\n                    \"text\": \"Oak clock\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:oak_lectern\",\n                    \"text\": \"Oak lectern\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:opulent_curtains\",\n                    \"text\": \"Opulent curtains\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:globe\",\n                    \"text\": \"globe\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:alchemical_chart\",\n                    \"text\": \"Alchemical chart\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:wooden_telescope\",\n                    \"text\": \"Wooden telescope\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:oak_eagle_lectern\",\n                    \"text\": \"Oak eagle lectern\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:oak_demon_lectern\",\n                    \"text\": \"Oak demon lectern\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:ornamental_globe\",\n                    \"text\": \"Ornamental globe\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:teak_clock\",\n                    \"text\": \"Teak clock\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:teak_eagle_lectern\",\n                    \"text\": \"Teak eagle lectern\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:teak_demon_lectern\",\n                    \"text\": \"Teak demon lectern\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:lunar_globe\",\n                    \"text\": \"Lunar globe\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:astronomical_chart\",\n                    \"text\": \"Astronomical chart\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:teak_telescope\",\n                    \"text\": \"Teak telescope\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:opulent_rug\",\n                    \"text\": \"Opulent rug\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:mahogany_eagle_lectern\",\n                    \"text\": \"Mahogany eagle lectern\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:mahogany_demon_lectern\",\n                    \"text\": \"Mahogany demon lectern\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:celestial_globe\",\n                    \"text\": \"Celestial globe\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:dungeon_candles\",\n                    \"text\": \"Dungeon candles\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:decorative_dungeon_bloodstain\",\n                    \"text\": \"Decorative dungeon bloodstain\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:armillary_sphere\",\n                    \"text\": \"Armillary sphere\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:marble_lectern\",\n                    \"text\": \"Marble lectern\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:infernal chart\",\n                    \"text\": \"Infernal chart\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:decorative_dungeon_pipe\",\n                    \"text\": \"Decorative dungeon pipe\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:dungeon_torches\",\n                    \"text\": \"Dungeon torches\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:mahoganys_scope\",\n                    \"text\": \"Mahogany's scope\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_clock\",\n                    \"text\": \"Gilded mahogany clock\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:small_orrery\",\n                    \"text\": \"Small orrery\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:hanging_dungeon_skeleton\",\n                    \"text\": \"Hanging dungeon skeleton\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:dungeon_skull_torches\",\n                    \"text\": \"Dungeon skull torches\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:large_orrery\",\n                    \"text\": \"Large orrery\",\n                    \"level\": 95\n                }\n            ]\n        },\n        {\n            \"name\": \"Trophies\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:oak_wall_decoration\",\n                    \"text\": \"Oak wall decoration\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:trophy_pedestal\",\n                    \"text\": \"Trophy pedestal\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rug_league_hall\",\n                    \"text\": \"Rug (league hall)\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:trailblazer_rug\",\n                    \"text\": \"Trailblazer rug\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:suit_of_armour\",\n                    \"text\": \"Suit of armour\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:banner_stand\",\n                    \"text\": \"Banner stand\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:league_statue\",\n                    \"text\": \"League statue\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:trailblazer_globe\",\n                    \"text\": \"Trailblazer globe\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:oak_outfit_stand\",\n                    \"text\": \"Oak outfit stand\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:small_potrait\",\n                    \"text\": \"Small potrait\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:oak_trophy_case\",\n                    \"text\": \"Oak trophy case\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:mounted_bass\",\n                    \"text\": \"mounted_bass\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:teak_wall_decoration\",\n                    \"text\": \"Teak wall decoration\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:minor_slayer_monster_head\",\n                    \"text\": \"Minor Slayer monster head\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:small_map\",\n                    \"text\": \"Small map\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:rune_display_cases\",\n                    \"text\": \"Rune display cases\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:mounted_sword\",\n                    \"text\": \"Mounted sword\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:small_landscape\",\n                    \"text\": \"Small landscape\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:mounted_anti_dragon_shield\",\n                    \"text\": \"Mounted Anti-Dragon Shield\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:mounted_amulet_of_glory\",\n                    \"text\": \"Mounted Amulet of Glory\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:mounted_cape_of_legends\",\n                    \"text\": \"Mounted Cape of Legends\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:mounted_mythical_cape\",\n                    \"text\": \"Mounted Mythical Cape\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:leagues_accomplishment_scroll\",\n                    \"text\": \"Leaugues accomomplishment scroll\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:large_potrait\",\n                    \"text\": \"Large potrait\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:gilded_mahogany_wall_decoration\",\n                    \"text\": \"Gilded mahogany wall decoration\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:mounted_swordfish\",\n                    \"text\": \"Mounted swordfish\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:medium_map\",\n                    \"text\": \"Medium map\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:medium_slayer_monster_head\",\n                    \"text\": \"Medium Slayer monster head\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:opulent_rug_league_hall\",\n                    \"text\": \"Opulent rug league hall\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:large_landscape\",\n                    \"text\": \"Large landscape\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:round_wall_mounted_shield\",\n                    \"text\": \"Round wall mounted shield\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:mahogany_outfit_stand\",\n                    \"text\": \"Mahogany outfit stand\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:mounted_shark\",\n                    \"text\": \"Mounted shark\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:square_wall_mounted_shield\",\n                    \"text\": \"Square wall-mounted shield\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:mahogany_trophy_case\",\n                    \"text\": \"Mahogany trophy case\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:major_slayer_monster_head\",\n                    \"text\": \"Major Slayer monster head\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:large_map\",\n                    \"text\": \"Large map\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:quest_list\",\n                    \"text\": \"Quest list\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:mounted_emblem\",\n                    \"text\": \"Mounted emblem\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:mounted_coins\",\n                    \"text\": \"Mounted coins\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:cape_hanger\",\n                    \"text\": \"Cape hanger\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:mounted_digsite_pendant\",\n                    \"text\": \"Mounted Digsite Pendant\",\n                    \"level\": 82\n                }\n            ]\n        },\n        {\n            \"name\": \"Games\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:hoop_and_stick_game\",\n                    \"text\": \"Hoop-and-stick game\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:boxing_ring\",\n                    \"text\": \"Boxing ring\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"boxing_glove_rack\",\n                    \"text\": \"Boxing glove rack\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:oak_prize_chest\",\n                    \"text\": \"Oak pricechest\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:lesser_magical_balance\",\n                    \"text\": \"Lesser magical balance\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:jester_game\",\n                    \"text\": \"Jester game\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:clay_attack_stone\",\n                    \"text\": \"Clay attack stone\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:fencing_ring\",\n                    \"text\": \"Fencing ring\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:weapons_rack\",\n                    \"text\": \"Weapons rack\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:teak_prize_chest\",\n                    \"text\": \"Teak_prize_chest\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:combat_dummy\",\n                    \"text\": \"Combat dummy\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:treasure_hunt_fairy_house\",\n                    \"text\": \"Treasure hunt fairy house\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:combat_ring\",\n                    \"text\": \"Combat ring\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:combat_dummy_undead_and_slayer\",\n                    \"text\": \"Combat dummy (Undead and slayer)\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:dartboard\",\n                    \"text\": \"Dartboard)\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:extra_weapons_rack\",\n                    \"text\": \"Extra weapons rack\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:mahogany_prize_chest\",\n                    \"text\": \"Mahogany prize chest\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:medium_balance\",\n                    \"text\": \"Medium balance\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:limestone_attack_stone\",\n                    \"text\": \"Limestone attack stone\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:hangman_game\",\n                    \"text\": \"Hangman game\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:simple_pet_arena\",\n                    \"text\": \"Simple pet arena\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:ranging pedestals\",\n                    \"text\": \"Ranging pedestals\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:greater_magical_Balance\",\n                    \"text\": \"Greater magical balance\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:marble_attack_stone\",\n                    \"text\": \"Marble attack stone\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:archery_target\",\n                    \"text\": \"Archergy target\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:balance_beam\",\n                    \"text\": \"Balance beam\",\n                    \"level\": 81\n                }\n            ]\n        },\n        {\n            \"name\": \"Garden\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:exit_portal\",\n                    \"text\": \"Exit portal\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:low_level_plants\",\n                    \"text\": \"Low-level plants\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:decorative_rock\",\n                    \"text\": \"Decorative_rock\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:con_tree\",\n                    \"text\": \"Tree\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:mid_level_plants\",\n                    \"text\": \"Mid-level plants\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:pond\",\n                    \"text\": \"Pond\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:nice_tree\",\n                    \"text\": \"Nice tree\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:high_level_plants\",\n                    \"text\": \"High-level plants\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:imp_statue\",\n                    \"text\": \"Imp statue\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:oak_tree\",\n                    \"text\": \"Oak tree\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:willow_tree\",\n                    \"text\": \"Willow tree\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:tip_jar\",\n                    \"text\": \"Tip jar\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:maple_tree\",\n                    \"text\": \"Maple tree\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:boundary_stones\",\n                    \"text\": \"Boundary stones\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:thorny_hedge\",\n                    \"text\": \"Thorny hedge\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:desert_pet_habitat\",\n                    \"text\": \"Desert_pet_habitat\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:wooden_fence\",\n                    \"text\": \"Wooden fence\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:nice_hedge\",\n                    \"text\": \"Nice hedge\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:yew_tree\",\n                    \"text\": \"Yew tree\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:stone_wall\",\n                    \"text\": \"Stone wall\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:small_box_hedge\",\n                    \"text\": \"Small box hedge\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:gazebo\",\n                    \"text\": \"Gazebo\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:zen_theme\",\n                    \"text\": \"Zen theme\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:topiary_bush\",\n                    \"text\": \"Topiary bush\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:sunflower\",\n                    \"text\": \"Sunflower\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:rosemary\",\n                    \"text\": \"Rosemary\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:teak_garden_bench\",\n                    \"text\": \"Teak garden bench\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:iron_railings\",\n                    \"text\": \"Iron railings\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:topiary_hedge\",\n                    \"text\": \"Topiary hedge\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:dungeon_entrance\",\n                    \"text\": \"Dungeon entrance\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:marigolds\",\n                    \"text\": \"marigolds\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:daffodils\",\n                    \"text\": \"Daffodils\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:picket_fence\",\n                    \"text\": \"Picket fence\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:small_fountain\",\n                    \"text\": \"Small fountain\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:fancy_hedge\",\n                    \"text\": \"Fancy hedge\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:magic_tree\",\n                    \"text\": \"Magic tree\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:otherwordly_theme\",\n                    \"text\": \"Otherworldly_theme\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:spirit_tree\",\n                    \"text\": \"Spirit tree\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:large_fountain\",\n                    \"text\": \"Large fountain\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:garden_fence\",\n                    \"text\": \"Garden_fence\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:tall_fancy_hedge\",\n                    \"text\": \"Tall fancy hedge\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:topiary_bush\",\n                    \"text\": \"Topiary bush\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:topiary_bush\",\n                    \"text\": \"Topiary bush\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:roses\",\n                    \"text\": \"Roses\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:bluebells\",\n                    \"text\": \"Bluebells\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:gnome_bench\",\n                    \"text\": \"Gnome bench\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:marble_wall\",\n                    \"text\": \"Marble wakk\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:obelisk\",\n                    \"text\": \"Obelisk\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:tall_box_hedge\",\n                    \"text\": \"Tall box hedge\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:posh_fountain\",\n                    \"text\": \"Posh fountain\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:fairy_ring\",\n                    \"text\": \"Fairy ring\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:marble_garden_bench\",\n                    \"text\": \"Marble garden bench (decoration only)\",\n                    \"level\": 88\n                }\n            ]\n        },\n        {\n            \"name\": \"Dungeon\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:throne_room_floor_decoration\",\n                    \"text\": \"2x Throne room floor decoration\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:oak_cage\",\n                    \"text\": \"Oak cage\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:oubliette_spikes\",\n                    \"text\": \"Oubliette spikes\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:steel_cage\",\n                    \"text\": \"Steel cage\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:oak_and_steel_cage\",\n                    \"text\": \"Oak and steel cage\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:skeleton_guard\",\n                    \"text\": \"Skeleton guard\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:tentacle_pool\",\n                    \"text\": \"Tentacle pool\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:spike_trap\",\n                    \"text\": \"Spike trap\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:large_trapdoor\",\n                    \"text\": \"Large trapdoor\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:oak_dungeon_door\",\n                    \"text\": \"Oak dungeon door\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:guard_dog\",\n                    \"text\": \"Guard dog\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:wooden_dungeon_treasure_crate\",\n                    \"text\": \"Wooden dungeon treasure crate\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:demon\",\n                    \"text\": \"Demon\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:steel_cage\",\n                    \"text\": \"Steel cage\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:man_trap\",\n                    \"text\": \"Man trap\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:oubliette_flame_pit\",\n                    \"text\": \"Oubliette flame pit\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:hobgoblin_guard\",\n                    \"text\": \"Hobgoblin guard\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:oak_dungeon_treasure_chest\",\n                    \"text\": \"Oak dungeon treasure chest\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:spiked_cage\",\n                    \"text\": \"Spiked cage\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:tangle_vine\",\n                    \"text\": \"Tangle vine\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:Kalphite soldier\",\n                    \"text\": \"Kalphite soldier\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:lesser_magic_cage\",\n                    \"text\": \"Lesser magic cage\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:baby_red_dragon\",\n                    \"text\": \"Baby red dragon\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:teak_dungeon_treasure_chest\",\n                    \"text\": \"Teak dungeon treasure chest\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:rocnar\",\n                    \"text\": \"Rocnar\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:steel_plated_oak_door\",\n                    \"text\": \"Steel plated oak door\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:marble_trap\",\n                    \"text\": \"Marble trap\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:tok-xil\",\n                    \"text\": \"Tok-Xil\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:bone_cage\",\n                    \"text\": \"Bone cage\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:huge_spider\",\n                    \"text\": \"Huge spider\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:mahogany_dungeon_treasure_chest\",\n                    \"text\": \"Mahogany dungeon treasure chest\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:teleport_trap\",\n                    \"text\": \"Teleport trap\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:greater_magic_cage\",\n                    \"text\": \"Greater magic cage\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:troll_guard\",\n                    \"text\": \"Troll guard\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:dagannoth\",\n                    \"text\": \"Dagannoth\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:magic_dungeon_treasure_chest\",\n                    \"text\": \"Magic dungeon treasure chest\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:marble_dungeon_door\",\n                    \"text\": \"Marble dungeon door\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:hellhound\",\n                    \"text\": \"Hellhound\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:steel_dragon\",\n                    \"text\": \"steel_dragon\",\n                    \"level\": 95\n                }\n            ]\n        },\n        {\n            \"name\": \"Chapel\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:oak_altar\",\n                    \"text\": \"Oak altar\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:steel_torches_chapel\",\n                    \"text\": \"Steel torches chapel\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:icon_of_gnome_child\",\n                    \"text\": \"Icon of Gnome Child\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:symbol_of_saradomin\",\n                    \"text\": \"Symbol of Saradomin\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:symbol_of_guthix\",\n                    \"text\": \"Symbol of Guthix\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:symbol_of_zamorak\",\n                    \"text\": \"Symbol of Zamorak\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:wooden_torches_chapel\",\n                    \"text\": \"Wooden torches (chapel)\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:chapel_windchimes\",\n                    \"text\": \"Chapel windchimes\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:small_chapel_statue\",\n                    \"text\": \"Small chapel statue\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:shuttered_chapel_window\",\n                    \"text\": \"Shuttered chapel window\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:teak_altar\",\n                    \"text\": \"Teak altar\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:steel_candlesticks\",\n                    \"text\": \"Steel candlesticks\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:cloth_covered_teak_altar\",\n                    \"text\": \"Cloth covered teak altar\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:gold_candlesticks\",\n                    \"text\": \"Gold candlesticks\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:chapel_bells\",\n                    \"text\": \"Chapel bells\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:icon_of_saradomin\",\n                    \"text\": \"Icon of Saradomin\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:Icon of Guthix\",\n                    \"text\": \"Icon of Guthix\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:icon_of_zamorak\",\n                    \"text\": \"Icon of Zamorak\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:cloth_covered_mahogany_altar\",\n                    \"text\": \"Cloth-covered mahogany altar\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:oak_incense_burners\",\n                    \"text\": \"Oak incense burners\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:limestone_altar\",\n                    \"text\": \"Limestone altar\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:mahogany_incense_burners\",\n                    \"text\": \"Mahogany incense burners\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:medium_chapel_statue\",\n                    \"text\": \"Medium_chapel_statue\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:chapel_organ\",\n                    \"text\": \"Chapel organ\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:decorative_chapel_window\",\n                    \"text\": \"Decorative chapel window\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:marble_incense_burner\",\n                    \"text\": \"Marble incense burner\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:marble_altar\",\n                    \"text\": \"Marble altar\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:icon_of_bob_the_cat\",\n                    \"text\": \"Icon of bob the Cat\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:gilded_marble_altar\",\n                    \"text\": \"Gilded marble altar\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:large_chapel_statue\",\n                    \"text\": \"Large chapel statue\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:stained_glass_chapel_window\",\n                    \"text\": \"Stained-glass chapel window\",\n                    \"level\": 89\n                }\n            ]\n        },\n        {\n            \"name\": \"Other\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:cup_of_tea\",\n                    \"text\": \"Mahogany Homes (beginner)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:cat_blanket\",\n                    \"text\": \"Cat blanket\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:stash_units\",\n                    \"text\": \"STASH units (beginner)\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:cat_basket\",\n                    \"text\": \"Cat basket\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:mahogany_homes\",\n                    \"text\": \"Mahogany Homes(novice)\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:rope_bell_pull\",\n                    \"text\": \"Rope bell pull\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:oak_staircase\",\n                    \"text\": \"Oak staircase\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:stash_units_easy\",\n                    \"text\": \"STASH units (easy)\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:cushioned_cat_basket\",\n                    \"text\": \"Cushioned cat basket\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:teak_bell_pull\",\n                    \"text\": \"Teak bell pull\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:crystal_ball\",\n                    \"text\": \"Crystal ball\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:stash_units_medium\",\n                    \"text\": \"STASH units (medium)\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:teak_staircase\",\n                    \"text\": \"Teak staircase\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:teak_portal_frame\",\n                    \"text\": \"Teak portal frame\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:cup_of_tea\",\n                    \"text\": \"Mahogany Homes (adept)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:elemental_sphere\",\n                    \"text\": \"Elemental sphere\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:stash_units_hard\",\n                    \"text\": \"STASH units (hard)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:hallowed_sepulchre\",\n                    \"text\": \"Hallowed Sepulchre - Repair bridge\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:gilded_teak_bell_pull\",\n                    \"text\": \"Gilded teak bell pull\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:greater_teleport_focus\",\n                    \"text\": \"Greater teleport focus\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:mahogany_portal_frame\",\n                    \"text\": \"Mahogany portal frame\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:crystal_of_power\",\n                    \"text\": \"Crystal of power\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:limestone_spiral_staircase\",\n                    \"text\": \"Limestone spiral staircase\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:oak_oubliette_ladrer\",\n                    \"text\": \"Oak oubliette ladder\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:oak_lever\",\n                    \"text\": \"Oak lever\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:stash_units_elite\",\n                    \"text\": \"STASH units (Elite)\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:teak_oubliette_ladder\",\n                    \"text\": \"Teak oubliette ladder\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:teak_lever\",\n                    \"text\": \"Teak lever\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:marble_portal_frame\",\n                    \"text\": \"Marble portal frame\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:scrying_pool\",\n                    \"text\": \"Scrying pool\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:marble_staircase\",\n                    \"text\": \"Marble staircase\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:mahogany_oubliette_ladder\",\n                    \"text\": \"Mahogany oubliette ladder\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:mahogany_lever\",\n                    \"text\": \"Mahogany lever\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:stash_units_master\",\n                    \"text\": \"STASH units (master)\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:marble_spiral\",\n                    \"text\": \"Marble spiral\",\n                    \"level\": 97\n                }\n            ]\n        },\n        {\n            \"name\": \"Servants\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:servant_rick\",\n                    \"text\": \"Rick\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:servant_maid\",\n                    \"text\": \"Maid\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:servant_cook\",\n                    \"text\": \"Cook\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:servant_butler\",\n                    \"text\": \"Butler\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:servant_demon_butler\",\n                    \"text\": \"Demon Butler\",\n                    \"level\": 50\n                }\n            ]\n        },\n        {\n            \"name\": \"House Size\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 24\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:saw\",\n                    \"text\": \"Maximum dimensions: 3x3\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:saw\",\n                    \"text\": \"Maximum dimensions: 4x4\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:saw\",\n                    \"text\": \"Maximum dimensions: 5x5\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:saw\",\n                    \"text\": \"Maximum dimensions: 6x6\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 25\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 26\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:saw\",\n                    \"text\": \"Maximum dimensions: 7x7\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 27\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 28\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 29\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 30\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 31\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 32\",\n                    \"level\": 92\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 33\",\n                    \"level\": 96\n                },\n                {\n                    \"item\": \"rs:hammer\",\n                    \"text\": \"Maximum rooms: 34\",\n                    \"level\": 99\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/cooking.json",
    "content": "{\n    \"id\": 129,\n    \"name\": \"Cooking\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Meats\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:meat\",\n                    \"text\": \"Meat\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:shrimps\",\n                    \"text\": \"Shrimp\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:chicken\",\n                    \"text\": \"Chicken\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:anchovies\",\n                    \"text\": \"Anchovies\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:sardine\",\n                    \"text\": \"Sardine\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:karambwan\",\n                    \"text\": \"Members: Karambwan\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:ugthanki_kebab\",\n                    \"text\": \"Ugthanki kebab\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:herring\",\n                    \"text\": \"Herring\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:mackerel\",\n                    \"text\": \"Mackerel\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:thin_snail_meat\",\n                    \"text\": \"Members: Thin snail\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:trout\",\n                    \"text\": \"Trout\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:lean_snail_meat\",\n                    \"text\": \"Members: Lean snail\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:cod\",\n                    \"text\": \"Members: Cod\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:pike\",\n                    \"text\": \"Pike\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:salmon\",\n                    \"text\": \"Salmon\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:tuna\",\n                    \"text\": \"Tuna\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:cooked_chompy\",\n                    \"text\": \"Members: Cooked chompy\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:fish_cake\",\n                    \"text\": \"Members: Fishcakes\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:cave_eel\",\n                    \"text\": \"Members: Cave eel\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:lobster\",\n                    \"text\": \"Lobster\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:jubbly\",\n                    \"text\": \"Members: Jubbly\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:bass\",\n                    \"text\": \"Members: Bass\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:swordfish\",\n                    \"text\": \"Swordfish\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:lava_eel\",\n                    \"text\": \"Members: Lava eel\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:shark\",\n                    \"text\": \"Members: Shark\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:sea_turtle\",\n                    \"text\": \"Members: Sea turtle\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:manta_ray\",\n                    \"text\": \"Members: Manta ray\",\n                    \"level\": 90\n                }\n            ]\n        },\n        {\n            \"name\": \"Bread\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bread\",\n                    \"text\": \"Bread\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:pitta_bread\",\n                    \"text\": \"Members: Pitta bread\",\n                    \"level\": 58\n                }\n            ]\n        },\n        {\n            \"name\": \"Pies\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:redberry_pie\",\n                    \"text\": \"Redberry pie\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:meat_pie\",\n                    \"text\": \"Meat pie\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:mud_pie\",\n                    \"text\": \"Members: Mud pie\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:apple_pie\",\n                    \"text\": \"Apple pie\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:garden_pie\",\n                    \"text\": \"Members: Garden pie\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:fish_pie\",\n                    \"text\": \"Members: Fish pie\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:admiral_pie\",\n                    \"text\": \"Members: Admiral pie\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:wild_pie\",\n                    \"text\": \"Members: Wild pie\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:summer_pie\",\n                    \"text\": \"Members: Summer pie\",\n                    \"level\": 95\n                }\n            ]\n        },\n        {\n            \"name\": \"Stews\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:stew\",\n                    \"text\": \"Stew\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:banana_stew\",\n                    \"text\": \"Banana stew\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:spicy_stew\",\n                    \"text\": \"Spicy stew\",\n                    \"level\": 25\n                }\n            ]\n        },\n        {\n            \"name\": \"Pizzas\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:plain_pizza\",\n                    \"text\": \"Plain pizza\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:meat_pizza\",\n                    \"text\": \"Meat pizza\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:anchovy_pizza\",\n                    \"text\": \"Anchovy pizza\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:pineapple_pizza\",\n                    \"text\": \"Members: Pineapple pizza\",\n                    \"level\": 65\n                }\n            ]\n        },\n        {\n            \"name\": \"Cakes\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:cake\",\n                    \"text\": \"Cake\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:chocolate_cake\",\n                    \"text\": \"Chocolate cake\",\n                    \"level\": 50\n                }\n            ]\n        },\n        {\n            \"name\": \"Wine\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:wine\",\n                    \"text\": \"Wine\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:wine_of_zamorak\",\n                    \"text\": \"Members: Wine of Zamorak\",\n                    \"level\": 65\n                }\n            ]\n        },\n        {\n            \"name\": \"Hot drinks\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:nettle_tea\",\n                    \"text\": \"Nettle tea\",\n                    \"level\": 20\n                }\n            ]\n        },\n        {\n            \"name\": \"Brewing\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:cider\",\n                    \"text\": \"Cider (4 Apple mush)\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:dwarven_stout\",\n                    \"text\": \"Dwarven Stout (4 Hammerstone hops)\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:asgarnian_ale\",\n                    \"text\": \"Asgarnian Ale (4 Asgarnian hops)\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:greenmans_ale\",\n                    \"text\": \"Greenman's Ale (4 Harralander leaves)\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:wizards_mind_bomb\",\n                    \"text\": \"Wizard's Mind Bomb (4 Yinillian hops)\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:dragon_bitter\",\n                    \"text\": \"Dragon Bitter (4 Krandorian hops)\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:moonlight_mead\",\n                    \"text\": \"Moonlight Mead (4 Bittercap mushrooms)\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:axemans_folly\",\n                    \"text\": \"Axeman's Folly (1 Oak root)\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:chefs_delight\",\n                    \"text\": \"Chef's Delight (4 Portions of chocolate dust)\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:slayers_respite\",\n                    \"text\": \"Slayer's Respite (4 Wildblood hops)\",\n                    \"level\": 54\n                }\n            ]\n        },\n        {\n            \"name\": \"Vegetable\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:baked_potato\",\n                    \"text\": \"Baked potato\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:spicy_sauce\",\n                    \"text\": \"Spicy sauce (topping ingredient)\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:scrambled_egg\",\n                    \"text\": \"Scrambled egg (topping ingredient)\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:scrambled_egg_and_tomato\",\n                    \"text\": \"Scrambled egg and tomato (topping)\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:sweetcorn\",\n                    \"text\": \"Sweetcorn\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:baked_potato_with_butter\",\n                    \"text\": \"Baked potato with butter\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:fried_onion\",\n                    \"text\": \"Fried onion (topping ingredient)\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:fried_mushroom\",\n                    \"text\": \"Fried mushroom (topping ingredient)\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:baked_potato_with_butter_and_cheese\",\n                    \"text\": \"Baked potato with butter and cheese\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:baked_potato_with_egg_and_tomato\",\n                    \"text\": \"Baked potato with egg and tomato\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:fried_mushroom_and_onion\",\n                    \"text\": \"Fried mushroom and onion (topping)\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:baked_potato_with_mushroom_and_onion\",\n                    \"text\": \"Baked potato with mushroom and onion\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:tuna_and_sweetcorn\",\n                    \"text\": \"Tuna and sweetcorn (topping)\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:baked_potato_with_tuna_and_sweetcorn\",\n                    \"text\": \"Baked potato with tuna and sweetcorn\",\n                    \"level\": 68\n                }\n            ]\n        },\n        {\n            \"name\": \"Diary\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:butter\",\n                    \"text\": \"Butter\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:cheese\",\n                    \"text\": \"Cheese\",\n                    \"level\": 48\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/crafting.json",
    "content": "{\n    \"id\": 131,\n    \"name\": \"Crafting\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Weaving\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:vegetable_sack\",\n                    \"text\": \"Vegetable sack\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:drift_net\",\n                    \"text\": \"Drift net\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:fruit_basket\",\n                    \"text\": \"Fruit basket\",\n                    \"level\": 36\n                }\n            ]\n        },\n        {\n            \"name\": \"Armour\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:leather_gloves\",\n                    \"text\": \"Leather gloves\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:leather_boots\",\n                    \"text\": \"Leather boots\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:leather_cowl\",\n                    \"text\": \"Leather cowl\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:leather_vambraces\",\n                    \"text\": \"Leather vambraces\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:leather_body\",\n                    \"text\": \"Leather body\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:snelmet\",\n                    \"text\": \"Members: Snail helmet\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:crab_shell_armour\",\n                    \"text\": \"Crab shell armour\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:leather_chaps\",\n                    \"text\": \"Leather chaps\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:hard_leather_body\",\n                    \"text\": \"Hard leather body\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:broodoo_shield\",\n                    \"text\": \"Members: Broodoo shield\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:coif\",\n                    \"text\": \"Members: Coif\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:studded_body\",\n                    \"text\": \"Members: Studded body\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:studded_chaps\",\n                    \"text\": \"Members: Studded chaps\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:snakeskin_boots\",\n                    \"text\": \"Members: Snakeskin boots\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:snakeskin_vambraces\",\n                    \"text\": \"Members: Snakeskin vambraces\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:snakeskin_bandana\",\n                    \"text\": \"Members: Snakeskin Bandana\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:snakeskin_chaps\",\n                    \"text\": \"Members: Snakeskin chaps\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:snakeskin_body\",\n                    \"text\": \"Members: Snakeskin body\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_vambraces\",\n                    \"text\": \"Members: Green Dragonhide vambraces\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_chaps\",\n                    \"text\": \"Members: Green dragonhide chaps\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:splitbark_gauntlets\",\n                    \"text\": \"Members: Splitbark gauntlets\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:splitbark_boots\",\n                    \"text\": \"Members: Splitbark boots\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:splitbark_helm\",\n                    \"text\": \"Members: Splitbark helm\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:splitbark_body\",\n                    \"text\": \"Members: Splitbark body\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:splitbark_legs\",\n                    \"text\": \"Members: Splitbark legs\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_chaps\",\n                    \"text\": \"Members: Green dragonhide chaps\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_chaps\",\n                    \"text\": \"Members: Blue dragonhide chaps\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_chaps\",\n                    \"text\": \"Members: Blue dragonhide chaps\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_body\",\n                    \"text\": \"Members: Blue dragonhide body\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_vambraces\",\n                    \"text\": \"Members: Red dragonhide vambraces\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_chaps\",\n                    \"text\": \"Members: Red dragonhide chaps\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_body\",\n                    \"text\": \"Members: Red dragonhide body\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_vambraces\",\n                    \"text\": \"Members: Snakeskin chaps\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_chaps\",\n                    \"text\": \"Members: Snakeskin chaps\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_body\",\n                    \"text\": \"Members: Black dragonhide body\",\n                    \"level\": 84\n                }\n            ]\n        },\n        {\n            \"name\": \"Spinning\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:wool\",\n                    \"text\": \"Wool\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:bow_strings\",\n                    \"text\": \"Members: Flax into bow strings\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:sinew_strings\",\n                    \"text\": \"Members: Sinew into crossbow strings\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:magic_strings\",\n                    \"text\": \"Members: Magic tree roots into magic strings\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:yak_hair\",\n                    \"text\": \"Members: Yak hair into rope\",\n                    \"level\": 30\n                }\n            ]\n        },\n        {\n            \"name\": \"Pottery\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:pot\",\n                    \"text\": \"Pot\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:pie_dish\",\n                    \"text\": \"Pie dish\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:bowl\",\n                    \"text\": \"Bowl\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:plant_pot\",\n                    \"text\": \"Members: Plant pot\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:pot_lid\",\n                    \"text\": \"Members: Pot lid\",\n                    \"level\": 25\n                }\n            ]\n        },\n        {\n            \"name\": \"Glass\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:beer_glass\",\n                    \"text\": \"Beer Glass\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:candle_lantern\",\n                    \"text\": \"Candle lantern\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:oil_lamp\",\n                    \"text\": \"Oil lamp\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:oil_lantern\",\n                    \"text\": \"Oil lantern\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:vial\",\n                    \"text\": \"Vial\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:fishbowl\",\n                    \"text\": \"Fishbowl\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:glass_orb\",\n                    \"text\": \"Glass orb\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:bullseye_lantern_lens\",\n                    \"text\": \"Bullsye lantern lens\",\n                    \"level\": 49\n                }\n            ]\n        },\n        {\n            \"name\": \"Jewellery\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:cut_opal\",\n                    \"text\": \"Cut opal\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:opal_ring\",\n                    \"text\": \"Opal ring\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:polished_buttons\",\n                    \"text\": \"Polished buttons\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:gold_ring\",\n                    \"text\": \"Gold ring\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:gold_necklace\",\n                    \"text\": \"Gold necklace\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:gold_amulet\",\n                    \"text\": \"Gold amulet\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:cut_jade\",\n                    \"text\": \"Cut jade\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:jade_ring\",\n                    \"text\": \"Jade ring\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:holy_symbol\",\n                    \"text\": \"Holy symbol\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:cut_red_topaz\",\n                    \"text\": \"Members: Cut red topaz\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:cut_sapphire\",\n                    \"text\": \"Cut sapphire\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:sapphire_ring\",\n                    \"text\": \"Sapphire ring\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:sapphire_necklace\",\n                    \"text\": \"Sapphire necklace\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:tiara\",\n                    \"text\": \"Tiara\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:sapphire_amulet\",\n                    \"text\": \"Sapphire amulet\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:cut_emerald\",\n                    \"text\": \"Cut emerald\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:emerald_ring\",\n                    \"text\": \"Emerald ring\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:emerald_necklace\",\n                    \"text\": \"Emerald necklace\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:emerald_amulet\",\n                    \"text\": \"Emerald amulet\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:cut_ruby\",\n                    \"text\": \"Cut ruby\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:ruby_ring\",\n                    \"text\": \"Ruby ring\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:ruby_necklace\",\n                    \"text\": \"Ruby necklace\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:cut_diamond\",\n                    \"text\": \"Cut diamond\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:diamond_ring\",\n                    \"text\": \"Diamond ring\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:ruby_amulet\",\n                    \"text\": \"Ruby amulet\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:cut_dragonstone\",\n                    \"text\": \"Members: Cut Dragonstone\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:dragonstone_ring\",\n                    \"text\": \"Members: Dragonstone ring\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:diamond_necklace\",\n                    \"text\": \"Diamond necklace\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:cut_onyx\",\n                    \"text\": \"Members: Cut onyx\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:onyx_ring\",\n                    \"text\": \"Onyx Ring\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:diamond_amulet\",\n                    \"text\": \"Diamond amulet\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:dragonstone_amulet\",\n                    \"text\": \"Members: Dragonstone amulet\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:onyx_necklace\",\n                    \"text\": \"Members Onyx necklace\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:onyx_amulet\",\n                    \"text\": \"Members: Onyx amulet\",\n                    \"level\": 90\n                }\n            ]\n        },\n        {\n            \"name\": \"Weaponry\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:silver_sickle\",\n                    \"text\": \"Silver sickle\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:silver_crossbow_bolts\",\n                    \"text\": \"Silver crossbow bolts\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:water_battlestaff\",\n                    \"text\": \"Water battlestaf\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:earth_battlestaff\",\n                    \"text\": \"Earth battlestaf\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:fire_battlestaff\",\n                    \"text\": \"Fire battlestaf\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:air_battlestaff\",\n                    \"text\": \"Air battlestaf\",\n                    \"level\": 66\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/defence.json",
    "content": "{\n    \"id\": 124,\n    \"name\": \"Defence\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Armour\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_medium_helm\",\n                    \"text\": \"Bronze armour\\\\nwield any bronze equipment\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:iron_medium_helm\",\n                    \"text\": \"Iron armour\\\\nwield any iron equipment\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:steel_medium_helm\",\n                    \"text\": \"Steel armour\\\\nwield any steel equipment\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:black_medium_helm\",\n                    \"text\": \"Black armour\\\\nwield any black equipment\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:white_medium_helm\",\n                    \"text\": \"Members: white armour\\\\nwield any white equipment\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:initiate_medium_helm\",\n                    \"text\": \"Members: Initiate armour\\\\n(after Recruitment Drive, with 10 Prayer)\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:mithril_medium_helm\",\n                    \"text\": \"Mithril armour\\\\nwield any mithril equipment\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:adamant_medium_helm\",\n                    \"text\": \"Adamant armour\\\\nwield any adamant equipment\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:proselyte_medium_helm\",\n                    \"text\": \"Members: Proselyte armour\\\\n(after Slug Menance, with 20 Prayer)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:rune_medium_helm\",\n                    \"text\": \"Rune armour\\\\nwield any rune equipment\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:rock_shell_helm\",\n                    \"text\": \"Members: Rock-shell armour\\\\n(after completing Fremennik Trials)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:berserker_helm\",\n                    \"text\": \"Members: Fremennik helmets\\\\n(after completing Fremennik Trials)\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:granite_shield\",\n                    \"text\": \"Granite armour\\\\nwield any granite equipment\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:helm_of_neitiznot\",\n                    \"text\": \"Members: Helm of neitiznot\\\\n(after Fremennik Isles)\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:Members: Dragon armour\",\n                    \"text\": \"Dragon armour\\\\nwield any dragon equipment\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:3rd_age_full_helmet\",\n                    \"text\": \"3rd Age armour\\\\nwield any 3rd Age equipment\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:torags_helmet\",\n                    \"text\": \"Barrows armour\\\\nwield any Barrows equipment\",\n                    \"level\": 70\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/farming.json",
    "content": "{\n    \"id\": 142,\n    \"name\": \"Farming\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Allotments\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:potatoes\",\n                    \"text\": \"Potatoes\\\\nPayment: Compost x2\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:onions\",\n                    \"text\": \"Onions\\\\nPayment: Potatoes(10)\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:cabbages\",\n                    \"text\": \"Cabbages\\\\nPayment: Onions(10)\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:tomatoes\",\n                    \"text\": \"Tomatoes\\\\nPayment: Cabbages(10)\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:sweetcorn\",\n                    \"text\": \"Sweetcorn\\\\nPayment: Jute fibre x10\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:strawberries\",\n                    \"text\": \"Strawberries\\\\nPayment: Apples(5)\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:watermelons\",\n                    \"text\": \"Watermelons\\\\nPayment: Curry leaf x10\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:snape_grass\",\n                    \"text\": \"Snape grass\\\\nPayment: Jangerberries x5\",\n                    \"level\": 61\n                }\n            ]\n        },\n        {\n            \"name\": \"Hops\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:barley\",\n                    \"text\": \"Barley\\\\nPayment: Compost x3\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:hammerstone_hops\",\n                    \"text\": \"Hammerstone hops\\\\nPayment: Marigolds\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:asgarnian_hops\",\n                    \"text\": \"Asgarnian hops\\\\nPayment: Onions(10)\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:jute_plants\",\n                    \"text\": \"Jute plants\\\\nPayment: Barley malt x6\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:yanillian_hops\",\n                    \"text\": \"Yanillian hops\\\\nPayment: Tomatoes(5)\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:krandorian_hops\",\n                    \"text\": \"Krandorian hops\\\\nPayment: Cabbages(10) x3\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:wildblood_hops\",\n                    \"text\": \"Wildblood hops\\\\nPayment: Nasturtiums\",\n                    \"level\": 28\n                }\n            ]\n        },\n        {\n            \"name\": \"Trees\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:oak_trees\",\n                    \"text\": \"Oak trees\\\\nPayment: Tomatoes(5)\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:willow_trees\",\n                    \"text\": \"Willow trees\\\\nPayment: Apples(5)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:maple_trees\",\n                    \"text\": \"Mapple trees\\\\nPayment: Oranges(5)\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:yew_trees\",\n                    \"text\": \"Yew trees\\\\nPayment: Cactus spine x10\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:magic_trees\",\n                    \"text\": \"Magic trees\\\\nPayment: Coconut x25\",\n                    \"level\": 75\n                }\n            ]\n        },\n        {\n            \"name\": \"Fruit Trees\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:apple_trees\",\n                    \"text\": \"Apple trees\\\\nPayment: Sweetcorn x9\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:banana_trees\",\n                    \"text\": \"Banana trees\\\\nPayment: Apples(5) x4\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:orange_trees\",\n                    \"text\": \"Orange trees\\\\nPayment: Strawberries(5) x3\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:curry_trees\",\n                    \"text\": \"Curry trees\\\\nPayment: Bananas(5) x3\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:pineapple_trees\",\n                    \"text\": \"Pineapple trees\\\\nPayment: Watermelon x10\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:papaya_trees\",\n                    \"text\": \"Papaya trees\\\\nPayment: Pineapple x10\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:palm_trees\",\n                    \"text\": \"Palm trees\\\\nPayment: Papaya fruit x15\",\n                    \"level\": 68\n                }\n            ]\n        },\n        {\n            \"name\": \"Bushes\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:redberry_bushes\",\n                    \"text\": \"Redberry bushes\\\\nPayment: Cabbages(10) x4\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:cadavaberry_bushes\",\n                    \"text\": \"Cadavanerry bushes\\\\nPayment: Tomatoes(5) x3\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:dwellberry_bushes\",\n                    \"text\": \"Dwellberry bushes\\\\nPayment: Strawberries(5) x3\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:jangerberry_bushes\",\n                    \"text\": \"Jangerberry bushes\\\\nPayment: Watermelon x6\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:white_berry_bushes\",\n                    \"text\": \"White berry bushes\\\\nPayment: Mushroom x8\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:poison_ivy_bushes\",\n                    \"text\": \"Poison ivy bushes\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Flowers\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:marigolds\",\n                    \"text\": \"Marigolds\\\\nProtects onions, tomatoes and potatoes\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:rosemary\",\n                    \"text\": \"Rosemary\\\\nProtects cabbages from disease\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"scarecrow\",\n                    \"text\": \"Make and place a scarecrow\\\\nProtects sweetcorn from birds\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:nasturtiums\",\n                    \"text\": \"Nasturtiums\\\\nProtects watermelons from disease\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:woad\",\n                    \"text\": \"Woad\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:limpwurt_root\",\n                    \"text\": \"Limpwurt plants\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:white_lily\",\n                    \"text\": \"White lily\\\\nProtects all allotment crops\",\n                    \"level\": 58\n                }\n            ]\n        },\n        {\n            \"name\": \"Herbs\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:guam_leaf\",\n                    \"text\": \"Guam leaf\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:marrentill\",\n                    \"text\": \"Marrentill\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"tarromin\",\n                    \"text\": \"Tarromin\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:harralander\",\n                    \"text\": \"Harralander\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:goutweed\",\n                    \"text\": \"Goutweed\\\\nMust have completed Eadgar's Ruse\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:ranarr_weed\",\n                    \"text\": \"Ranarr weed\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:toadflax\",\n                    \"text\": \"Toadflax\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:avantoe\",\n                    \"text\": \"Avantoe\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:kwuarm\",\n                    \"text\": \"Kwuarm\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:snapdragon\",\n                    \"text\": \"Snapdragon\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:cadantine\",\n                    \"text\": \"Cadantine\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:lantadyme\",\n                    \"text\": \"Lantadyme\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:dwarf_weed\",\n                    \"text\": \"Dwarf weed\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:torstol\",\n                    \"text\": \"Torstol\",\n                    \"level\": 85\n                }\n            ]\n        },\n        {\n            \"name\": \"special\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:teak_trees\",\n                    \"text\": \"Teak trees\\\\nPayment: Limpwurt root x15\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:grapes\",\n                    \"text\": \"Grapes\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:bittercap_mushrooms\",\n                    \"text\": \"Bittercap mushrooms\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:mahogany_trees\",\n                    \"text\": \"Mahogany trees\\\\nPayment: yanillian hops x25\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:cacti\",\n                    \"text\": \"Cacti\\\\nPayment: Cadava berries x6\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:belladonna\",\n                    \"text\": \"Belladonna\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:potato_cacti\",\n                    \"text\": \"Potato cacti\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:hespori\",\n                    \"text\": \"Hespori\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:calquat_trees\",\n                    \"text\": \"Calquat trees\\\\nPayment: Poison ivy Berries x8\",\n                    \"level\": 72\n                }\n            ]\n        },\n        {\n            \"name\": \"Multiple roots\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:oak_roots\",\n                    \"text\": \"2x oak roots\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:oak_roots\",\n                    \"text\": \"3x oak roots\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:willow_roots\",\n                    \"text\": \"2x willow roots\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:oak_roots\",\n                    \"text\": \"4x oak roots\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:willow_roots\",\n                    \"text\": \"3x willow roots\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:mapple_roots\",\n                    \"text\": \"2x mapple roots\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:willow_roots\",\n                    \"text\": \"4x willow roots\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:maple_roots\",\n                    \"text\": \"3x mapple roots\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:yew_roots\",\n                    \"text\": \"2x yew roots\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:maple_roots\",\n                    \"text\": \"4x mapple roots\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:yew_roots\",\n                    \"text\": \"3x yew roots\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:magic_roots\",\n                    \"text\": \"2x magic roots\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:yew_roots\",\n                    \"text\": \"4x yew roots\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:magic_roots\",\n                    \"text\": \"3x magic roots\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:magic_roots\",\n                    \"text\": \"4x magic roots\",\n                    \"level\": 99\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/firemaking.json",
    "content": "{\n    \"id\": 132,\n    \"name\": \"Firemaking\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Burning\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:logs\",\n                    \"text\": \"Normal logs\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:achey_logs\",\n                    \"text\": \"Members: Achey logs\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:pyre_logs\",\n                    \"text\": \"Members: Pyre logs\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:oak_logs\",\n                    \"text\": \"Oak logs\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:oak_pyre_logs\",\n                    \"text\": \"Members: Oak pyre logs\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:willow_logs\",\n                    \"text\": \"Willow logs\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:teak_logs\",\n                    \"text\": \"Members: Teak logs\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:willow_pyre_logs\",\n                    \"text\": \"Members: Willow pyre logs\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:teak_pyre_logs\",\n                    \"text\": \"Members: Teak pyre logs\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:maple_logs\",\n                    \"text\": \"Maple logs\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:mahogany_logs\",\n                    \"text\": \"Members: Mahogany logs\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:maple_pyre_logs\",\n                    \"text\": \"Members: Maple pyre logs\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:mahogany_pyre_logs\",\n                    \"text\": \"Members: Mahogany pyre logs\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:yew_logs\",\n                    \"text\": \"Yew logs\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:yew_pyre_logs\",\n                    \"text\": \"Members: Yew pyre logs\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:magic_logs\",\n                    \"text\": \"Members: Magic logs\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:magic_pyre_logs\",\n                    \"text\": \"Members: Magic pyre logs\",\n                    \"level\": 80\n                }\n            ]\n        },\n        {\n            \"name\": \"Barbarian\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:normal_pyre_ships\",\n                    \"text\": \"Normal Pyre Ships (with 11 Crafting)\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:normal_logs\",\n                    \"text\": \"Normal Logs\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:achey_logs\",\n                    \"text\": \"Achey Logs\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:oak_pyre_ships\",\n                    \"text\": \"Oak Pyre Ships (with 25 Crafting)\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:oak_logs\",\n                    \"text\": \"Oak Logs\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:willow_pyre_ships\",\n                    \"text\": \"Willow Pyre Ships (with 40 Crafting)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:teak_pyre_ships\",\n                    \"text\": \"Teak Pyre Ships (with 45 Crafting)\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:willow_logs\",\n                    \"text\": \"Willow Logs\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:teak_logs\",\n                    \"text\": \"Teak Logs\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:maple_pyre_ships\",\n                    \"text\": \"Maple Pyre Ships (with 55 Crafting)\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mahogany_pyre_ships\",\n                    \"text\": \"Mahogany Pyre Ships (with 60 Crafting)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:maple_logs\",\n                    \"text\": \"Maple Logs\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:yew_pyre_ships\",\n                    \"text\": \"Yew Pyre Ships (with 70 Crafting)\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:mahogany_logs\",\n                    \"text\": \"Mahogany Logs\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:yew_logs\",\n                    \"text\": \"Yew Logs\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:magic_pyre_ships\",\n                    \"text\": \"Magic Pyre Ships\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:magic_logs\",\n                    \"text\": \"Magic Logs\",\n                    \"level\": 95\n                }\n            ]\n        },\n        {\n            \"name\": \"Equipment\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:candle\",\n                    \"text\": \"Candle\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:candle_lanterns\",\n                    \"text\": \"Candle lanterns\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:oil_lamps\",\n                    \"text\": \"Oil lamps\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:iron_spits\",\n                    \"text\": \"Members: Iron spits\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:oil_lanterns\",\n                    \"text\": \"Oil lanterns\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:harpie_bug_lanterns\",\n                    \"text\": \"Harpie bug lanterns (not a light source)\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:bullseye_lanterns\",\n                    \"text\": \"Bullseye lanterns\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:sapphire_lanterns\",\n                    \"text\": \"Sapphire lanterns\",\n                    \"level\": 49\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/fishing.json",
    "content": "{\n    \"id\": 126,\n    \"name\": \"Fishing\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Catching\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:raw_shrimp\",\n                    \"text\": \"Raw Shrimp\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:raw_sardine\",\n                    \"text\": \"Raw Sardine\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:raw_karambwanji\",\n                    \"text\": \"Members: Raw Karambwanji\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:raw_herring\",\n                    \"text\": \"Raw Herring\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:raw_anchovies\",\n                    \"text\": \"Raw Anchovies\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:raw_mackerel\",\n                    \"text\": \"Members: Raw Mackerel\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:raw_trout\",\n                    \"text\": \"Raw Trout\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:raw_cod\",\n                    \"text\": \"Members: Raw Cod\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:raw_pike\",\n                    \"text\": \"Raw Pike\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:raw_slimy_eel\",\n                    \"text\": \"Members: Raw Slimy Eel\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:raw_salmon\",\n                    \"text\": \"Raw Salmon\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:raw_tuna\",\n                    \"text\": \"Raw Tuna\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:raw_cave_eel\",\n                    \"text\": \"Members: Raw Cave Eel\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:raw_lobster\",\n                    \"text\": \"Raw Lobster\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:raw_bass\",\n                    \"text\": \"Members: Raw Bass\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:raw_swordfish\",\n                    \"text\": \"Raw Swordfish\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:raw_lava_eel\",\n                    \"text\": \"Members: Raw Lava Eel\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:raw_monkfish\",\n                    \"text\": \"Members: Raw Monkfish\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:raw_karambwan\",\n                    \"text\": \"Members: Raw Karambwan\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:raw_shark\",\n                    \"text\": \"Raw Shark\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:raw_sea_turtle\",\n                    \"text\": \"Raw Sea Turtle\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:raw_manta_ray\",\n                    \"text\": \"Raw Manta Ray\",\n                    \"level\": 81\n                }\n            ]\n        },\n        {\n            \"name\": \"Equipment\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:small_fishing_net\",\n                    \"text\": \"Small Fishing Net\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:fishing_rod\",\n                    \"text\": \"Fishing Rod\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:big_fishing_net\",\n                    \"text\": \"Big Fishing Net\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:fly_fishing_rod\",\n                    \"text\": \"Members: Fly Fishing Rod\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:harpoon\",\n                    \"text\": \"harpoon\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:lobster_pot\",\n                    \"text\": \"Lobster_Pot\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:oily_fishing_rod\",\n                    \"text\": \"Oily Fishing Rod\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:karambwan_vessel\",\n                    \"text\": \"Karambwan Vessel\",\n                    \"level\": 65\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/fletching.json",
    "content": "{\n    \"id\": 138,\n    \"name\": \"Fletching\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Arrows\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"15 Arrow shafts (logs)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:bronze_arrow\",\n                    \"text\": \"Bronze arrows\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"30 Arrow shafts (Oak logs)\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:iron_arrow\",\n                    \"text\": \"Iron arrows\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"45 Arrow shafts (Willow logs)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:steel_arrow\",\n                    \"text\": \"Steel arrows\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"60 Arrow shafts (Maple logs)\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:mithril_arrow\",\n                    \"text\": \"Mithril Arrows\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:broad_arrow\",\n                    \"text\": \"Broad arrows\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"75 Arrow shafts (Yew logs)\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:adamant_arrow\",\n                    \"text\": \"Adamant arrows\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:arrow_shaft\",\n                    \"text\": \"90 Arrow shafts (Magic logs)\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:rune_arrow\",\n                    \"text\": \"Rune arrows\",\n                    \"level\": 75\n                }\n            ]\n        },\n        {\n            \"name\": \"Bows\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:shortbow\",\n                    \"text\": \"Shortbows\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:longbow\",\n                    \"text\": \"Longbows\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:oak_shortbow\",\n                    \"text\": \"Oak shortbows\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:oak_longbow\",\n                    \"text\": \"Oak longbows\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:willow_shortbow\",\n                    \"text\": \"Willow shortbows\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:willow_longbow\",\n                    \"text\": \"Willow longbows\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:maple_shortbow\",\n                    \"text\": \"Maple shortbows\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:maple_longbow\",\n                    \"text\": \"Maple longbows\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:yew_shortbow\",\n                    \"text\": \"Yew shortbows\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:yew_longbow\",\n                    \"text\": \"Yew longbows\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:magic_shortbow\",\n                    \"text\": \"Magic shortbows\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:magic_longbow\",\n                    \"text\": \"Magic longbows\",\n                    \"level\": 85\n                }\n            ]\n        },\n        {\n            \"name\": \"Bolts\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_bolt\",\n                    \"text\": \"Bronze bolts\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:opal_bolt\",\n                    \"text\": \"Opal-tipped bronze bolts\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:blurite_bolt\",\n                    \"text\": \"Blurite bolts (After Knight´s Sword quest)\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:jade_bolt\",\n                    \"text\": \"Jade-tipped blurite bolts\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:iron_bolt\",\n                    \"text\": \"Iron bolts\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:silver_bolt\",\n                    \"text\": \"Silver bolts\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:steel_bolt\",\n                    \"text\": \"Steel bolts\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:mithril_bolt\",\n                    \"text\": \"Mithril bolts\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:sapphire_bolt\",\n                    \"text\": \"Sapphire-tipped mithril bolts\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:emerald_bolt\",\n                    \"text\": \"Emerald-tipped mithril bolts\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:adamant_bolt\",\n                    \"text\": \"Adamant bolts\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:runite_bolt\",\n                    \"text\": \"Runite bolts\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:onyx_bolt\",\n                    \"text\": \"Onyx bolts\",\n                    \"level\": 69\n                }\n            ]\n        },\n        {\n            \"name\": \"Darts\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_dart\",\n                    \"text\": \"Bronze darts\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:iron_dart\",\n                    \"text\": \"Iron darts\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:steel_dart\",\n                    \"text\": \"Steel darts\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:mithril_dart\",\n                    \"text\": \"Mithril darts\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:adamant_dart\",\n                    \"text\": \"Adamant darts\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:rune_dart\",\n                    \"text\": \"Rune darts\",\n                    \"level\": 81\n                }\n            ]\n        },\n        {\n            \"name\": \"Javelins\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_javelin\",\n                    \"text\": \"Bronze javelins\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:iron_javelin\",\n                    \"text\": \"Iron javelins\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:steel_javelin\",\n                    \"text\": \"Steel javelins\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:mithril_javelin\",\n                    \"text\": \"Mithril javelins\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:adamant_javelin\",\n                    \"text\": \"Adamant javelins\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:rune_javelin\",\n                    \"text\": \"Rune javelins\",\n                    \"level\": 77\n                }\n            ]\n        },\n        {\n            \"name\": \"Other\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:battlestaff\",\n                    \"text\": \"Battlestaves\",\n                    \"level\": 40\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/herblore.json",
    "content": "{\n    \"id\": 125,\n    \"name\": \"Herblore\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Potions\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:herblore_attack_potion\",\n                    \"text\": \"Attack potion\\\\nGuam leaf & eye of newt\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:herblore_anti_poison_potion\",\n                    \"text\": \"Anti-poison potion\\\\nMarrentil & ground unicorn horn\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:herblore_relicyms_balm\",\n                    \"text\": \"Relicym's balm\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:herblore_strength_potion\",\n                    \"text\": \"Strength potion\\\\nTarromin & limpwurt root\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:herblore_serum_207\",\n                    \"text\": \"Serum 207\\\\nTarromin & ashes\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:herblore_stat_restore_potion\",\n                    \"text\": \"Stat restore potion\\\\nHarralander & red spiders' eggs\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:guthix_balance_potion\",\n                    \"text\": \"Guthix balance potion\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:herblore_energy_potion\",\n                    \"text\": \"Energy potion\\\\nHarralander & chocolate dust\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:herblore_defence_potion\",\n                    \"text\": \"Defence potion\\\\nRanarr weed & white berries\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:herblore_combat_potion\",\n                    \"text\": \"Combat potion\\\\nHarralander & ground desert goat horn\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:herblore_prayer_restore_potion\",\n                    \"text\": \"Prayer restore potion\\\\nRanarr weed & snape grass\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:herblore_super_attack_potion\",\n                    \"text\": \"Super attack potion\\\\nIrit leaf & eye of newt\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:herblore_super_anti_poison_potion\",\n                    \"text\": \"Super anti-poison potion\\\\nIrit leaf & ground unicorn horn\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:herblore_fishing_potion\",\n                    \"text\": \"Fishing potion\\\\nAvantoe & snape grass\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:herblore_super_energy_potion\",\n                    \"text\": \"Super energy potion\\\\nAvantoe & Mort Myre fungi\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:herblore_hunting_potion\",\n                    \"text\": \"Hunting potion - Avantoe & ground\\\\nsabre-toothed kebbit teeth\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:herblore_super_strength_potion\",\n                    \"text\": \"Super strength potion\\\\nKwuarm & limpwurt root\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:herblore_magic_essence_potion\",\n                    \"text\": \"Magic essence potion\\\\nStar flower & ground gorak's claw\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:herblore_weapon_poison\",\n                    \"text\": \"Weapon Poison\\\\nKwuarm & ground blue dragon scale\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:herblore_super_defence_potion\",\n                    \"text\": \"Super defence potion\\\\nCadantine & white berries\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:herblore_antidote_plus\",\n                    \"text\": \"Antidote+\\\\nCoconut milk, toadflax & yew roots\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:herblore_anti_firebreath_potion\",\n                    \"text\": \"Anti-firebreath potion\\\\nLantadyme & ground blue dragon scale\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:herblore_ranging_potion\",\n                    \"text\": \"Ranging potion\\\\nDwarf weed & wine of Zamorak\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:herblore_weapon_poison_plus\",\n                    \"text\": \"Weapon poison(+)\\\\nCoconut milk, cactus spine & red spiders' eggs\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:herblore_magic_potion\",\n                    \"text\": \"Magic potion\\\\nLantadyme & potato cactus\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:herblore_zamorak_brew\",\n                    \"text\": \"Zamorak brew\\\\nTorstol & jangerberries\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:herblore_antidote_plus_plus\",\n                    \"text\": \"Antidote++\\\\nCoconut milk, irit leaf & magic tree roots\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:herblore_saradomin_brew\",\n                    \"text\": \"Saradomin brew\\\\nToadflax & crushed birdnest\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:herblore_weapon_poison_plus_plus\",\n                    \"text\": \"Weapon poison(++)\\\\nCoconut milk, nightshade & poison ivy berries\",\n                    \"level\": 82\n                }\n            ]\n        },\n        {\n            \"name\": \"Herbs\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:guam_leaf\",\n                    \"text\": \"Guam leaf\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:rogues_purse\",\n                    \"text\": \"Rogue's purse\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:snake_weed\",\n                    \"text\": \"Snake weed\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:marrentill\",\n                    \"text\": \"Marrentill\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:tarromin\",\n                    \"text\": \"Tarromin\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:harralander\",\n                    \"text\": \"Harralander\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:ranarr_weed\",\n                    \"text\": \"Ranarr weed\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:toadflax\",\n                    \"text\": \"Toadflax\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:irit_leaf\",\n                    \"text\": \"Irit leaf\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:avantoe\",\n                    \"text\": \"Avantoe\",\n                    \"level\": 48\n                },\n                {\n                    \"item\": \"rs:kwuarm\",\n                    \"text\": \"Kwuarm\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:snapdragon\",\n                    \"text\": \"Snapdragon\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:cadantine\",\n                    \"text\": \"Cadantine\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:lantadyme\",\n                    \"text\": \"Lantadyme\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:dwarf_weed\",\n                    \"text\": \"Dwarf weed\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:torstol\",\n                    \"text\": \"Torstol\",\n                    \"level\": 75\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/hitpoint.json",
    "content": "{\n    \"id\": 119,\n    \"name\": \"Hitpoints\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Hitpoints\",\n            \"lines\": [\n                {\n                    \"text\": \"Hitpoints are used to tell you how healthy\\\\n your character is. A character who reaches\\\\n0 Hitpoints has died, but will reappear in\\\\ntheir chosen respawn location(normally Lumbridge).\\\\n\\\\nIf you see any red 'hit splash' during combat\\\\n, the number shown corresponds to the number\\\\n of Hitpoints lost as a result of that strike.\\\\n\\\\nBlue hit splash means no damage has been dealt.\\\\n\\\\nGreen hit splash are poison damage.\",\n                    \"level\": \"\"\n                }\n            ]\n        },\n        {\n            \"name\": \"Healing\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:purple_sweets\",\n                    \"text\": \"Purple Sweets: Restores 1-3 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:anchovies\",\n                    \"text\": \"Anchovies: Restores 1 Hitpoint\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:shrimp\",\n                    \"text\": \"Shrimp: Restores 3 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cooked_chicken\",\n                    \"text\": \"Cooked chicken: Restores 3 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:sardine\",\n                    \"text\": \"Sardine: Restores 3 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cooked_meat\",\n                    \"text\": \"Cooked meat: Restores 3 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:bread\",\n                    \"text\": \"Bread: Restores 5 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:bread\",\n                    \"text\": \"Bread: Restores 5 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:herring\",\n                    \"text\": \"Herring: Restores 5 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cooked_rabbit\",\n                    \"text\": \"Cooked Rabbit: Restores 5 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:mackerel\",\n                    \"text\": \"Mackerel: Restores 6 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:botanical_pie\",\n                    \"text\": \"Botanical Pie: Restores 6 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:slimy_eel\",\n                    \"text\": \"Slimy Eel: Restores 6-10 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:trout\",\n                    \"text\": \"Trout: Restores 7 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cod\",\n                    \"text\": \"Cod: Restores 7 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:roast_rabbit\",\n                    \"text\": \"Roast Rabbit: Restores 7 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cave_eel\",\n                    \"text\": \"Cave Eel: Restores 7-11 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:pike\",\n                    \"text\": \"Pike: Restores 8 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:salmon\",\n                    \"text\": \"Salmon: Restores 9 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:redberry_pie\",\n                    \"text\": \"Redberry Pie: Restores 9 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:tuna\",\n                    \"text\": \"Tuna: Restores 10 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:crab_meat\",\n                    \"text\": \"Crab meat: Restores 10 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:fishcake\",\n                    \"text\": \"Cooked Fishcake: Restores 11 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:jug_of_wine\",\n                    \"text\": \"Jug of wine: Restores 11 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:meat_pie\",\n                    \"text\": \"Meat pie: Restores 11 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:lava_eel\",\n                    \"text\": \"Lava Eel: Restores 11 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:garden_pie\",\n                    \"text\": \"Garden pie: Restores 12 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:fish_pie\",\n                    \"text\": \"Fish pie: Restores 12 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cake\",\n                    \"text\": \"Cake: Restores 12 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:lobster\",\n                    \"text\": \"Lobster: Restores 12 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:bass\",\n                    \"text\": \"Bass: Restores 13 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:swordfish\",\n                    \"text\": \"Swordfish: Restores 14 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:plain_pizza\",\n                    \"text\": \"Plain pizza: Restores 14 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:apple_pie\",\n                    \"text\": \"Apple pie: Restores 14 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:potato_with_butter\",\n                    \"text\": \"Potato with butter: Restores 14 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:chilli_potato\",\n                    \"text\": \"Chilli Potato: Restores 14 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:chocolate_cake\",\n                    \"text\": \"Chocolate Cake: Restores 15 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:monkfish\",\n                    \"text\": \"Monkfish: Restores 16 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:admiral_pie\",\n                    \"text\": \"Admiral pie: Restores 16 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:meat_pizza\",\n                    \"text\": \"Meat pizza: Restores 16 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:potato_with_cheese\",\n                    \"text\": \"Potato with cheese: Restores 16 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:egg_potato\",\n                    \"text\": \"Egg Potato: Restores 16 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:cooked_karambwan\",\n                    \"text\": \"Cooked karambwan: Restores 18 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:anchovy_pizza\",\n                    \"text\": \"Anchovy pizza: Restores 18 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:ugthanki_kebab\",\n                    \"text\": \"Ugthanki kebab: Restores 19 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:jug_of_wine\",\n                    \"text\": \"Jug of wine: Restores 11 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:shark\",\n                    \"text\": \"Shark: Restores 20 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:mushroom_potato\",\n                    \"text\": \"Mushroom Potato: Restores 20 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:sea_turtle\",\n                    \"text\": \"Sea Turtle: Restores 21 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:manta_ray\",\n                    \"text\": \"Manta Ray: Restores 22 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:tuna_potato\",\n                    \"text\": \"Tuna Potato: Restores 22 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:wild_pie\",\n                    \"text\": \"Wild pie: Restores 22 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:summer_pie\",\n                    \"text\": \"Summer pie: Restores 22 Hitpoints\\\\n(Members only)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:pineapple_pizza\",\n                    \"text\": \"Pineapple pizza: Restores 22 Hitpoints\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:saradomin_brew\",\n                    \"text\": \"Saradomin brew: Restores 15% of your\\\\nHitpoints level plus 2 - can boost beyond\\\\nyour level (members only)\",\n                    \"level\": \"\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/magic.json",
    "content": "{\n    \"id\": 137,\n    \"name\": \"Magic\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Normal Spells\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:battle_staff\",\n                    \"text\": \"Click a spell icon below to see the\\\\nrequired runes to cast each spell.\\\\n\\\\nBy opening the spellbook icon on the side\\\\n interface, you can see what each spell does\\\\nby moving your mouse over its icon.\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_home_teleport\",\n                    \"text\": \"Click a spell icon below to see the\\\\nrequired runes to cast each spell.\\\\n\\\\nBy opening the spellbook icon on the side\\\\n interface, you can see what each spell does\\\\nby moving your mouse over its icon.\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_wind_strike\",\n                    \"text\": \"Wind Strike\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:spell_confuse\",\n                    \"text\": \"Confuse\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:spell_water_strike\",\n                    \"text\": \"Water Strike\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:spell_level_1_enchant\",\n                    \"text\": \"Level 1 enchant\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:spell_earth_strike\",\n                    \"text\": \"Earth Strike\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:spell_weaken\",\n                    \"text\": \"Weaken\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:spell_fire_strike\",\n                    \"text\": \"Fire Strike\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:spell_bones_to_bananas\",\n                    \"text\": \"Bones to bananas\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:spell_wind_bolt\",\n                    \"text\": \"Wind Bolt\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:spell_curse\",\n                    \"text\": \"Curse\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:spell_bind\",\n                    \"text\": \"Bind\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:spell_low_level_alchemy\",\n                    \"text\": \"Low level alchemy\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:spell_water_bolt\",\n                    \"text\": \"Water Bolt\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:spell_varrock_teleport\",\n                    \"text\": \"Varrock teleport\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:spell_level_2_enchant\",\n                    \"text\": \"Level 2 enchant\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:spell_earth_bolt\",\n                    \"text\": \"Earth Bolt\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:spell_lumbridge_teleport\",\n                    \"text\": \"Lumbridge teleport\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:spell_telekinetic_grab\",\n                    \"text\": \"Telekinetic grab\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:spell_fire_bolt\",\n                    \"text\": \"Fire Bolt\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:spell_falador_teleport\",\n                    \"text\": \"Falador teleport\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:spell_crumble_undead\",\n                    \"text\": \"Crumble undead\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:spell_teleport_to_house\",\n                    \"text\": \"Teleport to house\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:spell_wind_blast\",\n                    \"text\": \"Wind Blast\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:spell_superheat_item\",\n                    \"text\": \"Superheat item\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:spell_camelot_teleport\",\n                    \"text\": \"Members: Camelot teleport\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:spell_water_blast\",\n                    \"text\": \"Water Blast\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:spell_level_3_enchant\",\n                    \"text\": \"Level 3 enchant\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:spell_iban_blast\",\n                    \"text\": \"Members: Iban blast\\\\n(after Underground Pass)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:spell_snare\",\n                    \"text\": \"Snare\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:spell_magic_dart\",\n                    \"text\": \"Members: Magic dart\\\\n(with 55 Slayer)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:spell_ardougne_teleport\",\n                    \"text\": \"Members: Ardougne teleport\\\\n(after Plague City)\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:spell_earth_blast\",\n                    \"text\": \"Earth Blast\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:spell_high_level_alchemy\",\n                    \"text\": \"High level alchemy\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:spell_charge_water_orb\",\n                    \"text\": \"Members: Charge water orb\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:spell_level_4_enchant\",\n                    \"text\": \"Level 4 enchant\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:spell_watchtower_teleport\",\n                    \"text\": \"Members: Watchtower teleport\\\\n(after Watchtower)\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:spell_fire_blast\",\n                    \"text\": \"Fire Blast\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:spell_charge_earth_orb\",\n                    \"text\": \"Members: Charge earth orb\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_bones_to_peaches\",\n                    \"text\": \"Members: Bones to peaches\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_claws_of_guthix\",\n                    \"text\": \"Members: Claws of Guthix\\\\n(after Mage Arena)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_flames_of_zamorak\",\n                    \"text\": \"Members: Flames of Zamorak\\\\n(after Mage Arena\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_saradomin_strike\",\n                    \"text\": \"Members: Saradomin Strike\\\\n(after Mage Arena)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_trollheim_teleport\",\n                    \"text\": \"Members: Trollheim teleport\\\\n(after Eadgar's Ruse)\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:spell_wind_wave\",\n                    \"text\": \"Members: Wind Wave\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:spell_charge_fire_orb\",\n                    \"text\": \"Members: Charge fire orb\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:spell_ape_atoll_teleport\",\n                    \"text\": \"Members: Ape atoll teleport\\\\n(after Recipe for Disaster)\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:spell_water_wave\",\n                    \"text\": \"Members: Water wave\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:spell_charge_air_orb\",\n                    \"text\": \"Members: Charge air orb\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:spell_vulnerability\",\n                    \"text\": \"Members: Vulnerability\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:spell_level_5_enchant\",\n                    \"text\": \"Members: Level 5 enchant\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:spell_earth_wave\",\n                    \"text\": \"Members: Earth Wave\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:spell_enfeeble\",\n                    \"text\": \"Members: Enfeeble\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:spell_teleother_lumbridge\",\n                    \"text\": \"Members: Teleother Lumbridge\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:spell_fire_wave\",\n                    \"text\": \"Members: Fire Wave\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:spell_entangle\",\n                    \"text\": \"Members: Entangle\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:spell_stun\",\n                    \"text\": \"Members: Stun\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:spell_charge\",\n                    \"text\": \"Members: Charge\\\\n(after Mage Arena)\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:spell_teleother_falador\",\n                    \"text\": \"Members: Teleother to Falador\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:spell_tele_block\",\n                    \"text\": \"Tele block\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:spell_level_6_enchant\",\n                    \"text\": \"Level 6 enchant\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:spell_teleother_camelot\",\n                    \"text\": \"Members: Teleother to Camelot\",\n                    \"level\": 90\n                }\n            ]\n        },\n        {\n            \"name\": \"Ancient Magicks\",\n            \"members\": true,\n            \"lines\": [\n                {\n                    \"item\": \"rs:ancient_magick_guide\",\n                    \"text\": \"Ancient magicks are available after\\\\ncompleting the Desert Treasure quest.\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_home_teleport_ancient\",\n                    \"text\": \"Home teleport (Edgeville)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_smoke_rush\",\n                    \"text\": \"Smoke rush\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:spell_shadow_rush\",\n                    \"text\": \"Shadow rush\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:spell_paddewwa_teleport\",\n                    \"text\": \"Paddewwa teleport\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:spell_blood_rush\",\n                    \"text\": \"Blood rush\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:spell_ice_rush\",\n                    \"text\": \"Mithril\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:spell_senntisten_teleport\",\n                    \"text\": \"Senntisten teleport\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:spell_smoke_burst\",\n                    \"text\": \"Smoke burst\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:spell_shadow_burst\",\n                    \"text\": \"Shadow burst\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:Kharyll teleport\",\n                    \"text\": \"Kharyll teleport\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:spell_blood_burst\",\n                    \"text\": \"Blood burst\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:spell_ice_burst\",\n                    \"text\": \"Ice burst\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:spell_lassar_teleport\",\n                    \"text\": \"Lassar teleport\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:spell_smoke_blitz\",\n                    \"text\": \"Smoke Blitz\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:spell_shadow_blitz\",\n                    \"text\": \"Shadow blitz\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:spell_dareeyak_teleport\",\n                    \"text\": \"Dareeyak teleport\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:spell_blood_blitz\",\n                    \"text\": \"Blood Blitz\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:spell_ice_blitz\",\n                    \"text\": \"Ice blitz\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:guthans_warspear\",\n                    \"text\": \"Members: Guthan's warspear\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:spell_carrallangar_teleport\",\n                    \"text\": \"Carrallangar teleport\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:spell_smoke_barrage\",\n                    \"text\": \"Smoke barrage\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:spell_shadow_barrage\",\n                    \"text\": \"Shadow barrage\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:spell_annakarl teleport\",\n                    \"text\": \"Annakarl teleport\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:spell_blood_barrage\",\n                    \"text\": \"Blood barrage\",\n                    \"level\": 92\n                },\n                {\n                    \"item\": \"rs:spell_ice_barrage\",\n                    \"text\": \"Ice barrage\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:spell_ghorrock_teleport\",\n                    \"text\": \"Ghorrock teleport\",\n                    \"level\": 96\n                }\n            ]\n        },\n        {\n            \"name\": \"Lunar Spells\",\n            \"members\": true,\n            \"lines\": [\n                {\n                    \"item\": \"rs:lunar_spells_guide\",\n                    \"text\": \"Lunar spells are available after completing\\\\nthe Lunar Diplomacy quest.\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_home_teleport_lunar\",\n                    \"text\": \"Home teleport (Lunar Isle)\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:spell_bake_pie\",\n                    \"text\": \"Bake pie\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:spell_geomancy\",\n                    \"text\": \"Geomancy\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:spell_cure_plant\",\n                    \"text\": \"Cure plant\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:spell_npc_contact\",\n                    \"text\": \"NPC Contact\",\n                    \"level\": 67\n                },\n                {\n                    \"item\": \"rs:spell_cure_other\",\n                    \"text\": \"Cure other\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:spell_moonclan_teleport\",\n                    \"text\": \"Moonclan teleport\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_moonclan\",\n                    \"text\": \"Tele group Moonclan\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:spell_cure_me\",\n                    \"text\": \"Cure me\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:spell_ourania_teleport\",\n                    \"text\": \"Ourania teleport\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:spell_waterbirth_teleport\",\n                    \"text\": \"Waterbirth teleport\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_waterbirth\",\n                    \"text\": \"Tele group Waterbirth\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:spell_barbarian_teleport\",\n                    \"text\": \"Barbarian teleport\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_barbarian\",\n                    \"text\": \"Tele group Barbarian\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:spell_spin_flax\",\n                    \"text\": \"Spin Flax\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:spell_superglass_make\",\n                    \"text\": \"Superglass make\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:spell_khazard_teleport\",\n                    \"text\": \"Khazard teleport\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_khazard\",\n                    \"text\": \"Tele group Khazard\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:spell_string_jewellery\",\n                    \"text\": \"String jewellery\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:spell_stat_restore_pot_share\",\n                    \"text\": \"Stat restore pot share\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:spell_magic_imbue\",\n                    \"text\": \"Magic imbue\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:spell_fertile_soil\",\n                    \"text\": \"Fertile soil\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:spell_boost_potion_share\",\n                    \"text\": \"Boost potion share\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:spell_fishing_guild_teleport\",\n                    \"text\": \"Fishing guild teleport\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_fishing_guild\",\n                    \"text\": \"Tele  group fishing guild\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:spell_catherby_teleport\",\n                    \"text\": \"Catherby teleport\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_catherby\",\n                    \"text\": \"Tele group Catherby\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:spell_recharge_dragonstone\",\n                    \"text\": \"Recharge Dragonstone\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:spell_ice_plateau_teleport\",\n                    \"text\": \"Ice plateau teleport\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:spell_telegroup_ice_plateau\",\n                    \"text\": \"Tele group Ice plateau\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:spell_energy_transfer\",\n                    \"text\": \"Energy transfer\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:spell_heal_other\",\n                    \"text\": \"Heal other\",\n                    \"level\": 92\n                },\n                {\n                    \"item\": \"rs:spell_vengeance_other\",\n                    \"text\": \"Vengeance other\",\n                    \"level\": 93\n                },\n                {\n                    \"item\": \"rs:spell_vengeancer\",\n                    \"text\": \"Vengeance\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:spell_heal_group\",\n                    \"text\": \"Heal group\",\n                    \"level\": 95\n                }\n            ]\n        },\n        {\n            \"name\": \"Armour\",\n            \"members\": true,\n            \"lines\": [\n                {\n                    \"item\": \"rs:wizard_boots\",\n                    \"text\": \"Wizard boots\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rsmystic_robes\",\n                    \"text\": \"Mystic robes\\\\n(20 Defence required)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:enchanted_robes\",\n                    \"text\": \"Enchanted robes\\\\n(20 Defence required)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:splitbark_armour\",\n                    \"text\": \"Splitbark armour\\\\n(40 Defence required)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:skeletal_armour\",\n                    \"text\": \"Skeletal_armour\\\\n(after The Fremmenik Trials, 40 Defence required\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:infinity_robes\",\n                    \"text\": \"Infinity robes\\\\n(25 Defence required)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:god_capes\",\n                    \"text\": \"God capes\\\\n(after Mage Arena)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:lunar_armour\",\n                    \"text\": \"Lunar armour\\\\n(after Lunar Diplomacy, 40 Defence required)\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:ahrims_robes\",\n                    \"text\": \"Ahrims robes\\\\n(with 70 Defence)\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Bolts\",\n            \"members\": true,\n            \"lines\": [\n                {\n                    \"item\": \"rs:spell_enchant_opal_tipped_crossbow_bolts\",\n                    \"text\": \"Opal-tipped crossbow bolts\\\\n1 cosmic + 2 air runes\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:spell_enchant_sapphire_tipped_crossbow_bolts\",\n                    \"text\": \"Sapphire-tipped crossbow bolts\\\\n1 cosmic + 1 water + 1 mind rune\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:spell_enchant_jade_tipped_crossbow_bolts\",\n                    \"text\": \"Jade-tipped crossbow bolts\\\\n1 cosmic + 2 earth runes\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:spell_enchant_pearl_tipped_crossbow_bolts\",\n                    \"text\": \"Pearl-tipped crossbow bolts\\\\n1 cosmic + 2 water runes\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:spell_enchant_emerald_tipped_crossbow_bolts\",\n                    \"text\": \"Emerald-tipped crossbow bolts\\\\n1 cosmic + 3 air runes + 1 nature rune\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:spell_enchant_red_topaz_tipped_crossbow_bolts\",\n                    \"text\": \"Red topaz-tipped crossbow bolts\\\\n1 cosmic + 2 blood runes\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:spell_enchant_ruby_tipped_crossbow_bolts\",\n                    \"text\": \"Opal-tipped crossbow bolts\\\\n1 cosmic + 5 fire + 1 blood runes\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:spell_enchant_diamond_tipped_crossbow_bolts\",\n                    \"text\": \"Diamond-tipped crossbow bolts\\\\n1 cosmic + 10 earth + 2 law runes\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:spell_enchant_dragonstone_tipped_crossbow_bolts\",\n                    \"text\": \"Dragonstone-tipped crossbow bolts\\\\n1 cosmic + 15 earth + 1 soul runes\",\n                    \"level\": 68\n                },\n                {\n                    \"item\": \"rs:spell_enchant_onyx_tipped_crossbow_bolts\",\n                    \"text\": \"Onyx-tipped crossbow bolts\\\\n1 cosmic + 20 fire + 1 death runes\",\n                    \"level\": 87\n                }\n            ]\n        },\n        {\n            \"name\": \"Weapons\",\n            \"members\": true,\n            \"lines\": [\n                {\n                    \"item\": \"rs:battlestaves\",\n                    \"text\": \"Battlestaves\\\\n(30 Attack required)\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:mysticstaves\",\n                    \"text\": \"Mystic staves\\\\n(40 Attack required)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:beginner_wand\",\n                    \"text\": \"Beginner wand\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:apprentice_wand\",\n                    \"text\": \"Apprentice wand\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:ancient_staff\",\n                    \"text\": \"Ancient staff\\\\n(after Desert Treasure, 50 Attack required\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:slayer_staff\",\n                    \"text\": \"Slayer staff\\\\n(with 55 Slayer)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:teacher_wand\",\n                    \"text\": \"Teacher wand\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:master_wand\",\n                    \"text\": \"Master wand\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:god_staves\",\n                    \"text\": \"God staves\\\\n(after Mage Arena)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:toktz-mej-tal\",\n                    \"text\": \"TokTz-Mej-Tal\\\\n(60 Attack required)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:ahrims_robes\",\n                    \"text\": \"Ahrim's staff\\\\n(70 Attack required)\",\n                    \"level\": 70\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/mining.json",
    "content": "{\n    \"id\": 120,\n    \"name\": \"Mining\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Rocks\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:rune_essence\",\n                    \"text\": \"Rune essence (after RuneMysteries quest)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:clay\",\n                    \"text\": \"Clay\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:copper_ore\",\n                    \"text\": \"Copper ore\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:tin_ore\",\n                    \"text\": \"Tin ore\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:blurite_ore\",\n                    \"text\": \"Blurite ore\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:limestone\",\n                    \"text\": \"Members: Limestone\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:iron_ore\",\n                    \"text\": \"Iron ore\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:silver_ore\",\n                    \"text\": \"Silver ore\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:coal\",\n                    \"text\": \"Coal\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:gold_ore\",\n                    \"text\": \"Gold\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:granite\",\n                    \"text\": \"Members: Granite\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:mithril_ore\",\n                    \"text\": \"Mithril ore\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:adamantite_ore\",\n                    \"text\": \"Adamantite ore\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:soft_clay\",\n                    \"text\": \"Members: Soft clay\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:runite_ore\",\n                    \"text\": \"Runite ore\",\n                    \"level\": 85\n                }\n            ]\n        },\n        {\n            \"name\": \"Equipment\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_pickaxe\",\n                    \"text\": \"Bronze pickaxe\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:iron_pickaxe\",\n                    \"text\": \"Iron pickaxe\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:steel_pickaxe\",\n                    \"text\": \"Steel pickaxe\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:mithril_pickaxe\",\n                    \"text\": \"Mithril pickaxe\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:adamant_pickaxe\",\n                    \"text\": \"Adamant pickaxe\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:rune_pickaxe\",\n                    \"text\": \"Rune pickaxe\",\n                    \"level\": 41\n                }\n            ]\n        },\n        {\n            \"name\": \"Areas\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:mithril_ore\",\n                    \"text\": \"Mining guild\",\n                    \"level\": 60\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/prayer.json",
    "content": "{\n    \"id\": 130,\n    \"name\": \"Prayer\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Prayers\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:prayer_guide\",\n                    \"text\": \"While any of your prayers are active, your prayer\\\\n points will drain. Once you have run out of prayer\\\\n points, you will no longer be able to use prayers.\\\\n Visit an altar to recharge your prayer points.\",\n                    \"level\": \"\"\n                },\n                {\n                    \"item\": \"rs:thick_skin\",\n                    \"text\": \"Thick Skin\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:burst_of_strength\",\n                    \"text\": \"Burst of Strength\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:clarity_of_thought\",\n                    \"text\": \"Clariy of Thought\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:sharp_eye\",\n                    \"text\": \"Sharp Eye\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:mystic_will\",\n                    \"text\": \"Mystic Will\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:rock_skin\",\n                    \"text\": \"Rock Skin\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:superhuman_strength\",\n                    \"text\": \"Superhuman Strength\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:improved_reflexes\",\n                    \"text\": \"Improved Reflexes\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:rapid_restore\",\n                    \"text\": \"Rapid Restore\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:rapid_heal\",\n                    \"text\": \"Rapid Heal\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:protect_item\",\n                    \"text\": \"Protect Item\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:hawk_eye\",\n                    \"text\": \"Hawk Eye\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:mystic_lore\",\n                    \"text\": \"Mystical Lore\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:steel_skin\",\n                    \"text\": \"Steel Skin\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:ultimate_strength\",\n                    \"text\": \"Ultimate Strength\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:incredible_reflexes\",\n                    \"text\": \"Incredible Reflexes\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:protect_from_magic\",\n                    \"text\": \"Protect from Magic\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:protect_from_missiles\",\n                    \"text\": \"Protect from Missiles\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:protect_from_melee\",\n                    \"text\": \"Protect from Melee\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:eagle_eye\",\n                    \"text\": \"Eagle Eye\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:mystic_might\",\n                    \"text\": \"Mystic Might\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:retribution\",\n                    \"text\": \"Members: Retribution\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:redemption\",\n                    \"text\": \"Members: Redemption\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:smite\",\n                    \"text\": \"Members: Smite\",\n                    \"level\": 52\n                }\n            ]\n        },\n        {\n            \"name\": \"Equipment\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:initate_full_helmet\",\n                    \"text\": \"Initiate armour\\\\n(after Recruitment Drive, with\\\\nlevel 20 defence.\",\n                    \"level\": \"10\"\n                },\n                {\n                    \"item\": \"rs:proselyte_full_helmet\",\n                    \"text\": \"Proselyte armour\\\\n(after Slug Menace, with\\\\nlevel 30 defence.\",\n                    \"level\": \"20\"\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/ranged.json",
    "content": "{\n    \"id\": 127,\n    \"name\": \"Ranged\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Bows\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:standard_bow\",\n                    \"text\": \"Standard bows\\\\n/Ammo: Arrows up to iron\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:oak_bow\",\n                    \"text\": \"Oak bows\\\\nAmmo: Arrows up to steel\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:willow_bow\",\n                    \"text\": \"Willow bows\\\\nAmmo: Arrows up to mithril\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:maple_bow\",\n                    \"text\": \"Maple bows\\\\nAmmo: Arrows up to adamant\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:ogre_composite_bow\",\n                    \"text\": \"Members: Ogre, composite bows\\\\nAmmo:'brutal' arrows up to rune\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:yew_bow\",\n                    \"text\": \"Yew bows\\\\nAmmo: Arrows up to rune\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:magic_bow\",\n                    \"text\": \"Magic bows\\\\nAmmo: Arrows up to rune\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:seercull_bow\",\n                    \"text\": \"Members: Seercull bows\\\\nAmmo: Arrows up to rune\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:crystal_bow\",\n                    \"text\": \"Members: Crystal bows (with 50 Agility)\\\\nAmmo: None\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Thrown\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_knife\",\n                    \"text\": \"Bronze knifes\\\\nthrowing knifes\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:iron_knife\",\n                    \"text\": \"Iron knifes\\\\nthrowing knifes\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:steel_knife\",\n                    \"text\": \"Steel knifes\\\\nthrowing knifes\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:black_knife\",\n                    \"text\": \"Black knifes\\\\nthrowing knifes\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:mithril_knife\",\n                    \"text\": \"Mithril knifes\\\\nthrowing knifes\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:adamant_knife\",\n                    \"text\": \"Adamant knifes\\\\nthrowing knifes)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:rune_knife\",\n                    \"text\": \"Rune knifes\\\\nthrowing knifes\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:toktz-xil-ul\",\n                    \"text\": \"Toktz xil ul\\\\nobsidian throwing rings\",\n                    \"level\": 60\n                }\n            ]\n        },\n        {\n            \"name\": \"Crossbows\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:crossbow\",\n                    \"text\": \"Crossbow\\\\nAmmo: Bronze crossbow bolts\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:phoenix_crossbow\",\n                    \"text\": \"Phoenix crossbow\\\\nAmmo: Bronze crossbow bolts\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:bronze_crossbow\",\n                    \"text\": \"Members: Crossbow\\\\nAmmo: Bronze crossbow bolts\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:blurite_crossbow\",\n                    \"text\": \"Members: Blurite Crossbow\\\\nAmmo: bolts up to blurite\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:iron_crossbow\",\n                    \"text\": \"Members: Crossbow\\\\nAmmo: Bolts up to iron\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:dorgeshuun_crossbow\",\n                    \"text\": \"Members: Crossbow\\\\nAmmo: Bolts up to iron\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:steel_crossbow\",\n                    \"text\": \"Members: Steel crossbow\\\\nAmmo: Bolts up to steel\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:mithril_crossbow\",\n                    \"text\": \"Members: mithril crossbow\\\\nAmmo: Bolts up to mithril\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:adamant_crossbow\",\n                    \"text\": \"Members: Adamant crossbow\\\\nAmmo: Bolts up to adamant\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:rune_crossbow\",\n                    \"text\": \"Members: Rune crossbow\\\\nAmmo: Bolts up to rune\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:karils_crossbow\",\n                    \"text\": \"Members: Karil's crossbow\\\\nAmmo: Bolt rack only\",\n                    \"level\": 70\n                }\n            ]\n        },\n        {\n            \"name\": \"Armour\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:plain_leather_items\",\n                    \"text\": \"Plain leather items\\\\nwield any bronze equipment\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:hard_leather_body\",\n                    \"text\": \"Hard leather body\\\\n(10 Defence required)\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:studded_leather_body\",\n                    \"text\": \"Studded leather chaps\\\\n(20 Defence required)\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:studded_leather_chaps\",\n                    \"text\": \"Studded leather chaps\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:coif\",\n                    \"text\": \"Coif\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:snake_skin_armour\",\n                    \"text\": \"Members: Snakeskin armour\\\\n(30 Defence required)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:ranger_boots\",\n                    \"text\": \"Ranger boots\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:robin_hood_hat\",\n                    \"text\": \"Robin Hood hat\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_vambraces\",\n                    \"text\": \"Members: Green dragonhide vambraces)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_chaps\",\n                    \"text\": \"Members: Proselyte armour)\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:green_dragonhide_body\",\n                    \"text\": \"Green dragonhide body\\\\n(40 Defence required)\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_vambraces\",\n                    \"text\": \"Members: Blue dragonhide vambraces)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_chaps\",\n                    \"text\": \"Members: Blue dragonhide chaps\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:blue_dragonhide_body\",\n                    \"text\": \"Members: Blue dragonhide body\\\\n(40 Defence required)\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_vambraces\",\n                    \"text\": \"Members: Red dragonhide vambraces)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_chaps\",\n                    \"text\": \"Members: Red dragonhide chaps)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:red_dragonhide_body\",\n                    \"text\": \"Members: Red dragonhide body\\\\n(40 Defence required)\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_vambraces\",\n                    \"text\": \"Members: Black dragonhide vambraces)\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_chaps\",\n                    \"text\": \"Members: Black dragonhide chaps)\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:black_dragonhide_body\",\n                    \"text\": \"Members: Black dragonhide body\\\\n(40 Defence required)\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:karils_leathertop\",\n                    \"text\": \"Members: Karil's leather armour\\\\(70 Defence required)\",\n                    \"level\": 70\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/runecrafting.json",
    "content": "{\n    \"id\": 140,\n    \"name\": \"Runecrafting\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Runes\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"Air runes\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"Mind runes\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:water_rune\",\n                    \"text\": \"Water runes\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:mist_rune\",\n                    \"text\": \"Members: Mist runes\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:earth_rune\",\n                    \"text\": \"Earth runes\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:dust_rune\",\n                    \"text\": \"Members: Dust runes\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:mud_rune\",\n                    \"text\": \"Members: Mud runes\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:fire_rune\",\n                    \"text\": \"Fire runes\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:smoke_rune\",\n                    \"text\": \"Members: Smoke runes\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:steam_rune\",\n                    \"text\": \"Members: Steam runes\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:body_rune\",\n                    \"text\": \"Body runes\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:lava_rune\",\n                    \"text\": \"Members: Lava runes\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:cosmic_rune\",\n                    \"text\": \"Members: Cosmic runes\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:chaos_rune\",\n                    \"text\": \"Members: Chaos runes\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:astral_rune\",\n                    \"text\": \"Members: Astral runes\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:nature_rune\",\n                    \"text\": \"Members: Nature runes\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:law_rune\",\n                    \"text\": \"Members: Law runes\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:death_rune\",\n                    \"text\": \"Members: Death runes\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:blood_rune\",\n                    \"text\": \"Members: Blood runes\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:soul_rune\",\n                    \"text\": \"Members: Soul runes\",\n                    \"level\": 90\n                }\n            ]\n        },\n        {\n            \"name\": \"Multiple\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"2 Air runes per essence\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"2 Mind runes per essence\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:water_rune\",\n                    \"text\": \"2 Water runes per essence\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"3 Air runes per essence\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:earth_rune\",\n                    \"text\": \"2 Earth runes per essence\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"3 Mind runes per essence\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"4 Air runes per essence\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:fire_rune\",\n                    \"text\": \"2 Fire runes per essence\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:water_rune\",\n                    \"text\": \"3 Water runes per essence\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"4 Mind runes per essence\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"5 Air runes per essence\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:body_rune\",\n                    \"text\": \"2 Body runes per essence\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:earth_rune\",\n                    \"text\": \"3 Earth runes per essence\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"6 Air runes per essence\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"5 Mind runes per essence\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:water_rune\",\n                    \"text\": \"4 Water runes per essence\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:cosmic_rune\",\n                    \"text\": \"Members: 2 Cosmic runes per essence\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"7 Air runes per essence\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"6 Mind runes per essence\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:fire_rune\",\n                    \"text\": \"3 Fire runes per essence\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:chaos_rune\",\n                    \"text\": \"Members: 2 Chaos runes per essence\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:water_rune\",\n                    \"text\": \"5 Water runes per essence\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"8 Air runes per essence\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:earth_rune\",\n                    \"text\": \"4 Earth runes per essence\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:astral_rune\",\n                    \"text\": \"Members: 2 Astral runes per essence\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"7 Mind runes per essence\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"9 Air runes per essence\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:nature_rune\",\n                    \"text\": \"Members: 2 Nature runes per essence\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:body_rune\",\n                    \"text\": \"3 Body runes per essence\",\n                    \"level\": 92\n                },\n                {\n                    \"item_id\": \"rs:water_rune\",\n                    \"text\": \"6 Water runes per essence\",\n                    \"level\": 95\n                },\n                {\n                    \"item\": \"rs:law_rune\",\n                    \"text\": \"Members: 2 Law runes per essence\",\n                    \"level\": 95\n                },\n                {\n                    \"item\": \"rs:mind_rune\",\n                    \"text\": \"8 Mind runes per essence\",\n                    \"level\": 98\n                },\n                {\n                    \"item\": \"rs:air_rune\",\n                    \"text\": \"10 Air runes per essence\",\n                    \"level\": 99\n                },\n                {\n                    \"item\": \"rs:death_rune\",\n                    \"text\": \"Members: 2 Death runes per essence\",\n                    \"level\": 99\n                }\n            ]\n        },\n        {\n            \"name\": \"Pouches\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:small_pouch\",\n                    \"text\": \"Small pouch: Holds 3 extra essence\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:medium_pouch\",\n                    \"text\": \"Medium pouch: Holds 6 extra essence\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:large_pouch\",\n                    \"text\": \"Large pouch: Holds 9 extra essence\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:giant_pouch\",\n                    \"text\": \"Giant pouch: Holds 12 extra essence\",\n                    \"level\": 75\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/skill-guide-config.ts",
    "content": "import { itemMap } from '@engine/config/config-handler';\nimport type { ItemDetails } from '@engine/config/item-config';\nimport { loadConfigurationFiles } from '@runejs/common/fs';\n\ninterface SkillGuideConfiguration {\n    id: number;\n    name: string;\n    members: boolean;\n    sub_guides: {\n        name: string;\n        lines: {\n            item: string;\n            text: string;\n            level: number;\n        }[];\n    }[];\n}\n\nexport interface SkillGuide {\n    id: number;\n    name: string;\n    members: boolean;\n    sub_guides: SkillSubGuide[];\n}\n\nexport interface SkillSubGuide {\n    name: string;\n    lines: {\n        item: ItemDetails | undefined;\n        text: string;\n        level: number;\n    }[];\n}\n\n/**\n * Loads the skill guides from the new json format.\n * @param path\n * @return SkillGuideConfiguration[]\n */\nexport async function loadSkillGuideConfigurations(path: string): Promise<SkillGuide[]> {\n    const skillGuides: SkillGuide[] = [];\n\n    const files = await loadConfigurationFiles<SkillGuideConfiguration>(path);\n    files.forEach(skillGuide => {\n        if (!skillGuide?.sub_guides) {\n            return;\n        }\n\n        const subGuides: SkillSubGuide[] = [];\n        skillGuide.sub_guides.forEach(subGuide => {\n            const subGuideLines: SkillSubGuide['lines'] = [];\n            subGuide.lines.forEach(line => {\n                subGuideLines.push({\n                    item: itemMap[line.item],\n                    text: line.text,\n                    level: line.level,\n                });\n            });\n            subGuides.push({\n                name: subGuide.name,\n                lines: subGuideLines,\n            });\n        });\n        skillGuides.push({\n            id: skillGuide.id,\n            name: skillGuide.name,\n            members: skillGuide.members,\n            sub_guides: subGuides,\n        });\n    });\n\n    return skillGuides;\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/skill-guides.plugin.ts",
    "content": "import type { ButtonActionHook, buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type { WidgetInteractionActionHook, widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\nimport type { SkillGuide, SkillSubGuide } from './skill-guide-config';\nimport { loadSkillGuideConfigurations } from './skill-guide-config';\n\nconst skillGuidePath = __dirname.replace(/dist/, 'src');\nconst sidebarTextIds = [131, 108, 109, 112, 122, 125, 128, 143, 146, 149, 159, 162, 165];\nconst sidebarIds = [129, 98, -1, 110, 113, 123, 126, 134, 144, 147, 150, 160, 163];\nlet guides: SkillGuide[] = [];\n\nfunction loadGuide(player: Player, guideId: number, subGuideId: number = 0, refreshSidebar: boolean = true): void {\n    const guide = guides.find(g => g.id === guideId);\n\n    if (!guide) {\n        logger.error(`Could not find skill guide with id ${guideId}`);\n        return;\n    }\n\n    if (refreshSidebar) {\n        player.modifyWidget(widgets.skillGuide, { childId: 133, text: guide.members ? 'Members only skill' : '' });\n\n        for (let i = 0; i < sidebarTextIds.length; i++) {\n            const sidebarId = sidebarIds[i];\n            let hidden: boolean = true;\n\n            if (i >= guide.sub_guides.length) {\n                player.modifyWidget(widgets.skillGuide, { childId: sidebarTextIds[i], text: '' });\n                hidden = true;\n            } else {\n                player.modifyWidget(widgets.skillGuide, { childId: sidebarTextIds[i], text: guide.sub_guides[i].name });\n                hidden = false;\n            }\n\n            if (sidebarId !== -1) {\n                // Apparently you can never have only TWO subguides...\n                // Because childId 98 deletes both options 2 AND 3. So, good thing there are no guides with only 2 sections, I guess?...\n                // Verified this in an interface editor, and they are indeed grouped in a single layer for some reason...\n                player.modifyWidget(widgets.skillGuide, { childId: sidebarIds[i], hidden });\n            }\n        }\n    }\n\n    const subGuide: SkillSubGuide = guide.sub_guides[subGuideId];\n\n    player.modifyWidget(widgets.skillGuide, { childId: 1, text: guide.name + ' - ' + subGuide.name });\n\n    const itemIds: number[] = subGuide.lines.map(g => g.item?.gameId || 0).concat(new Array(30 - subGuide.lines.length).fill(null));\n    player.outgoingPackets.sendUpdateAllWidgetItemsById({ widgetId: widgets.skillGuide, containerId: 132 }, itemIds);\n\n    for (let i = 0; i < 30; i++) {\n        if (subGuide.lines.length <= i) {\n            player.modifyWidget(widgets.skillGuide, { childId: 5 + i, text: '' });\n            player.modifyWidget(widgets.skillGuide, { childId: 45 + i, text: '' });\n        } else {\n            player.modifyWidget(widgets.skillGuide, { childId: 5 + i, text: subGuide.lines[i].level.toString() });\n            player.modifyWidget(widgets.skillGuide, { childId: 45 + i, text: subGuide.lines[i].text });\n        }\n    }\n\n    player.interfaceState.openWidget(widgets.skillGuide, {\n        slot: 'screen',\n        multi: false,\n    });\n    player.metadata.activeSkillGuide = guideId;\n}\n\nexport const guideHandler: buttonActionHandler = async details => {\n    const { player, buttonId } = details;\n\n    if (!guides.length) {\n        guides = await loadSkillGuideConfigurations(skillGuidePath);\n    }\n\n    loadGuide(player, buttonId);\n};\n\nexport const subGuideHandler: widgetInteractionActionHandler = async details => {\n    const { player, childId } = details;\n\n    if (!guides.length) {\n        guides = await loadSkillGuideConfigurations(skillGuidePath);\n    }\n\n    const activeSkillGuide = player.metadata.activeSkillGuide;\n\n    if (!activeSkillGuide) {\n        return;\n    }\n\n    const guide = guides.find(g => g.id === activeSkillGuide);\n\n    if (!guide) {\n        logger.error(`Could not find skill guide with id ${activeSkillGuide}`);\n        return;\n    }\n\n    const subGuideId = sidebarTextIds.indexOf(childId);\n\n    if (subGuideId >= guide.sub_guides.length) {\n        return;\n    }\n\n    loadGuide(player, guide.id, subGuideId, true);\n};\n\nexport default {\n    pluginId: 'rs:skill_guides',\n    hooks: [\n        {\n            type: 'button',\n            widgetId: widgets.skillsTab,\n            handler: guideHandler,\n        } as ButtonActionHook,\n        {\n            type: 'widget_interaction',\n            widgetIds: widgets.skillGuide,\n            optionId: 0,\n            handler: subGuideHandler,\n        } as WidgetInteractionActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/slayer.json",
    "content": "{\n    \"id\": 141,\n    \"name\": \"Slayer\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Monsters\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:crawling_hands\",\n                    \"text\": \"Crawling hands\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:cave_bugs\",\n                    \"text\": \"Cave bugs\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:cave_crawlers\",\n                    \"text\": \"Cave crawlers\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:Banshees\",\n                    \"text\": \"banshees\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:cave_slime\",\n                    \"text\": \"Cave slime\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:rockslugs\",\n                    \"text\": \"Rockslugs\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:desert_lizards\",\n                    \"text\": \"Desert lizards\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:cockatrice\",\n                    \"text\": \"Cockatrice\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:pyrefiends\",\n                    \"text\": \"Pyrefiends\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:mogres\",\n                    \"text\": \"Mogres\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:harpie_bug_swarms\",\n                    \"text\": \"Harpie bug swarms\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:wall_beasts\",\n                    \"text\": \"Wall beasts\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:killerwatts\",\n                    \"text\": \"Killerwatts\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:basilisks\",\n                    \"text\": \"Basilisks\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:fever_spiders\",\n                    \"text\": \"Fever spiders\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:infernal_mages\",\n                    \"text\": \"Infernal mages\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:brine_rats\",\n                    \"text\": \"Brine rats\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:bloodvelds\",\n                    \"text\": \"Bloodvelds\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:jellies\",\n                    \"text\": \"Jellies\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:turoth\",\n                    \"text\": \"Turoth\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:cave_horrors\",\n                    \"text\": \"Cave horrors\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:aberrant_spectres\",\n                    \"text\": \"Aberrant spectres\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:dust_devils\",\n                    \"text\": \"Dust devils\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:kurask\",\n                    \"text\": \"Kurask\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:skeletal_wyverns\",\n                    \"text\": \"Skeletal wyverns\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:gargoyles\",\n                    \"text\": \"Gargoyles\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:nechryael\",\n                    \"text\": \"Nechryael\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:abyssal demons\",\n                    \"text\": \"Abyssal demons\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:dark_beasts\",\n                    \"text\": \"Dark beasts\",\n                    \"level\": 58\n                }\n            ]\n        },\n        {\n            \"name\": \"Equipment\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:spiny_helmet\",\n                    \"text\": \"Spiney helmet\\\\n(5 Defence required)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:rock_hammer\",\n                    \"text\": \"Rock hammer & Rock Thrownhammer\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:facemask\",\n                    \"text\": \"Facemask\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:earmuffs\",\n                    \"text\": \"Earmuffs\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:mirror_shield\",\n                    \"text\": \"Mirror shield\\\\n(20 Defence required)\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:harpie_bug_lantern\",\n                    \"text\": \"Harpie bug lantern\\\\n(not a light source)\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:witchwood_icon\",\n                    \"text\": \"Witchwood icon\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:insulated_boots\",\n                    \"text\": \"Insultated boots\",\n                    \"level\": 37\n                },\n                {\n                    \"item\": \"rs:slayer_bell\",\n                    \"text\": \"Slayer bell\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:slayer_gloves\",\n                    \"text\": \"Slayer gloves\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:boots_of_stone\",\n                    \"text\": \"Boots of Stone\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:leaf_bladed_spear\",\n                    \"text\": \"Leafbladed spear\\\\n(50 Attack required)\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:broad_arrows\",\n                    \"text\": \"Broad arrows\\\\n(50 Ranged required)\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:slayer_staff\",\n                    \"text\": \"Slayer's staff\\\\n(50 Magic required)\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:fungicide_spray\",\n                    \"text\": \"Fungicide spray\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:nose_peg\",\n                    \"text\": \"Nose peg\",\n                    \"level\": 60\n                }\n            ]\n        },\n        {\n            \"name\": \"Slayer Masters\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:enchanted_gem\",\n                    \"text\": \"Burthorpe master\\\\n(Level 3 combat required)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:enchanted_gem\",\n                    \"text\": \"Canifis master\\\\n(Level 20 combat required)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:enchanted_gem\",\n                    \"text\": \"Edgeville Dungeon master\\\\n(Level 40 combat required)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:enchanted_gem\",\n                    \"text\": \"Zanaris master\\\\n(Level 70 combat required)\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:enchanted_gem\",\n                    \"text\": \"Shilo Village master\\\\n(Level 100 combat required)\",\n                    \"level\": 50\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/smithing.json",
    "content": "{\n    \"id\": 123,\n    \"name\": \"Smithing\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Smelting\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_bar\",\n                    \"text\": \"Bronze bar\\\\n1 tin ore & 1 copper ore\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:blurite_bar\",\n                    \"text\": \"Members: Blurite\\\\n(after Knight's Sword quest)\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:iron_bar\",\n                    \"text\": \"Iron bar\\\\n50% chance of success\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:silver_bar\",\n                    \"text\": \"Silver bar\\\\nSilver ore only\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:steel_bar\",\n                    \"text\": \"Steel bar\\\\n2 coal & iron ore\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:gold_bar\",\n                    \"text\": \"Gold bar\\\\nGold ore only\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:mithril_bar\",\n                    \"text\": \"Mithril bar\\\\n4 coal * 1 mithril ore\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:adamant_bar\",\n                    \"text\": \"Adamant bar\\\\n6 coal & 1 adamantite ore\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:runite_bar\",\n                    \"text\": \"Runite bar\\\\n8 coal & 1 runite ore\",\n                    \"level\": 85\n                }\n            ]\n        },\n        {\n            \"name\": \"Bronze\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_dagger\",\n                    \"text\": \"Bronze daggers\\\\nrequires 1 bar\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:bronze_axes\",\n                    \"text\": \"Bronze axes \\\\nrequires 1 bar\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:bronze_mace\",\n                    \"text\": \"Bronze maces\\\\nrequires 1 bar\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:bronze_medium_helm\",\n                    \"text\": \"Bronze medium helms\\\\nrequires 1 bar\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:bronze_crossbow_bolts\",\n                    \"text\": \"Members: Bronze crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 3\n                },\n                {\n                    \"item\": \"rs:bronze_sword\",\n                    \"text\": \"Bronze swords\\\\nrequires 1 bar\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:bronze_dart_tips\",\n                    \"text\": \"Members: Bronze dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:bronze_wire\",\n                    \"text\": \"Bronze wire\\\\nrequires 1 bar\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:bronze_nails\",\n                    \"text\": \"Bronze nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 4\n                },\n                {\n                    \"item\": \"rs:bronze_scimitars\",\n                    \"text\": \"Bronze scimitars\\\\nrequires 2 bars\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:bronze_spears\",\n                    \"text\": \"Bronze spears\\\\nrequires 1 bar\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:bronze_hastae\",\n                    \"text\": \"Bronze hastae\\\\nrequires 1 bar & 1 log\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:bronze_arrowheads\",\n                    \"text\": \"Bronze arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:bronze_crossbow_limb\",\n                    \"text\": \"Bronze crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:bronze_longsword\",\n                    \"text\": \"Bronze longswords \\\\nrequires 2 bars\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:bronze_javelin_heads\",\n                    \"text\": \"Members: Bronze javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:bronze_full_helm\",\n                    \"text\": \"Bronze full helms\\\\nrequires 2 bars\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:bronze_throwing_knives\",\n                    \"text\": \"Members: Bronze throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 7\n                },\n                {\n                    \"item\": \"rs:bronze_square_shield\",\n                    \"text\": \"Bronze square shields\\\\nrequires 2 bars\",\n                    \"level\": 8\n                },\n                {\n                    \"item\": \"rs:bronze_warhammer\",\n                    \"text\": \"Bronze warhammers\\\\nrequires 3 bars\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:bronze_battleaxe\",\n                    \"text\": \"Bronze battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:bronze_chainbody\",\n                    \"text\": \"Bronze chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:bronze_kiteshield\",\n                    \"text\": \"Bronze kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 12\n                },\n                {\n                    \"item\": \"rs:bronze_claws\",\n                    \"text\": \"Members: Bronze claws\\\\nrequires 2 bars\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:bronze_two_handed_sword\",\n                    \"text\": \"Bronze two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 14\n                },\n                {\n                    \"item\": \"rs:bronze_platelegs\",\n                    \"text\": \"Bronze platelegs\\\\nrequires 3 bars\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:bronze_skirt\",\n                    \"text\": \"Bronze plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:bronze_platebody\",\n                    \"text\": \"Bronze platebodies \\\\nrequires 5 bars\",\n                    \"level\": 18\n                }\n            ]\n        },\n        {\n            \"name\": \"Blurite\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_bolt\",\n                    \"text\": \"Bronze bolts\",\n                    \"level\": 9\n                },\n                {\n                    \"item\": \"rs:opal_bolt\",\n                    \"text\": \"Opal-tipped bronze bolts\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:blurite_bolt\",\n                    \"text\": \"Blurite bolts (After Knight´s Sword quest)\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:jade_bolt\",\n                    \"text\": \"Jade-tipped blurite bolts\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:iron_bolt\",\n                    \"text\": \"Iron bolts\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:silver_bolt\",\n                    \"text\": \"Silver bolts\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:steel_bolt\",\n                    \"text\": \"Steel bolts\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:mithril_bolt\",\n                    \"text\": \"Mithril bolts\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:sapphire_bolt\",\n                    \"text\": \"Sapphire-tipped mithril bolts\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:emerald_bolt\",\n                    \"text\": \"Emerald-tipped mithril bolts\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:adamant_bolt\",\n                    \"text\": \"Adamant bolts\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:runite_bolt\",\n                    \"text\": \"Runite bolts\",\n                    \"level\": 69\n                },\n                {\n                    \"item\": \"rs:onyx_bolt\",\n                    \"text\": \"Onyx bolts\",\n                    \"level\": 69\n                }\n            ]\n        },\n        {\n            \"name\": \"Iron\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:iron_dagger\",\n                    \"text\": \"Iron daggers\\\\nrequires 1 bar\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:iron_axes\",\n                    \"text\": \"Iron axes \\\\nrequires 1 bar\",\n                    \"level\": 16\n                },\n                {\n                    \"item\": \"rs:iron_mace\",\n                    \"text\": \"Iron maces\\\\nrequires 1 bar\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:iron_medium_helm\",\n                    \"text\": \"Iron medium helms\\\\nrequires 1 bar\",\n                    \"level\": 17\n                },\n                {\n                    \"item\": \"rs:iron_crossbow_bolts\",\n                    \"text\": \"Members: Iron crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:iron_sword\",\n                    \"text\": \"Iron swords\\\\nrequires 1 bar\",\n                    \"level\": 18\n                },\n                {\n                    \"item\": \"rs:iron_dart_tips\",\n                    \"text\": \"Members: Iron dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:iron_wire\",\n                    \"text\": \"Iron wire\\\\nrequires 1 bar\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:iron_nails\",\n                    \"text\": \"Iron nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 19\n                },\n                {\n                    \"item\": \"rs:iron_scimitars\",\n                    \"text\": \"Iron scimitars\\\\nrequires 2 bars\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:iron_spears\",\n                    \"text\": \"Iron spears\\\\nrequires 1 bar\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:iron_hastae\",\n                    \"text\": \"Iron hastae\\\\nrequires 1 bar & 1 log\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:iron_arrowheads\",\n                    \"text\": \"Iron arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:iron_crossbow_limb\",\n                    \"text\": \"Iron crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:iron_longsword\",\n                    \"text\": \"Iron longswords\\\\nrequires 2 bars\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:iron_javelin_heads\",\n                    \"text\": \"Members: Iron javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:iron_full_helm\",\n                    \"text\": \"Iron full helms\\\\nrequires 2 bars\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:iron_throwing_knives\",\n                    \"text\": \"Members: Iron throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:iron_square_shield\",\n                    \"text\": \"Iron square shields\\\\nrequires 2 bars\",\n                    \"level\": 23\n                },\n                {\n                    \"item\": \"rs:iron_warhammer\",\n                    \"text\": \"Iron warhammers\\\\nrequires 3 bars\",\n                    \"level\": 24\n                },\n                {\n                    \"item\": \"rs:iron_battleaxe\",\n                    \"text\": \"Iron battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:iron_chainbody\",\n                    \"text\": \"Iron chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 26\n                },\n                {\n                    \"item\": \"rs:iron_kiteshield\",\n                    \"text\": \"Iron kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:iron_claws\",\n                    \"text\": \"Members: Iron claws\\\\nrequires 2 bars\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:iron_two_handed_sword\",\n                    \"text\": \"Iron two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 29\n                },\n                {\n                    \"item\": \"rs:iron_platelegs\",\n                    \"text\": \"Iron platelegs\\\\nrequires 3 bars\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:iron_skirt\",\n                    \"text\": \"Iron plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:iron_platebody\",\n                    \"text\": \"Iron platebodies \\\\nrequires 5 bars\",\n                    \"level\": 33\n                }\n            ]\n        },\n        {\n            \"name\": \"Steel\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:Steel_dagger\",\n                    \"text\": \"Steel daggers\\\\nrequires 1 bar\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:Steel_axes\",\n                    \"text\": \"Steel axes \\\\nrequires 1 bar\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:Steel_mace\",\n                    \"text\": \"Steel maces\\\\nrequires 1 bar\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:Steel_medium_helm\",\n                    \"text\": \"Steel medium helms\\\\nrequires 1 bar\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:Steel_crossbow_bolts\",\n                    \"text\": \"Members: Steel crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:Steel_sword\",\n                    \"text\": \"Steel swords\\\\nrequires 1 bar\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:Steel_dart_tips\",\n                    \"text\": \"Members: Steel dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:Steel_wire\",\n                    \"text\": \"Steel wire\\\\nrequires 1 bar\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:Steel_nails\",\n                    \"text\": \"Steel nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:Steel_scimitars\",\n                    \"text\": \"Steel scimitars\\\\nrequires 2 bars\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:Steel_spears\",\n                    \"text\": \"Steel spears\\\\nrequires 1 bar\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:Steel_hastae\",\n                    \"text\": \"Steel hastae\\\\nrequires 1 bar & 1 Willow log\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:Steel_arrowheads\",\n                    \"text\": \"Steel arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:Steel_crossbow_limb\",\n                    \"text\": \"Steel crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:Steel_longsword\",\n                    \"text\": \"Steel longswords\\\\nrequires 2 bars\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:Steel_javelin_heads\",\n                    \"text\": \"Members: Steel javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:Steel_full_helm\",\n                    \"text\": \"Steel full helms\\\\nrequires 2 bars\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:Steel_throwing_knives\",\n                    \"text\": \"Members: Steel throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 33\n                },\n                {\n                    \"item\": \"rs:Steel_square_shield\",\n                    \"text\": \"Steel square shields\\\\nrequires 2 bars\",\n                    \"level\": 34\n                },\n                {\n                    \"item\": \"rs:Steel_warhammer\",\n                    \"text\": \"Steel warhammers\\\\nrequires 3 bars\",\n                    \"level\": 39\n                },\n                {\n                    \"item\": \"rs:Steel_battleaxe\",\n                    \"text\": \"Steel battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:Steel_chainbody\",\n                    \"text\": \"Steel chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:Steel_kiteshield\",\n                    \"text\": \"Steel kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:Steel_claws\",\n                    \"text\": \"Members: Steel claws\\\\nrequires 2 bars\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:Steel_two_handed_sword\",\n                    \"text\": \"Steel two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 44\n                },\n                {\n                    \"item\": \"rs:Steel_platelegs\",\n                    \"text\": \"Steel platelegs\\\\nrequires 3 bars\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:Steel_skirt\",\n                    \"text\": \"Steel plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 46\n                },\n                {\n                    \"item\": \"rs:Steel_platebody\",\n                    \"text\": \"Steel platebodies \\\\nrequires 5 bars\",\n                    \"level\": 44\n                }\n            ]\n        },\n        {\n            \"name\": \"Mithril\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:mithril_dagger\",\n                    \"text\": \"Mithril daggers\\\\nrequires 1 bar\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:mithril_axes\",\n                    \"text\": \"Mithril axes \\\\nrequires 1 bar\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:mithril_mace\",\n                    \"text\": \"Mithril maces\\\\nrequires 1 bar\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:mithril_medium_helm\",\n                    \"text\": \"Mithril medium helms\\\\nrequires 1 bar\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:mithril_crossbow_bolts\",\n                    \"text\": \"Members: Mithril crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:mithril_sword\",\n                    \"text\": \"Mithril swords\\\\nrequires 1 bar\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:mithril_dart_tips\",\n                    \"text\": \"Members: Mithril dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:mithril_wire\",\n                    \"text\": \"Mithril wire\\\\nrequires 1 bar\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:mithril_nails\",\n                    \"text\": \"Mithril nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 54\n                },\n                {\n                    \"item\": \"rs:mithril_scimitars\",\n                    \"text\": \"Mithril scimitars\\\\nrequires 2 bars\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mithril_spears\",\n                    \"text\": \"Mithril spears\\\\nrequires 1 bar\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mithril_hastae\",\n                    \"text\": \"Mithril hastae\\\\nrequires 1 bar & 1 maple log\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mithril_arrowheads\",\n                    \"text\": \"Mithril arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:mithril_crossbow_limb\",\n                    \"text\": \"Mithril crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:mithril_longsword\",\n                    \"text\": \"Mithril longswords\\\\nrequires 2 bars\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:mithril_javelin_heads\",\n                    \"text\": \"Members: Mithril javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 56\n                },\n                {\n                    \"item\": \"rs:mithril_full_helm\",\n                    \"text\": \"Mithril full helms\\\\nrequires 2 bars\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:mithril_throwing_knives\",\n                    \"text\": \"Members: Mithril throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 57\n                },\n                {\n                    \"item\": \"rs:mithril_square_shield\",\n                    \"text\": \"Mithril square shields\\\\nrequires 2 bars\",\n                    \"level\": 58\n                },\n                {\n                    \"item\": \"rs:mithril_warhammer\",\n                    \"text\": \"Mithril warhammers\\\\nrequires 3 bars\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:mithril_battleaxe\",\n                    \"text\": \"Mithril battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:mithril_chainbody\",\n                    \"text\": \"Mithril chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:mithril_kiteshield\",\n                    \"text\": \"Mithril kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 62\n                },\n                {\n                    \"item\": \"rs:mithril_claws\",\n                    \"text\": \"Members: Mithril claws\\\\nrequires 2 bars\",\n                    \"level\": 63\n                },\n                {\n                    \"item\": \"rs:mithril_two_handed_sword\",\n                    \"text\": \"Mithril two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:mithril_platelegs\",\n                    \"text\": \"Mithril platelegs\\\\nrequires 3 bars\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:mithril_skirt\",\n                    \"text\": \"Mithril plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 66\n                },\n                {\n                    \"item\": \"rs:mithril_platebody\",\n                    \"text\": \"Mithril platebodies \\\\nrequires 5 bars\",\n                    \"level\": 68\n                }\n            ]\n        },\n        {\n            \"name\": \"Adamant\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:adamant_dagger\",\n                    \"text\": \"Adamant daggers\\\\nrequires 1 bar\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:adamant_axes\",\n                    \"text\": \"Adamant axes \\\\nrequires 1 bar\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:adamant_mace\",\n                    \"text\": \"Adamant maces\\\\nrequires 1 bar\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:adamant_medium_helm\",\n                    \"text\": \"Adamant medium helms\\\\nrequires 1 bar\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:adamant_crossbow_bolts\",\n                    \"text\": \"Members: Adamant crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 73\n                },\n                {\n                    \"item\": \"rs:adamant_sword\",\n                    \"text\": \"Adamant swords\\\\nrequires 1 bar\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:adamant_dart_tips\",\n                    \"text\": \"Members: Adamant dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:adamant_wire\",\n                    \"text\": \"Adamant wire\\\\nrequires 1 bar\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:adamant_nails\",\n                    \"text\": \"Adamant nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 74\n                },\n                {\n                    \"item\": \"rs:adamant_scimitars\",\n                    \"text\": \"Adamant scimitars\\\\nrequires 2 bars\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:adamant_spears\",\n                    \"text\": \"Adamant spears\\\\nrequires 1 bar\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:adamant_hastae\",\n                    \"text\": \"Adamant hastae\\\\nrequires 1 bar & 1 yew log\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:adamant_arrowheads\",\n                    \"text\": \"Adamant arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:adamant_crossbow_limb\",\n                    \"text\": \"Adamant crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:adamant_longsword\",\n                    \"text\": \"Adamant longswords\\\\nrequires 2 bars\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:adamant_javelin_heads\",\n                    \"text\": \"Members: Adamant javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 76\n                },\n                {\n                    \"item\": \"rs:adamant_full_helm\",\n                    \"text\": \"Adamant full helms\\\\nrequires 2 bars\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:adamant_throwing_knives\",\n                    \"text\": \"Members: Adamant throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 77\n                },\n                {\n                    \"item\": \"rs:adamant_square_shield\",\n                    \"text\": \"Adamant square shields\\\\nrequires 2 bars\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:adamant_warhammer\",\n                    \"text\": \"Adamant warhammers\\\\nrequires 3 bars\",\n                    \"level\": 79\n                },\n                {\n                    \"item\": \"rs:adamant_battleaxe\",\n                    \"text\": \"Adamant battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:adamant_chainbody\",\n                    \"text\": \"Adamant chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:adamant_kiteshield\",\n                    \"text\": \"Adamant kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:adamant_claws\",\n                    \"text\": \"Members: Adamant claws\\\\nrequires 2 bars\",\n                    \"level\": 83\n                },\n                {\n                    \"item\": \"rs:adamant_two_handed_sword\",\n                    \"text\": \"Adamant two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 84\n                },\n                {\n                    \"item\": \"rs:adamant_platelegs\",\n                    \"text\": \"Adamant platelegs\\\\nrequires 3 bars\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:adamant_skirt\",\n                    \"text\": \"Adamant plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:adamant_platebody\",\n                    \"text\": \"Adamant platebodies \\\\nrequires 5 bars\",\n                    \"level\": 88\n                }\n            ]\n        },\n        {\n            \"name\": \"Rune\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:rune_dagger\",\n                    \"text\": \"Rune daggers\\\\nrequires 1 bar\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:rune_axes\",\n                    \"text\": \"Rune axes \\\\nrequires 1 bar\",\n                    \"level\": 86\n                },\n                {\n                    \"item\": \"rs:rune_mace\",\n                    \"text\": \"Rune maces\\\\nrequires 1 bar\",\n                    \"level\": 87\n                },\n                {\n                    \"item\": \"rs:rune_medium_helm\",\n                    \"text\": \"Rune medium helms\\\\nrequires 1 bar\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:rune_crossbow_bolts\",\n                    \"text\": \"Members: Rune crossbow bolts\\\\nrequires 1 bar to make 10\",\n                    \"level\": 88\n                },\n                {\n                    \"item\": \"rs:rune_sword\",\n                    \"text\": \"Rune swords\\\\nrequires 1 bar\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:rune_dart_tips\",\n                    \"text\": \"Members: Rune dart tips\\\\nrequires 1 bar to make 10\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:rune_wire\",\n                    \"text\": \"Rune wire\\\\nrequires 1 bar\",\n                    \"level\": 89\n                },\n                {\n                    \"item\": \"rs:rune_nails\",\n                    \"text\": \"Rune nails\\\\nrequires 1 bar to make 10\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:rune_scimitars\",\n                    \"text\": \"Rune scimitars\\\\nrequires 2 bars\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:rune_spears\",\n                    \"text\": \"Rune spears\\\\nrequires 1 bar\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:rune_hastae\",\n                    \"text\": \"Rune hastae\\\\nrequires 1 bar & 1 magic log\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:rune_arrowheads\",\n                    \"text\": \"Rune arrowheads\\\\nrequires 1 bar to make 15\",\n                    \"level\": 90\n                },\n                {\n                    \"item\": \"rs:rune_crossbow_limb\",\n                    \"text\": \"Rune crossbow limbs\\\\nrequires 1 bar\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:rune_longsword\",\n                    \"text\": \"Rune longswords\\\\nrequires 2 bars\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:rune_javelin_heads\",\n                    \"text\": \"Members: Rune javelin heads\\\\nrequires 1 bar to make 5\",\n                    \"level\": 91\n                },\n                {\n                    \"item\": \"rs:rune_full_helm\",\n                    \"text\": \"Rune full helms\\\\nrequires 2 bars\",\n                    \"level\": 92\n                },\n                {\n                    \"item\": \"rs:rune_throwing_knives\",\n                    \"text\": \"Members: Rune throwing knives\\\\nrequires 1 bar to make 5\",\n                    \"level\": 92\n                },\n                {\n                    \"item\": \"rs:rune_square_shield\",\n                    \"text\": \"Rune square shields\\\\nrequires 2 bars\",\n                    \"level\": 93\n                },\n                {\n                    \"item\": \"rs:rune_warhammer\",\n                    \"text\": \"Rune warhammers\\\\nrequires 3 bars\",\n                    \"level\": 94\n                },\n                {\n                    \"item\": \"rs:rune_battleaxe\",\n                    \"text\": \"Rune battleaxes\\\\nrequires 3 bars\",\n                    \"level\": 95\n                },\n                {\n                    \"item\": \"rs:rune_chainbody\",\n                    \"text\": \"Rune chainbodies\\\\nrequires 3 bars\",\n                    \"level\": 96\n                },\n                {\n                    \"item\": \"rs:rune_kiteshield\",\n                    \"text\": \"Rune kiteshields\\\\nrequires 3 bars\",\n                    \"level\": 97\n                },\n                {\n                    \"item\": \"rs:rune_claws\",\n                    \"text\": \"Members: Rune claws\\\\nrequires 2 bars\",\n                    \"level\": 98\n                },\n                {\n                    \"item\": \"rs:rune_two_handed_sword\",\n                    \"text\": \"Rune two-handed swords\\\\nrequires 3 bars\",\n                    \"level\": 99\n                },\n                {\n                    \"item\": \"rs:rune_platelegs\",\n                    \"text\": \"Rune platelegs\\\\nrequires 3 bars\",\n                    \"level\": 99\n                },\n                {\n                    \"item\": \"rs:rune_skirt\",\n                    \"text\": \"Rune plateskirts\\\\nrequires 3 bars\",\n                    \"level\": 99\n                },\n                {\n                    \"item\": \"rs:rune_platebody\",\n                    \"text\": \"Rune platebodies \\\\nrequires 5 bars\",\n                    \"level\": 99\n                }\n            ]\n        },\n        {\n            \"name\": \"Other\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:dragon_square_shield\",\n                    \"text\": \"Dragon square shield\",\n                    \"level\": 60\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/thieving.json",
    "content": "{\n    \"id\": 128,\n    \"name\": \"Attack\",\n    \"members\": true,\n    \"sub_guides\": [\n        {\n            \"name\": \"Pickpocket\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:citizen\",\n                    \"text\": \"Citizen\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:farmer\",\n                    \"text\": \"Farmer\",\n                    \"level\": 10\n                },\n                {\n                    \"item\": \"rs:female_ham_follower\",\n                    \"text\": \"Female H.A.M follower\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:male_ham_follower\",\n                    \"text\": \"Male H.A.M follower\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:warrior\",\n                    \"text\": \"Warrior\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:rogue\",\n                    \"text\": \"Rogue\",\n                    \"level\": 32\n                },\n                {\n                    \"item\": \"rs:cave_goblin\",\n                    \"text\": \"Cave goblin\",\n                    \"level\": 36\n                },\n                {\n                    \"item\": \"rs:master_farmer\",\n                    \"text\": \"Master f armer\",\n                    \"level\": 38\n                },\n                {\n                    \"item\": \"rs:guard\",\n                    \"text\": \"Guard\",\n                    \"level\": 40\n                },\n                {\n                    \"item\": \"rs:fremennik\",\n                    \"text\": \"Fremennik\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:bearded_pollnivnian_bandit\",\n                    \"text\": \"Bearded Pollnivnian bandit\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:desert_bandit\",\n                    \"text\": \"Desert bandit\",\n                    \"level\": 53\n                },\n                {\n                    \"item\": \"rs:knight\",\n                    \"text\": \"Knight\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:pollnivnian_bandit\",\n                    \"text\": \"Pollnivnian bandit\",\n                    \"level\": 55\n                },\n                {\n                    \"item\": \"rs:watchman\",\n                    \"text\": \"Watchman\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:menaphite_thug\",\n                    \"text\": \"Menaphite thug\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:paladin\",\n                    \"text\": \"Paladin\",\n                    \"level\": 70\n                },\n                {\n                    \"item\": \"rs:gnome\",\n                    \"text\": \"Gnome\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:hero\",\n                    \"text\": \"Hero\",\n                    \"level\": 80\n                },\n                {\n                    \"item\": \"rs:vyre\",\n                    \"text\": \"Vyre\",\n                    \"level\": 82\n                },\n                {\n                    \"item\": \"rs:elf\",\n                    \"text\": \"Elf\",\n                    \"level\": 85\n                },\n                {\n                    \"item\": \"rs:tzhaar\",\n                    \"text\": \"TzHaar\",\n                    \"level\": 90\n                }\n            ]\n        },\n        {\n            \"name\": \"Stalls\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:vegetable_stall\",\n                    \"text\": \"Vegetable stall\",\n                    \"level\": 2\n                },\n                {\n                    \"item\": \"rs:Cake_stall\",\n                    \"text\": \"Cake stall\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:tea_stall\",\n                    \"text\": \"Tea stall\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:crafting_stall\",\n                    \"text\": \"Crafting stall\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:monkey_food_stall\",\n                    \"text\": \"Monkey food stall\",\n                    \"level\": 5\n                },\n                {\n                    \"item\": \"rs:silk_stall\",\n                    \"text\": \"Silk stall\",\n                    \"level\": 20\n                },\n                {\n                    \"item\": \"rs:wine_stall\",\n                    \"text\": \"Wine stall\",\n                    \"level\": 22\n                },\n                {\n                    \"item\": \"rs:fruit_stall\",\n                    \"text\": \"Fruit stall\",\n                    \"level\": 25\n                },\n                {\n                    \"item\": \"rs:seed_stall\",\n                    \"text\": \"Seed stall\",\n                    \"level\": 27\n                },\n                {\n                    \"item\": \"rs:fur_stall\",\n                    \"text\": \"Fur stall\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:fish_stall\",\n                    \"text\": \"Fish stall\",\n                    \"level\": 42\n                },\n                {\n                    \"item\": \"rs:crossbow_stall\",\n                    \"text\": \"Crossbow stall\",\n                    \"level\": 49\n                },\n                {\n                    \"item\": \"rs:silver_stall\",\n                    \"text\": \"Silver stall\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:magic_stall\",\n                    \"text\": \"Magic stall\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:scimitar_stall\",\n                    \"text\": \"Scimitar stall\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:spices_stall\",\n                    \"text\": \"Spices stall\",\n                    \"level\": 65\n                },\n                {\n                    \"item\": \"rs:gems_stall\",\n                    \"text\": \"Gems stall\",\n                    \"level\": 75\n                },\n                {\n                    \"item\": \"rs:ore_stall\",\n                    \"text\": \"Ore stall\",\n                    \"level\": 82\n                }\n            ]\n        },\n        {\n            \"name\": \"Chests\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:ardougne_relekka_wilderness\",\n                    \"text\": \"Ardougne, Rellekka and the Wilderness\",\n                    \"level\": 13\n                },\n                {\n                    \"item\": \"rs:upstairs_ardougne_rellekka\",\n                    \"text\": \"Upstairs in Ardougne and Rellekka\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:the_isles_of_souls\",\n                    \"text\": \"The Isle of Souls\",\n                    \"level\": 28\n                },\n                {\n                    \"item\": \"rs:upstairs_ardougne\",\n                    \"text\": \"Upstairs in Ardougne\",\n                    \"level\": 43\n                },\n                {\n                    \"item\": \"rs:hemenster_rellekka\",\n                    \"text\": \"Hemenster and Rellekka\",\n                    \"level\": 47\n                },\n                {\n                    \"item\": \"rs:dorgesh_kaan_average_chest\",\n                    \"text\": \"Dorgesh-Kaan (average chests)\",\n                    \"level\": 52\n                },\n                {\n                    \"item\": \"rs:chaos_druid_tower_north_ardougne\",\n                    \"text\": \"Chaos druid tower north of Ardougne\",\n                    \"level\": 59\n                },\n                {\n                    \"item\": \"rs:lizardman_temple_molch\",\n                    \"text\": \"Lizardman Temple beneath Molch\",\n                    \"level\": 64\n                },\n                {\n                    \"item\": \"rs:ardougne_castle\",\n                    \"text\": \"Ardougne Castle\",\n                    \"level\": 72\n                },\n                {\n                    \"item\": \"rs:dorgesh_kaan_rich\",\n                    \"text\": \"Dorgesh-Kaan (rich chests)\",\n                    \"level\": 78\n                },\n                {\n                    \"item\": \"rs:wilderness_rogues_castle\",\n                    \"text\": \"Wilderness Rogue's Castle\",\n                    \"level\": 84\n                }\n            ]\n        },\n        {\n            \"name\": \"Other\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:pyramid_plunder_room_1\",\n                    \"text\": \"Pyramid Plunder minigame - Room 1\\\\nIn the Jalsavrah Pyramid in Sophanem\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_2\",\n                    \"text\": \"Pyramid Plunder - Room 2\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_3\",\n                    \"text\": \"Pyramid Plunder - Room 3\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:uncut_sapphire\",\n                    \"text\": \"Can crack the wall safes in Rogues'Den\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_4\",\n                    \"text\": \"Pyramid Plunder - Room 4\",\n                    \"level\": 51\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_5\",\n                    \"text\": \"Pyramid Plunder - Room 5\",\n                    \"level\": 61\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_6\",\n                    \"text\": \"Pyramid Plunder - 6\",\n                    \"level\": 71\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_7\",\n                    \"text\": \"Pyramid Plunder - 7)\",\n                    \"level\": 81\n                },\n                {\n                    \"item\": \"rs:pyramid_plunder_room_8\",\n                    \"text\": \"Pyramid Plunder - 8\",\n                    \"level\": 91\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/skill-guides/woodcutting.json",
    "content": "{\n    \"id\": 139,\n    \"name\": \"Woodcutting\",\n    \"members\": false,\n    \"sub_guides\": [\n        {\n            \"name\": \"Trees\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:logs\",\n                    \"text\": \"Normal trees\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:achey_logs\",\n                    \"text\": \"Achey trees\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:oak_logs\",\n                    \"text\": \"Oak trees\",\n                    \"level\": 15\n                },\n                {\n                    \"item\": \"rs:willow_logs\",\n                    \"text\": \"Willow trees\",\n                    \"level\": 30\n                },\n                {\n                    \"item\": \"rs:teak_logs\",\n                    \"text\": \"Teak trees\",\n                    \"level\": 35\n                },\n                {\n                    \"item\": \"rs:maple_logs\",\n                    \"text\": \"Maple trees\",\n                    \"level\": 45\n                },\n                {\n                    \"item\": \"rs:mahogany_logs\",\n                    \"text\": \"Mahogany trees\",\n                    \"level\": 50\n                },\n                {\n                    \"item\": \"rs:yew_logs\",\n                    \"text\": \"Yew trees\",\n                    \"level\": 60\n                },\n                {\n                    \"item\": \"rs:magic_logs\",\n                    \"text\": \"Magic trees\",\n                    \"level\": 75\n                }\n            ]\n        },\n        {\n            \"name\": \"Axes\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:bronze_axe\",\n                    \"text\": \"Bronze axe\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:iron_axe\",\n                    \"text\": \"Iron axe\",\n                    \"level\": 1\n                },\n                {\n                    \"item\": \"rs:steel_axe\",\n                    \"text\": \"Steel axe\",\n                    \"level\": 6\n                },\n                {\n                    \"item\": \"rs:black_axe\",\n                    \"text\": \"Black axe\",\n                    \"level\": 11\n                },\n                {\n                    \"item\": \"rs:mithril_axe\",\n                    \"text\": \"Mithril axe\",\n                    \"level\": 21\n                },\n                {\n                    \"item\": \"rs:adamant_axe\",\n                    \"text\": \"Adamant axe\",\n                    \"level\": 31\n                },\n                {\n                    \"item\": \"rs:rune_axe\",\n                    \"text\": \"Rune axe\",\n                    \"level\": 41\n                },\n                {\n                    \"item\": \"rs:dragon_axe\",\n                    \"text\": \"Dragon axe\",\n                    \"level\": 61\n                }\n            ]\n        },\n        {\n            \"name\": \"Other\",\n            \"lines\": [\n                {\n                    \"item\": \"rs:dwarf_remains\",\n                    \"text\": \"Missing content\",\n                    \"level\": 1\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "src/plugins/skills/smithing/forging-constants.ts",
    "content": "import { itemIds } from '@engine/world/config/item-ids';\nimport type { Smithable } from '@plugins/skills/smithing/forging-types';\n\nexport const anvilIds: number[] = [2782, 2783, 4306, 6150];\n\n/**\n * Map bars and levels.\n */\nexport const bars: Map<number, number> = new Map<number, number>([\n    [itemIds.bars.bronze, 1],\n    [itemIds.bars.iron, 15],\n    [itemIds.bars.steel, 30],\n    [itemIds.bars.mithril, 50],\n    [itemIds.bars.adamantite, 70],\n    [itemIds.bars.runite, 85],\n]);\n\nexport const smithables: Map<string, Map<string, Smithable>> = new Map<string, Map<string, Smithable>>([\n    [\n        'dagger',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 1,\n                    experience: 12.5,\n                    item: { itemId: itemIds.daggers.bronze, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 15,\n                    experience: 25,\n                    item: { itemId: itemIds.daggers.iron, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 30,\n                    experience: 37.5,\n                    item: { itemId: itemIds.daggers.steel, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 50,\n                    experience: 50,\n                    item: { itemId: itemIds.daggers.mithril, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 70,\n                    experience: 62.5,\n                    item: { itemId: itemIds.daggers.adamant, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 85,\n                    experience: 75,\n                    item: { itemId: itemIds.daggers.rune, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'axe',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 1,\n                    experience: 12.5,\n                    item: { itemId: itemIds.axes.bronze, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 16,\n                    experience: 25,\n                    item: { itemId: itemIds.axes.iron, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 31,\n                    experience: 37.5,\n                    item: { itemId: itemIds.axes.steel, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 51,\n                    experience: 50,\n                    item: { itemId: itemIds.axes.mithril, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 71,\n                    experience: 62.5,\n                    item: { itemId: itemIds.axes.adamantite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 86,\n                    experience: 75,\n                    item: { itemId: itemIds.axes.runite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'mace',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 2,\n                    experience: 12.5,\n                    item: { itemId: itemIds.maces.bronze, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 17,\n                    experience: 25,\n                    item: { itemId: itemIds.maces.iron, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 32,\n                    experience: 37.5,\n                    item: { itemId: itemIds.maces.steel, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 52,\n                    experience: 50,\n                    item: { itemId: itemIds.maces.mithril, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 72,\n                    experience: 62.5,\n                    item: { itemId: itemIds.maces.adamantite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 87,\n                    experience: 75,\n                    item: { itemId: itemIds.maces.runite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'mediumHelm',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 3,\n                    experience: 12.5,\n                    item: { itemId: itemIds.mediumHelmets.bronze, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 18,\n                    experience: 25,\n                    item: { itemId: itemIds.mediumHelmets.iron, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 33,\n                    experience: 37.5,\n                    item: { itemId: itemIds.mediumHelmets.steel, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 53,\n                    experience: 50,\n                    item: { itemId: itemIds.mediumHelmets.mithril, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 73,\n                    experience: 62.5,\n                    item: { itemId: itemIds.mediumHelmets.adamantite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 88,\n                    experience: 75,\n                    item: { itemId: itemIds.mediumHelmets.runite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'bolts',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 3,\n                    experience: 12.5,\n                    item: { itemId: itemIds.bolts.bronze, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 18,\n                    experience: 25,\n                    item: { itemId: itemIds.bolts.iron, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 33,\n                    experience: 37.5,\n                    item: { itemId: itemIds.bolts.steel, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 53,\n                    experience: 50,\n                    item: { itemId: itemIds.bolts.mithril, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 73,\n                    experience: 62.5,\n                    item: { itemId: itemIds.bolts.adamantite, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 88,\n                    experience: 75,\n                    item: { itemId: itemIds.bolts.runite, amount: 15 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'sword',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 4,\n                    experience: 12.5,\n                    item: { itemId: itemIds.swords.bronze, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.bronze, amount: 1 },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 19,\n                    experience: 25,\n                    item: { itemId: itemIds.swords.iron, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.iron, amount: 1 },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 34,\n                    experience: 37.5,\n                    item: { itemId: itemIds.swords.steel, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.steel, amount: 1 },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 54,\n                    experience: 50,\n                    item: { itemId: itemIds.swords.mithril, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.mithril, amount: 1 },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 74,\n                    experience: 62.5,\n                    item: { itemId: itemIds.swords.adamantite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.adamantite, amount: 1 },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 89,\n                    experience: 75,\n                    item: { itemId: itemIds.swords.runite, amount: 1 },\n                    ingredient: { itemId: itemIds.bars.runite, amount: 1 },\n                },\n            ],\n        ]),\n    ],\n    [\n        'dartTips',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 4,\n                    experience: 12.5,\n                    item: {\n                        itemId: itemIds.dartTips.bronze,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 19,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.dartTips.iron,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 34,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.dartTips.steel,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 54,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.dartTips.mithril,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 74,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.dartTips.adamantite,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 89,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.dartTips.runite,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'nails',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 4,\n                    experience: 12.5,\n                    item: {\n                        itemId: itemIds.nails.bronze,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 19,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.nails.iron,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 34,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.nails.steel,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 54,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.nails.mithril,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 74,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.nails.adamantite,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 89,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.nails.runite,\n                        amount: 10,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'scimitar',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 5,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.scimitars.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 20,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.scimitars.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 35,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.scimitars.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 55,\n                    experience: 100,\n                    item: {\n                        itemId: itemIds.scimitars.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 75,\n                    experience: 125,\n                    item: {\n                        itemId: itemIds.scimitars.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 90,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.scimitars.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 2,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'spear',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 5,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.spears.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 20,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.spears.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 35,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.spears.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 55,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.spears.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 75,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.spears.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 90,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.spears.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'arrowTips',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 5,\n                    experience: 12.5,\n                    item: {\n                        itemId: itemIds.arrowTips.bronze,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 20,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.arrowTips.iron,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 35,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.arrowTips.steel,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 55,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.arrowTips.mithril,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 75,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.arrowTips.adamantite,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 90,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.arrowTips.runite,\n                        amount: 15,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'limbs',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 6,\n                    experience: 12.5,\n                    item: {\n                        itemId: itemIds.limbs.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 23,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.limbs.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 36,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.limbs.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 56,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.limbs.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 76,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.limbs.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 91,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.limbs.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'longsword',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 6,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.longswords.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 21,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.longswords.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 36,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.longswords.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 56,\n                    experience: 100,\n                    item: {\n                        itemId: itemIds.longswords.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 76,\n                    experience: 125,\n                    item: {\n                        itemId: itemIds.longswords.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 91,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.longswords.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 2,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'fullHelm',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 7,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.fullHelmets.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 22,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.fullHelmets.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 37,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.fullHelmets.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 57,\n                    experience: 100,\n                    item: {\n                        itemId: itemIds.fullHelmets.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 77,\n                    experience: 125,\n                    item: {\n                        itemId: itemIds.fullHelmets.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 92,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.fullHelmets.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 2,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'knife',\n        new Map<string, Smithable>([\n            [\n                'steel',\n                {\n                    level: 37,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.throwingKnives.steel,\n                        amount: 5,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 57,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.throwingKnives.mithril,\n                        amount: 5,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 77,\n                    experience: 62.5,\n                    item: {\n                        itemId: itemIds.throwingKnives.adamantite,\n                        amount: 5,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 1,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 92,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.throwingKnives.runite,\n                        amount: 5,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'squareShield',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 8,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.squareShields.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 23,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.squareShields.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 38,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.squareShields.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 58,\n                    experience: 100,\n                    item: {\n                        itemId: itemIds.squareShields.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 78,\n                    experience: 125,\n                    item: {\n                        itemId: itemIds.squareShields.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 93,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.squareShields.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 2,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'warhammer',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 9,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.warhammers.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 24,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.warhammers.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 39,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.warhammers.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 59,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.warhammers.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 79,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.warhammers.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 94,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.warhammers.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'battleaxe',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 9,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.battleAxes.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 24,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.battleAxes.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 39,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.battleAxes.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 59,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.battleAxes.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 79,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.battleAxes.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 94,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.battleAxes.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'chainbody',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 11,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.chainbodies.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 26,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.chainbodies.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 41,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.chainbodies.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 61,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.chainbodies.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 81,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.chainbodies.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 96,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.chainbodies.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'kiteshield',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 12,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.kiteshields.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 27,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.kiteshields.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 42,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.kiteshields.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 62,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.kiteshields.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 82,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.kiteshields.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 97,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.kiteshields.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'claws',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 13,\n                    experience: 25,\n                    item: {\n                        itemId: itemIds.claws.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 28,\n                    experience: 50,\n                    item: {\n                        itemId: itemIds.claws.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 43,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.claws.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 63,\n                    experience: 100,\n                    item: {\n                        itemId: itemIds.claws.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 83,\n                    experience: 125,\n                    item: {\n                        itemId: itemIds.claws.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 2,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 98,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.claws.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 2,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'twoHandedSword',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 14,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.twoHandSwords.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 29,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.twoHandSwords.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 44,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.twoHandSwords.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 64,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.twoHandSwords.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 84,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.twoHandSwords.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 99,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.twoHandSwords.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'platelegs',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 16,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.platelegs.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 31,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.platelegs.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 46,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.platelegs.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 66,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.platelegs.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 86,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.platelegs.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 99,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.platelegs.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'plateskirt',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 16,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.plateskirts.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 31,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.plateskirts.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 46,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.plateskirts.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 66,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.plateskirts.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 86,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.plateskirts.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 3,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 99,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.plateskirts.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 3,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'platebody',\n        new Map<string, Smithable>([\n            [\n                'bronze',\n                {\n                    level: 16,\n                    experience: 37.5,\n                    item: {\n                        itemId: itemIds.platebodys.bronze,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 5,\n                    },\n                },\n            ],\n            [\n                'iron',\n                {\n                    level: 31,\n                    experience: 75,\n                    item: {\n                        itemId: itemIds.platebodys.iron,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.iron,\n                        amount: 5,\n                    },\n                },\n            ],\n            [\n                'steel',\n                {\n                    level: 46,\n                    experience: 112.5,\n                    item: {\n                        itemId: itemIds.platebodys.steel,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.steel,\n                        amount: 5,\n                    },\n                },\n            ],\n            [\n                'mithril',\n                {\n                    level: 66,\n                    experience: 150,\n                    item: {\n                        itemId: itemIds.platebodys.mithril,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.mithril,\n                        amount: 5,\n                    },\n                },\n            ],\n            [\n                'adamant',\n                {\n                    level: 86,\n                    experience: 187.5,\n                    item: {\n                        itemId: itemIds.platebodys.adamantite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.adamantite,\n                        amount: 5,\n                    },\n                },\n            ],\n            [\n                'rune',\n                {\n                    level: 99,\n                    experience: 225,\n                    item: {\n                        itemId: itemIds.platebodys.runite,\n                        amount: 1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.runite,\n                        amount: 5,\n                    },\n                },\n            ],\n        ]),\n    ],\n    [\n        'unknown',\n        new Map<string, Smithable>([\n            [\n                'any',\n                {\n                    level: 1,\n                    experience: 0,\n                    item: {\n                        itemId: -1,\n                        amount: -1,\n                    },\n                    ingredient: {\n                        itemId: itemIds.bars.bronze,\n                        amount: 1,\n                    },\n                },\n            ],\n        ]),\n    ],\n]);\n\n/**\n * TODO (Jameskmonger) I ran a find-and-replace over this to stop TypeScript errors, recommend refactoring\n */\nexport const widgetItems: Map<number, Map<number, Smithable[]>> = new Map<number, Map<number, Smithable[]>>([\n    [\n        itemIds.bars.bronze,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('bronze') as Smithable,\n                    smithables.get('sword')?.get('bronze') as Smithable,\n                    smithables.get('scimitar')?.get('bronze') as Smithable,\n                    smithables.get('longsword')?.get('bronze') as Smithable,\n                    smithables.get('twoHandedSword')?.get('bronze') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('bronze') as Smithable,\n                    smithables.get('mace')?.get('bronze') as Smithable,\n                    smithables.get('warhammer')?.get('bronze') as Smithable,\n                    smithables.get('battleaxe')?.get('bronze') as Smithable,\n                    smithables.get('claws')?.get('bronze') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('bronze') as Smithable,\n                    smithables.get('platelegs')?.get('bronze') as Smithable,\n                    smithables.get('plateskirt')?.get('bronze') as Smithable,\n                    smithables.get('platebody')?.get('bronze') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('bronze') as Smithable,\n                    smithables.get('fullHelm')?.get('bronze') as Smithable,\n                    smithables.get('squareShield')?.get('bronze') as Smithable,\n                    smithables.get('kiteshield')?.get('bronze') as Smithable,\n                    smithables.get('nails')?.get('bronze') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('bronze') as Smithable,\n                    smithables.get('arrowTips')?.get('bronze') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('bronze') as Smithable,\n                    smithables.get('limbs')?.get('bronze') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n    [\n        itemIds.bars.iron,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('iron') as Smithable,\n                    smithables.get('sword')?.get('iron') as Smithable,\n                    smithables.get('scimitar')?.get('iron') as Smithable,\n                    smithables.get('longsword')?.get('iron') as Smithable,\n                    smithables.get('twoHandedSword')?.get('iron') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('iron') as Smithable,\n                    smithables.get('mace')?.get('iron') as Smithable,\n                    smithables.get('warhammer')?.get('iron') as Smithable,\n                    smithables.get('battleaxe')?.get('iron') as Smithable,\n                    smithables.get('claws')?.get('iron') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('iron') as Smithable,\n                    smithables.get('platelegs')?.get('iron') as Smithable,\n                    smithables.get('plateskirt')?.get('iron') as Smithable,\n                    smithables.get('platebody')?.get('iron') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('iron') as Smithable,\n                    smithables.get('fullHelm')?.get('iron') as Smithable,\n                    smithables.get('squareShield')?.get('iron') as Smithable,\n                    smithables.get('kiteshield')?.get('iron') as Smithable,\n                    smithables.get('nails')?.get('iron') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('iron') as Smithable,\n                    smithables.get('arrowTips')?.get('iron') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('iron') as Smithable,\n                    smithables.get('limbs')?.get('iron') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n    [\n        itemIds.bars.steel,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('steel') as Smithable,\n                    smithables.get('sword')?.get('steel') as Smithable,\n                    smithables.get('scimitar')?.get('steel') as Smithable,\n                    smithables.get('longsword')?.get('steel') as Smithable,\n                    smithables.get('twoHandedSword')?.get('steel') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('steel') as Smithable,\n                    smithables.get('mace')?.get('steel') as Smithable,\n                    smithables.get('warhammer')?.get('steel') as Smithable,\n                    smithables.get('battleaxe')?.get('steel') as Smithable,\n                    smithables.get('claws')?.get('steel') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('steel') as Smithable,\n                    smithables.get('platelegs')?.get('steel') as Smithable,\n                    smithables.get('plateskirt')?.get('steel') as Smithable,\n                    smithables.get('platebody')?.get('steel') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('steel') as Smithable,\n                    smithables.get('fullHelm')?.get('steel') as Smithable,\n                    smithables.get('squareShield')?.get('steel') as Smithable,\n                    smithables.get('kiteshield')?.get('steel') as Smithable,\n                    smithables.get('nails')?.get('steel') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('steel') as Smithable,\n                    smithables.get('arrowTips')?.get('steel') as Smithable,\n                    smithables.get('knife')?.get('steel') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('steel') as Smithable,\n                    smithables.get('limbs')?.get('steel') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n    [\n        itemIds.bars.mithril,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('mithril') as Smithable,\n                    smithables.get('sword')?.get('mithril') as Smithable,\n                    smithables.get('scimitar')?.get('mithril') as Smithable,\n                    smithables.get('longsword')?.get('mithril') as Smithable,\n                    smithables.get('twoHandedSword')?.get('mithril') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('mithril') as Smithable,\n                    smithables.get('mace')?.get('mithril') as Smithable,\n                    smithables.get('warhammer')?.get('mithril') as Smithable,\n                    smithables.get('battleaxe')?.get('mithril') as Smithable,\n                    smithables.get('claws')?.get('mithril') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('mithril') as Smithable,\n                    smithables.get('platelegs')?.get('mithril') as Smithable,\n                    smithables.get('plateskirt')?.get('mithril') as Smithable,\n                    smithables.get('platebody')?.get('mithril') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('mithril') as Smithable,\n                    smithables.get('fullHelm')?.get('mithril') as Smithable,\n                    smithables.get('squareShield')?.get('mithril') as Smithable,\n                    smithables.get('kiteshield')?.get('mithril') as Smithable,\n                    smithables.get('nails')?.get('mithril') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('mithril') as Smithable,\n                    smithables.get('arrowTips')?.get('mithril') as Smithable,\n                    smithables.get('knife')?.get('mithril') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('mithril') as Smithable,\n                    smithables.get('limbs')?.get('mithril') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n    [\n        itemIds.bars.adamantite,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('adamant') as Smithable,\n                    smithables.get('sword')?.get('adamant') as Smithable,\n                    smithables.get('scimitar')?.get('adamant') as Smithable,\n                    smithables.get('longsword')?.get('adamant') as Smithable,\n                    smithables.get('twoHandedSword')?.get('adamant') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('adamant') as Smithable,\n                    smithables.get('mace')?.get('adamant') as Smithable,\n                    smithables.get('warhammer')?.get('adamant') as Smithable,\n                    smithables.get('battleaxe')?.get('adamant') as Smithable,\n                    smithables.get('claws')?.get('adamant') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('adamant') as Smithable,\n                    smithables.get('platelegs')?.get('adamant') as Smithable,\n                    smithables.get('plateskirt')?.get('adamant') as Smithable,\n                    smithables.get('platebody')?.get('adamant') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('adamant') as Smithable,\n                    smithables.get('fullHelm')?.get('adamant') as Smithable,\n                    smithables.get('squareShield')?.get('adamant') as Smithable,\n                    smithables.get('kiteshield')?.get('adamant') as Smithable,\n                    smithables.get('nails')?.get('adamant') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('adamant') as Smithable,\n                    smithables.get('arrowTips')?.get('adamant') as Smithable,\n                    smithables.get('knife')?.get('adamant') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('adamant') as Smithable,\n                    smithables.get('limbs')?.get('adamant') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n    [\n        itemIds.bars.runite,\n        new Map<number, Smithable[]>([\n            [\n                146,\n                [\n                    // Dagger, Sword, Scimitar, Longsword, 2h sword\n                    smithables.get('dagger')?.get('rune') as Smithable,\n                    smithables.get('sword')?.get('rune') as Smithable,\n                    smithables.get('scimitar')?.get('rune') as Smithable,\n                    smithables.get('longsword')?.get('rune') as Smithable,\n                    smithables.get('twoHandedSword')?.get('rune') as Smithable,\n                ],\n            ],\n            [\n                147,\n                [\n                    // Axe, Mace, Warhammer, Battleaxe, Claws\n                    smithables.get('axe')?.get('rune') as Smithable,\n                    smithables.get('mace')?.get('rune') as Smithable,\n                    smithables.get('warhammer')?.get('rune') as Smithable,\n                    smithables.get('battleaxe')?.get('rune') as Smithable,\n                    smithables.get('claws')?.get('rune') as Smithable,\n                ],\n            ],\n            [\n                148,\n                [\n                    // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern*\n                    smithables.get('chainbody')?.get('rune') as Smithable,\n                    smithables.get('platelegs')?.get('rune') as Smithable,\n                    smithables.get('plateskirt')?.get('rune') as Smithable,\n                    smithables.get('platebody')?.get('rune') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                149,\n                [\n                    // Medium helm, Full helm, Sq shield, kite shield, Nails\n                    smithables.get('mediumHelm')?.get('rune') as Smithable,\n                    smithables.get('fullHelm')?.get('rune') as Smithable,\n                    smithables.get('squareShield')?.get('rune') as Smithable,\n                    smithables.get('kiteshield')?.get('rune') as Smithable,\n                    smithables.get('nails')?.get('rune') as Smithable,\n                ],\n            ],\n            [\n                150,\n                [\n                    // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs*\n                    smithables.get('dartTips')?.get('rune') as Smithable,\n                    smithables.get('arrowTips')?.get('rune') as Smithable,\n                    smithables.get('knife')?.get('rune') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n            [\n                151,\n                [\n                    // Bolts, Limbs, Grapple tips\n                    smithables.get('bolts')?.get('rune') as Smithable,\n                    smithables.get('limbs')?.get('rune') as Smithable,\n                    smithables.get('unknown')?.get('any') as Smithable,\n                ],\n            ],\n        ]),\n    ],\n]);\n"
  },
  {
    "path": "src/plugins/skills/smithing/forging-task.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport { ActorTask } from '@engine/task/impl/actor-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport type { Smithable } from './forging-types';\n\n/**\n * A task that handles the forging of an item.\n *\n * Operates repeatedly every 4 ticks, and stops when the player has forged the amount they wanted.\n *\n * @author jameskmonger\n */\nexport class ForgingTask extends ActorTask<Player> {\n    private elapsedTicks = 0;\n    private amountForged = 0;\n\n    constructor(\n        player: Player,\n        private readonly smithable: Smithable,\n        private readonly amount: number,\n    ) {\n        super(player);\n    }\n\n    public execute(): void {\n        const taskIteration = this.elapsedTicks++;\n\n        // completed the task if we've forged the amount we wanted\n        if (this.amountForged >= this.amount) {\n            this.stop();\n            return;\n        }\n\n        // TODO (Jameskmonger) remove magic number\n        this.actor.playAnimation(898);\n\n        // only do something every 4 ticks\n        if (taskIteration % 4 !== 0) {\n            return;\n        }\n\n        // can't continue\n        if (!this.hasMaterials()) {\n            this.stop();\n            // TODO (Jameskmonger) send message\n            return;\n        }\n\n        // Remove ingredients\n        for (let i = 0; i < this.smithable.ingredient.amount; i++) {\n            this.actor.inventory.removeFirst(this.smithable.ingredient.itemId);\n        }\n\n        // Add item to inventory\n        this.actor.inventory.add({\n            itemId: this.smithable.item.itemId,\n            amount: this.smithable.item.amount,\n        });\n\n        this.actor.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.actor.inventory);\n        this.actor.skills.addExp(Skill.SMITHING, this.smithable.experience);\n\n        this.amountForged++;\n    }\n\n    /**\n     * Whether the player has the required materials to forge the item.\n     * @returns {boolean} True if the player has the required materials, false otherwise.\n     */\n    private hasMaterials() {\n        return this.smithable.ingredient.amount <= this.actor.inventory.findAll(this.smithable.ingredient.itemId).length;\n    }\n}\n"
  },
  {
    "path": "src/plugins/skills/smithing/forging-types.ts",
    "content": "import type { Item } from '@engine/world/items/item';\nexport interface Smithable {\n    item: Item;\n    level: number;\n    experience: number;\n    ingredient: Item;\n}\n"
  },
  {
    "path": "src/plugins/skills/smithing/forging.plugin.ts",
    "content": "import type { ItemInteractionActionHook } from '@engine/action/pipe/item-interaction.action';\nimport type { ItemOnObjectActionHook, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { findItem } from '@engine/config/config-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport { Position } from '@engine/world/position';\nimport { anvilIds, bars, smithables, widgetItems } from '@plugins/skills/smithing/forging-constants';\nimport type { Smithable } from '@plugins/skills/smithing/forging-types';\nimport { logger } from '@runejs/common';\nimport { ForgingTask } from './forging-task';\n\n/**\n * Get the item ids of all the smithable items, as a flat array.\n *\n * @param input A two-dimensional map of smithables, keyed by type and then by item id.\n *              e.g. smithables.get('dagger').get('bronze')\n * @returns A flat array of item ids, e.g. [ bronze_dagger_id, iron_dagger_id, ...]\n * @remarks This is used to check if the player has the correct item in their inventory.\n */\nconst mapSmithableItemIdsToFlatArray = (input: Map<string, Map<string, Smithable>>) => {\n    const result: number[] = [];\n    input.forEach(type => {\n        type.forEach(smithable => {\n            result.push(smithable.item.itemId);\n        });\n    });\n    return result;\n};\n\n/**\n * Flatten a two-dimensional map of Smithables into an array.\n *\n * TODO (Jameskmonger): this should not be done at runtime! At startup would be one thing,\n *                  but this happens in the `canActivate` method.\n *\n * @param input A two-dimensional map of smithables, keyed by type and then by item id.\n *              e.g. smithables.get('dagger').get('bronze')\n * @returns A flat array of item ids, e.g. [ bronze_dagger_id, iron_dagger_id, ...]\n * @remarks This is used to check if the player has the correct item in their inventory.\n */\nconst mapSmithablesToFlatArray = (input: Map<string, Map<string, Smithable>>) => {\n    const results: Smithable[] = [];\n    input.forEach(values => {\n        values.forEach(value => {\n            results.push(value);\n        });\n    });\n    return results;\n};\n\n/**\n * Lookup a smithable from just an item id.\n * @param itemId\n */\nconst findSmithableByItemId = (itemId: number): Smithable | null => {\n    return (\n        mapSmithablesToFlatArray(smithables).find(smithable => {\n            return smithable.item.itemId === itemId;\n        }) || null\n    );\n};\n\n/**\n * Check if the player is able to forge an item.\n */\nconst canForge = (player: Player, smithable: Smithable): boolean => {\n    // In case the smithable doesn't exist.\n    if (!smithable) {\n        return false;\n    }\n\n    // Check if the player has the level required.\n    if (smithable.level > player.skills.getLevel(Skill.SMITHING)) {\n        const item = findItem(smithable.item.itemId);\n\n        if (!item) {\n            logger.error(`Could not find smithable item with id ${smithable.item.itemId}`);\n            return false;\n        }\n\n        player.sendMessage(`You have to be at least level ${smithable.level} to smith ${item.name}s.`, true);\n        return false;\n    }\n\n    // Check if the player has sufficient materials.\n    if (!hasMaterials(player, smithable)) {\n        const ingredient = findItem(smithable.ingredient.itemId);\n\n        if (!ingredient) {\n            logger.error(`Could not find smithable ingredient with id ${smithable.ingredient.itemId}`);\n            return false;\n        }\n\n        player.sendMessage(`You don't have enough ${ingredient.name}s.`, true);\n        return false;\n    }\n\n    player.interfaceState.closeAllSlots();\n\n    return true;\n};\n\n/**\n * Checks if the player has enough materials\n * @param player\n * @param smithable\n */\nconst hasMaterials = (player: Player, smithable: Smithable) => {\n    return smithable.ingredient.amount <= player.inventory.findAll(smithable.ingredient.itemId).length;\n};\n\n/**\n * Opens the forging interface, and loads the items.\n * @param details\n */\nconst openForgingInterface: itemOnObjectActionHandler = details => {\n    const { player, item, object } = details;\n    const amountInInventory = player.inventory.findAll(item).length;\n\n    player.face(new Position(object.x, object.y));\n\n    // The player does not have a hammer.\n    if (!player.inventory.has(itemIds.hammer)) {\n        player.sendMessage(`You need a hammer to work the metal with.`, true);\n        return;\n    }\n\n    const barLevel = bars.get(item.itemId);\n\n    if (barLevel === undefined) {\n        logger.warn(`Could not find bar level for item id ${item.itemId}`);\n        return;\n    }\n\n    const bar = findItem(item.itemId);\n\n    if (barLevel === undefined || !bar) {\n        logger.error(`Could not find bar with id ${item.itemId}`);\n        return;\n    }\n\n    if (barLevel > player.skills.getLevel(Skill.SMITHING)) {\n        player.sendMessage(`You have to be at least level ${barLevel} to smith ${bar.name}s.`, true);\n        return;\n    }\n\n    player.outgoingPackets.updateClientConfig(210, amountInInventory);\n    player.outgoingPackets.updateClientConfig(211, player.skills.getLevel(Skill.SMITHING));\n\n    details.player.interfaceState.openWidget(widgets.anvil.widgetId, {\n        slot: 'screen',\n    });\n\n    const barWidgetItems = widgetItems.get(item.itemId);\n\n    if (barWidgetItems === undefined) {\n        logger.warn(`Could not find bar widget items for item id ${item.itemId}`);\n        return;\n    }\n\n    barWidgetItems.forEach((items, containerId) => {\n        items.forEach((smithable, index) => {\n            player.outgoingPackets.sendUpdateSingleWidgetItem(\n                {\n                    widgetId: widgets.anvil.widgetId,\n                    containerId: containerId,\n                },\n                index,\n                smithable.item,\n            );\n        });\n    });\n};\n\nexport default {\n    pluginId: 'rs:forging',\n    hooks: [\n        {\n            type: 'item_on_object',\n            itemIds: [...bars.keys()],\n            objectIds: anvilIds,\n            walkTo: true,\n\n            cancelOtherActions: true,\n            handler: openForgingInterface,\n        } as ItemOnObjectActionHook,\n        {\n            type: 'item_interaction',\n            itemIds: [...mapSmithableItemIdsToFlatArray(smithables)],\n            options: ['make', 'make-5', 'make-10'],\n            cancelOtherActions: true,\n            handler: ({ player, itemId, option }) => {\n                const smithable = findSmithableByItemId(itemId);\n\n                if (!smithable) {\n                    logger.error(`Could not find smithable with item id ${itemId}`);\n                    player.sendMessage('Could not find smithable, please tell a dev.');\n                    return;\n                }\n\n                let wantedAmount = 0;\n\n                switch (option) {\n                    case 'make':\n                        wantedAmount = 1;\n                        break;\n                    case 'make-5':\n                        wantedAmount = 5;\n                        break;\n                    case 'make-10':\n                        wantedAmount = 10;\n                        break;\n                }\n\n                if (!canForge(player, smithable)) {\n                    return;\n                }\n\n                player.enqueueTask(ForgingTask, [smithable, wantedAmount]);\n            },\n        } as ItemInteractionActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/smithing/smelting-constants.ts",
    "content": "import { widgets } from '@engine/config/config-handler';\nimport { itemIds } from '@engine/world/config/item-ids';\nimport type { Bar, Smeltable } from '@plugins/skills/smithing/smelting-types';\n\nconst BRONZE: Bar = {\n    barId: itemIds.bars.bronze,\n    requiredLevel: 1,\n    experience: 6.2,\n    ingredients: [\n        { itemId: itemIds.ores.copper, amount: 1 },\n        { itemId: itemIds.ores.tin, amount: 1 },\n    ],\n};\n\nconst BLURITE: Bar = {\n    barId: itemIds.bars.blurite,\n    quest: 'theKnightsSword',\n    requiredLevel: 8,\n    experience: 8,\n    ingredients: [{ itemId: itemIds.ores.blurite, amount: 1 }],\n};\n\nconst IRON: Bar = {\n    barId: itemIds.bars.iron,\n    requiredLevel: 15,\n    experience: 12.5,\n    ingredients: [{ itemId: itemIds.ores.iron, amount: 1 }],\n};\n\nconst SILVER: Bar = {\n    barId: itemIds.bars.silver,\n    requiredLevel: 20,\n    experience: 13.6,\n    ingredients: [{ itemId: itemIds.ores.silver, amount: 1 }],\n};\n\nconst STEEL: Bar = {\n    barId: itemIds.bars.steel,\n    requiredLevel: 30,\n    experience: 17.5,\n    ingredients: [\n        { itemId: itemIds.ores.iron, amount: 1 },\n        { itemId: itemIds.ores.coal, amount: 2 },\n    ],\n};\n\nconst GOLD: Bar = {\n    barId: itemIds.bars.gold,\n    requiredLevel: 40,\n    experience: 22.5,\n    ingredients: [{ itemId: itemIds.ores.gold, amount: 1 }],\n};\n\nconst MITHRIL: Bar = {\n    barId: itemIds.bars.mithril,\n    requiredLevel: 50,\n    experience: 30,\n    ingredients: [\n        { itemId: itemIds.ores.mithril, amount: 1 },\n        { itemId: itemIds.ores.coal, amount: 4 },\n    ],\n};\n\nconst ADAMANTITE: Bar = {\n    barId: itemIds.bars.adamantite,\n    requiredLevel: 70,\n    experience: 37.5,\n    ingredients: [\n        { itemId: itemIds.ores.adamantite, amount: 1 },\n        { itemId: itemIds.ores.coal, amount: 6 },\n    ],\n};\n\nconst RUNEITE: Bar = {\n    barId: itemIds.bars.runite,\n    requiredLevel: 85,\n    experience: 50,\n    ingredients: [\n        { itemId: itemIds.ores.runite, amount: 1 },\n        { itemId: itemIds.ores.coal, amount: 8 },\n    ],\n};\n\nexport const widgetItems = [\n    { slot: widgets.furnace.slots.slot1, bar: BLURITE },\n    { slot: widgets.furnace.slots.slot2, bar: IRON },\n    { slot: widgets.furnace.slots.slot3, bar: SILVER },\n    { slot: widgets.furnace.slots.slot4, bar: STEEL },\n    { slot: widgets.furnace.slots.slot5, bar: GOLD },\n    { slot: widgets.furnace.slots.slot6, bar: MITHRIL },\n    { slot: widgets.furnace.slots.slot7, bar: ADAMANTITE },\n    { slot: widgets.furnace.slots.slot8, bar: RUNEITE },\n];\n\n/**\n * Defines the widget button ids.\n */\nexport const widgetButtonIds: Map<number, Smeltable> = new Map<number, Smeltable>([\n    [16, { takesInput: false, count: 1, bar: BRONZE }],\n    [15, { takesInput: false, count: 5, bar: BRONZE }],\n    [14, { takesInput: false, count: 10, bar: BRONZE }],\n    [13, { takesInput: true, count: 0, bar: BRONZE }],\n    [20, { takesInput: false, count: 1, bar: BLURITE }],\n    [19, { takesInput: false, count: 5, bar: BLURITE }],\n    [18, { takesInput: false, count: 10, bar: BLURITE }],\n    [17, { takesInput: true, count: 0, bar: BLURITE }],\n    [24, { takesInput: false, count: 1, bar: IRON }],\n    [23, { takesInput: false, count: 5, bar: IRON }],\n    [22, { takesInput: false, count: 10, bar: IRON }],\n    [21, { takesInput: true, count: 0, bar: IRON }],\n    [28, { takesInput: false, count: 1, bar: SILVER }],\n    [27, { takesInput: false, count: 5, bar: SILVER }],\n    [26, { takesInput: false, count: 10, bar: SILVER }],\n    [25, { takesInput: true, count: 0, bar: SILVER }],\n    [32, { takesInput: false, count: 1, bar: STEEL }],\n    [31, { takesInput: false, count: 5, bar: STEEL }],\n    [30, { takesInput: false, count: 10, bar: STEEL }],\n    [29, { takesInput: true, count: 0, bar: STEEL }],\n    [36, { takesInput: false, count: 1, bar: GOLD }],\n    [35, { takesInput: false, count: 5, bar: GOLD }],\n    [34, { takesInput: false, count: 10, bar: GOLD }],\n    [33, { takesInput: true, count: 0, bar: GOLD }],\n    [40, { takesInput: false, count: 1, bar: MITHRIL }],\n    [39, { takesInput: false, count: 5, bar: MITHRIL }],\n    [38, { takesInput: false, count: 10, bar: MITHRIL }],\n    [37, { takesInput: true, count: 0, bar: MITHRIL }],\n    [44, { takesInput: false, count: 1, bar: ADAMANTITE }],\n    [43, { takesInput: false, count: 5, bar: ADAMANTITE }],\n    [42, { takesInput: false, count: 10, bar: ADAMANTITE }],\n    [41, { takesInput: true, count: 0, bar: ADAMANTITE }],\n    [48, { takesInput: false, count: 1, bar: RUNEITE }],\n    [47, { takesInput: false, count: 5, bar: RUNEITE }],\n    [46, { takesInput: false, count: 10, bar: RUNEITE }],\n    [45, { takesInput: true, count: 0, bar: RUNEITE }],\n]);\n"
  },
  {
    "path": "src/plugins/skills/smithing/smelting-task.ts",
    "content": "import { findItem } from '@engine/config/config-handler';\nimport { ActorTask } from '@engine/task/impl/actor-task';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport { animationIds } from '@engine/world/config/animation-ids';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport type { Smeltable } from './smelting-types';\n\n/**\n * A task that handles the smelting of an item.\n *\n * Operates repeatedly every 3 ticks, and stops when the player has smelted the amount they wanted.\n *\n * @author jameskmonger\n */\nexport class SmeltingTask extends ActorTask<Player> {\n    private elapsedTicks = 0;\n    private amountSmelted = 0;\n\n    constructor(\n        player: Player,\n        private readonly smeltable: Smeltable,\n        private readonly amount: number,\n    ) {\n        super(player);\n    }\n\n    public execute(): void {\n        const taskIteration = this.elapsedTicks++;\n\n        // completed the task if we've smelted the amount we wanted\n        if (this.amountSmelted >= this.amount) {\n            this.stop();\n            return;\n        }\n\n        const bar = this.smeltable.bar;\n        const barItem = findItem(bar.barId);\n\n        if (!barItem) {\n            this.actor.sendMessage(`Could not find item with id ${bar.barId}. Please tell a dev.`);\n            this.stop();\n            return;\n        }\n\n        if (!this.hasMaterials()) {\n            this.actor.sendMessage(`You don't have enough ${barItem.name.toLowerCase()}.`, true);\n            this.stop();\n            return;\n        }\n\n        if (!this.hasLevel()) {\n            this.actor.sendMessage(`You need a smithing level of ${bar.requiredLevel} to smelt ${barItem.name.toLowerCase()}s.`, true);\n            return;\n        }\n\n        // Smelting takes 3 ticks for each item\n        if (taskIteration % 3 !== 0) {\n            return;\n        }\n\n        bar.ingredients.forEach(item => {\n            for (let i = 0; i < item.amount; i++) {\n                this.actor.removeFirstItem(item.itemId);\n            }\n        });\n\n        this.actor.giveItem(bar.barId);\n        this.actor.skills.addExp(Skill.SMITHING, bar.experience);\n        this.amountSmelted++;\n\n        this.actor.playAnimation(animationIds.smelting);\n        this.actor.outgoingPackets.playSound(soundIds.smelting, 5);\n    }\n\n    /**\n     * Whether the player has the required materials to smelt the item.\n     * @returns {boolean} True if the player has the required materials, false otherwise.\n     */\n    private hasMaterials() {\n        return this.smeltable.bar.ingredients.every(item => {\n            const itemIndex = this.actor.inventory.findIndex(item);\n            if (itemIndex === -1 || this.actor.inventory.amountInStack(itemIndex) < item.amount) {\n                return false;\n            }\n\n            return true;\n        });\n    }\n\n    /**\n     * Whether the player has the required level to smelt the item.\n     * @returns {boolean} True if the player has the required level, false otherwise.\n     */\n    private hasLevel() {\n        return this.actor.skills.hasLevel(Skill.SMITHING, this.smeltable.bar.requiredLevel);\n    }\n}\n"
  },
  {
    "path": "src/plugins/skills/smithing/smelting-types.ts",
    "content": "import type { Item } from '@engine/world/items/item';\n\nexport interface Bar {\n    barId: number;\n    quest?: string;\n    requiredLevel: number;\n    ingredients: Item[];\n    experience: number;\n}\n\nexport interface Smeltable {\n    takesInput: boolean;\n    count: number;\n    bar: Bar;\n}\n"
  },
  {
    "path": "src/plugins/skills/smithing/smelting.plugin.ts",
    "content": "import type { ButtonActionHook, buttonActionHandler } from '@engine/action/pipe/button.action';\nimport type {\n    ObjectInteractionAction,\n    ObjectInteractionActionHook,\n    objectInteractionActionHandler,\n} from '@engine/action/pipe/object-interaction.action';\nimport { widgets } from '@engine/config/config-handler';\nimport { colors } from '@engine/util/colors';\nimport { Skill } from '@engine/world/actor/skills';\nimport { objectIds } from '@engine/world/config/object-ids';\nimport { widgetButtonIds, widgetItems } from '@plugins/skills/smithing/smelting-constants';\nimport { SmeltingTask } from './smelting-task';\n\nexport const openSmeltingInterface: objectInteractionActionHandler = details => {\n    details.player.interfaceState.openWidget(widgets.furnace.widgetId, {\n        slot: 'chatbox',\n    });\n    loadSmeltingInterface(details);\n};\n\n// We need to tell the widget what the bars actually look like.\nconst loadSmeltingInterface = (details: ObjectInteractionAction) => {\n    const theKnightsSwordQuest = details.player.quests.find(quest => quest.questId === 'theKnightsSword');\n\n    // Send the items to the widget.\n    widgetItems.forEach(item => {\n        details.player.outgoingPackets.setItemOnWidget(widgets.furnace.widgetId, item.slot.modelId, item.bar.barId, 125);\n        if (!details.player.skills.hasLevel(Skill.SMITHING, item.bar.requiredLevel)) {\n            details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.red });\n        } else {\n            details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.black });\n        }\n\n        // TODO (Jameskmonger) I don't think that this logic is correct.. it targets all items, not just those related to the quest.\n        // Check if the player has completed 'The Knight's Sword' quest, even if the level is okay.\n        if (Boolean(item.bar.quest) && (!theKnightsSwordQuest || theKnightsSwordQuest.complete)) {\n            details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.red });\n        }\n    });\n};\n\nexport const buttonClicked: buttonActionHandler = details => {\n    // Check if player might be spawning widget clientside\n    // TODO - this should be handled by the engine\n    if (!details.player.interfaceState.findWidget(widgets.furnace.widgetId)) {\n        return;\n    }\n\n    const smeltable = widgetButtonIds.get(details.buttonId);\n\n    // TODO (Jameskmonger) check for quest-specific items, e.g. the knights sword\n    // const theKnightsSwordQuest: PlayerQuest = details.player.quests.find(quest => quest.questId === 'theKnightsSword');\n    // if (bar.quest !== undefined && (theKnightsSwordQuest == undefined || theKnightsSwordQuest.complete)) {\n    //     details.player.sendMessage(`You need to complete The Knight's Sword quest first.`, true);\n    //     return;\n    // }\n\n    if (!smeltable) {\n        details.player.sendMessage(`Could not find smeltable for button id ${details.buttonId}. Please tell a dev.`);\n        return;\n    }\n\n    details.player.interfaceState.closeAllSlots();\n\n    if (!smeltable.takesInput) {\n        details.player.enqueueTask(SmeltingTask, [smeltable, smeltable.count]);\n        return;\n    }\n\n    const numericInputSpinSubscription = details.player.numericInputEvent.subscribe(number => {\n        actionCancelledSpinSubscription?.unsubscribe();\n        numericInputSpinSubscription?.unsubscribe();\n\n        details.player.enqueueTask(SmeltingTask, [smeltable, number]);\n    });\n\n    const actionCancelledSpinSubscription = details.player.actionsCancelled.subscribe(() => {\n        actionCancelledSpinSubscription?.unsubscribe();\n        numericInputSpinSubscription?.unsubscribe();\n    });\n\n    details.player.outgoingPackets.showNumberInputDialogue();\n};\n\nexport default {\n    pluginId: 'rs:smelting',\n    hooks: [\n        {\n            type: 'object_interaction',\n            objectIds: [objectIds.furnace, 11666],\n            options: ['smelt'],\n            walkTo: true,\n            handler: openSmeltingInterface,\n        } as ObjectInteractionActionHook,\n        {\n            type: 'button',\n            widgetId: widgets.furnace.widgetId,\n            buttonIds: Array.from(widgetButtonIds.keys()),\n            handler: buttonClicked,\n        } as ButtonActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/woodcutting/chance.ts",
    "content": "import { randomBetween } from '@engine/util/num';\nimport type { IHarvestable } from '@engine/world/config/harvestable-object';\n\n/**\n * Roll a random number between 0 and 255 and compare it to the percent needed to cut the tree.\n *\n * @param tree The tree to cut\n * @param toolLevel The level of the axe being used\n * @param woodcuttingLevel The player's woodcutting level\n *\n * @returns True if the tree was successfully cut, false otherwise\n */\nexport const canCut = (tree: IHarvestable, toolLevel: number, woodcuttingLevel: number): boolean => {\n    const successChance = randomBetween(0, 255);\n\n    const percentNeeded = tree.baseChance + toolLevel + woodcuttingLevel;\n    return successChance <= percentNeeded;\n};\n"
  },
  {
    "path": "src/plugins/skills/woodcutting/index.ts",
    "content": "import type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action';\nimport { getTreeIds } from '@engine/world/config/harvestable-object';\nimport { runWoodcuttingTask } from './woodcutting-task';\n\n/**\n * Woodcutting plugin\n *\n * This uses the task system to schedule actions.\n */\nexport default {\n    pluginId: 'rs:woodcutting',\n    hooks: [\n        /**\n         * \"Chop down\" / \"chop\" object interaction hook.\n         */\n        {\n            type: 'object_interaction',\n            options: ['chop down', 'chop'],\n            objectIds: getTreeIds(),\n            handler: ({ player, object }) => {\n                runWoodcuttingTask(player, object);\n            },\n        } as ObjectInteractionActionHook,\n    ],\n};\n"
  },
  {
    "path": "src/plugins/skills/woodcutting/woodcutting-task.ts",
    "content": "import { findItem, findObject } from '@engine/config/config-handler';\nimport { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task';\nimport { colors } from '@engine/util/colors';\nimport { randomBetween } from '@engine/util/num';\nimport { colorText } from '@engine/util/strings';\nimport { activeWorld } from '@engine/world';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { Skill } from '@engine/world/actor/skills';\nimport type { IHarvestable } from '@engine/world/config/harvestable-object';\nimport { getTreeFromHealthy } from '@engine/world/config/harvestable-object';\nimport { soundIds } from '@engine/world/config/sound-ids';\nimport { rollBirdsNestType } from '@engine/world/skill-util/harvest-roll';\nimport { canInitiateHarvest } from '@engine/world/skill-util/harvest-skill';\nimport { logger } from '@runejs/common';\nimport type { LandscapeObject } from '@runejs/filestore';\nimport { canCut } from './chance';\n\nclass WoodcuttingTask extends ActorLandscapeObjectInteractionTask<Player> {\n    /**\n     * The tree being cut down.\n     */\n    private treeInfo: IHarvestable;\n\n    /**\n     * The number of ticks that `execute` has been called inside this task.\n     */\n    private elapsedTicks = 0;\n\n    /**\n     * Create a new woodcutting task.\n     *\n     * @param player The player that is attempting to cut down the tree.\n     * @param landscapeObject The object that represents the tree.\n     * @param sizeX The size of the tree in x axis.\n     * @param sizeY The size of the tree in y axis.\n     */\n    constructor(player: Player, landscapeObject: LandscapeObject, sizeX: number, sizeY: number) {\n        super(player, landscapeObject, sizeX, sizeY);\n\n        if (!landscapeObject) {\n            this.stop();\n            return;\n        }\n\n        this.treeInfo = getTreeFromHealthy(landscapeObject.objectId);\n        if (!this.treeInfo) {\n            this.stop();\n            return;\n        }\n    }\n\n    private getItemToAdd(): string | null {\n        this.actor.sendMessage(`Looking for item ${this.treeInfo.items}`);\n        if (typeof this.treeInfo.items === 'string') {\n            return this.treeInfo.items;\n        }\n\n        // Handle weighted items\n        const totalWeight = this.treeInfo.items.reduce((sum, item) => sum + item.weight, 0);\n        let random = randomBetween(1, totalWeight);\n\n        for (const item of this.treeInfo.items) {\n            random -= item.weight;\n            if (random <= 0) {\n                return item.itemConfigId;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Execute the main woodcutting task loop. This method is called every game tick until the task is completed.\n     *\n     * As this task extends {@link ActorLandscapeObjectInteractionTask}, it's important that the\n     * `super.execute` method is called at the start of this method.\n     *\n     * The base `execute` performs a number of checks that allow this task to function healthily.\n     */\n    public execute(): void {\n        super.execute();\n\n        if (!this.isActive || !this.landscapeObject) {\n            return;\n        }\n\n        // store the tick count before incrementing so we don't need to keep track of it in all the separate branches\n        const taskIteration = this.elapsedTicks++;\n\n        const tool = canInitiateHarvest(this.actor, this.treeInfo, Skill.WOODCUTTING);\n\n        if (!tool) {\n            this.stop();\n            return;\n        }\n\n        if (taskIteration === 0) {\n            this.actor.sendMessage('You swing your axe at the tree.');\n            this.actor.face(this.landscapeObjectPosition);\n            this.actor.playAnimation(tool.animation);\n            // First tick / iteration should never proceed beyond this point.\n            return;\n        }\n\n        // play a random axe sound at the correct time\n        if (taskIteration % 3 !== 0) {\n            const randomSoundIdx = Math.floor(Math.random() * soundIds.axeSwing.length);\n            this.actor.playSound(soundIds.axeSwing[randomSoundIdx], 7, 0);\n        }\n\n        // roll for success\n        const succeeds = canCut(this.treeInfo, tool.level, this.actor.skills.woodcutting.level);\n\n        if (!succeeds) {\n            this.actor.playAnimation(tool.animation);\n\n            // Keep chopping.\n            return;\n        }\n\n        const itemConfigId = this.getItemToAdd();\n        if (!itemConfigId) {\n            logger.error('Could not determine item to add from tree');\n            this.actor.sendMessage('Sorry, an error occurred. Please report this to a developer.');\n            this.stop();\n            return;\n        }\n\n        const logItem = findItem(itemConfigId);\n        if (!logItem) {\n            logger.error(`Could not find log item with id ${itemConfigId}`);\n            this.actor.sendMessage('Sorry, an error occurred. Please report this to a developer.');\n            this.stop();\n            return;\n        }\n\n        const targetName = (logItem.name || '').toLowerCase();\n\n        // if player doesn't have space in inventory, stop the task\n        if (!this.actor.inventory.hasSpace()) {\n            this.actor.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true);\n            this.actor.playSound(soundIds.inventoryFull);\n            this.stop();\n            return;\n        }\n\n        const roll = randomBetween(1, 256);\n        // roll for bird nest chance\n        if (roll === 1) {\n            this.actor.sendMessage(colorText(`A bird's nest falls out of the tree.`, colors.red));\n            activeWorld.globalInstance.spawnWorldItem(rollBirdsNestType(), this.actor.position, {\n                owner: this.actor || null,\n                expires: 300,\n            });\n        } else {\n            // Standard log chopper\n            this.actor.sendMessage(`You manage to chop some ${targetName}.`);\n            this.actor.giveItem(itemConfigId);\n        }\n\n        this.actor.skills.woodcutting.addExp(this.treeInfo.experience);\n\n        // check if the tree should be broken\n        if (randomBetween(0, 100) <= this.treeInfo.break) {\n            // TODO (Jameskmonger) is this the correct sound?\n            this.actor.playSound(soundIds.oreDepeleted);\n\n            const brokenTreeId = this.treeInfo.objects.get(this.landscapeObject.objectId);\n\n            if (brokenTreeId !== undefined) {\n                this.actor.instance.replaceGameObject(\n                    brokenTreeId,\n                    this.landscapeObject,\n                    randomBetween(this.treeInfo.respawnLow, this.treeInfo.respawnHigh),\n                );\n            } else {\n                logger.error(`Could not find broken tree id for tree id ${this.landscapeObject.objectId}`);\n            }\n\n            this.stop();\n        }\n    }\n\n    /**\n     * This method is called when the task stops.\n     */\n    public onStop(): void {\n        super.onStop();\n\n        this.actor.stopAnimation();\n    }\n}\n\nexport function runWoodcuttingTask(player: Player, landscapeObject: LandscapeObject): void {\n    const objectConfig = findObject(landscapeObject.objectId);\n\n    if (!objectConfig) {\n        logger.warn(`Player ${player.username} attempted to run a woodcutting task on an invalid object (id: ${landscapeObject.objectId})`);\n        return;\n    }\n\n    const sizeX = objectConfig.rendering.sizeX;\n    const sizeY = objectConfig.rendering.sizeY;\n\n    player.enqueueTask(WoodcuttingTask, [landscapeObject, sizeX, sizeY]);\n}\n"
  },
  {
    "path": "src/server/game/game-server-config.ts",
    "content": "export interface GameServerConfig {\n    rsaMod: string;\n    rsaExp: string;\n    host: string;\n    port: number;\n    encryptionEnabled: boolean;\n    loginServerHost: string;\n    loginServerPort: number;\n    updateServerHost: string;\n    updateServerPort: number;\n    showWelcome: boolean;\n    expRate: number;\n    giveAchievements: boolean;\n    checkCredentials: boolean;\n    tutorialEnabled: boolean;\n    adminDropsEnabled: boolean;\n    bypassTeleportRequirements?: boolean;\n}\n"
  },
  {
    "path": "src/server/game/game-server-connection.ts",
    "content": "import type { Socket } from 'net';\n\nimport { handlePacket, incomingPackets } from '@engine/net/inbound-packet-handler';\nimport type { Player } from '@engine/world/actor/player/player';\nimport { logger } from '@runejs/common';\nimport { ByteBuffer } from '@runejs/common';\n\nexport class GameServerConnection {\n    private activePacketId: number | null = null;\n    private activePacketSize: number | null = null;\n    private activeBuffer: ByteBuffer | null;\n\n    public constructor(\n        private readonly clientSocket: Socket,\n        private readonly player: Player,\n    ) {}\n\n    public decodeMessage(buffer?: ByteBuffer): void | Promise<void> {\n        if (!this.activeBuffer) {\n            if (!buffer) {\n                logger.error(`No buffer provided to decodeMessage.`);\n                return;\n            } else {\n                this.activeBuffer = buffer;\n            }\n        } else if (buffer) {\n            const readable = this.activeBuffer.readable;\n            const newBuffer = new ByteBuffer(readable + buffer.length);\n            this.activeBuffer.copy(newBuffer, 0, this.activeBuffer.readerIndex);\n            buffer.copy(newBuffer, readable, 0);\n            this.activeBuffer = newBuffer;\n        }\n\n        if (this.activePacketId === null) {\n            this.activePacketId = -1;\n        }\n\n        if (this.activePacketSize === null) {\n            this.activePacketSize = -1;\n        }\n\n        const inCipher = this.player.inCipher;\n\n        if (this.activePacketId === -1) {\n            if (this.activeBuffer.readable < 1) {\n                return;\n            }\n\n            this.activePacketId = this.activeBuffer.get('byte', 'u');\n            this.activePacketId = (this.activePacketId - inCipher.rand()) & 0xff;\n            const incomingPacket = incomingPackets.get(this.activePacketId);\n            if (incomingPacket) {\n                this.activePacketSize = incomingPacket.size;\n            } else {\n                this.activePacketSize = -3;\n            }\n        }\n\n        // Packet will provide the size\n        if (this.activePacketSize === -1) {\n            if (this.activeBuffer.readable < 1) {\n                return;\n            }\n\n            this.activePacketSize = this.activeBuffer.get('byte', 'u');\n        }\n\n        // Packet has no set size\n        let clearBuffer = false;\n        if (this.activePacketSize === -3) {\n            if (this.activeBuffer.readable < 1) {\n                return;\n            }\n\n            this.activePacketSize = this.activeBuffer.readable;\n            clearBuffer = true;\n        }\n\n        if (this.activeBuffer.readable < this.activePacketSize) {\n            return;\n        }\n\n        // read packet data\n        let packetData: ByteBuffer | null = null;\n        if (this.activePacketSize !== 0) {\n            packetData = new ByteBuffer(this.activePacketSize);\n            this.activeBuffer.copy(packetData, 0, this.activeBuffer.readerIndex, this.activeBuffer.readerIndex + this.activePacketSize);\n            this.activeBuffer.readerIndex += this.activePacketSize;\n        }\n\n        if (packetData && !handlePacket(this.player, this.activePacketId, this.activePacketSize, packetData)) {\n            logger.error(\n                `Player packets out of sync for ${this.player.username}, resetting packet buffer...`,\n                `If you're seeing this, there's a packet that needs fixing. :)`,\n            );\n            clearBuffer = true;\n        }\n\n        if (clearBuffer) {\n            this.activeBuffer = null;\n        }\n\n        this.activePacketId = null;\n        this.activePacketSize = null;\n\n        if (this.activeBuffer !== null && this.activeBuffer.readable > 0) {\n            this.decodeMessage();\n        }\n    }\n\n    public connectionDestroyed(): void {\n        logger.info(`Connection destroyed.`);\n        this.player?.logout();\n    }\n\n    public closeSocket(): void {\n        this.clientSocket.destroy();\n    }\n}\n"
  },
  {
    "path": "src/server/game/game-server.ts",
    "content": "import { logger } from '@runejs/common';\nimport { SocketServer, parseServerConfig } from '@runejs/common/net';\nimport { Filestore } from '@runejs/filestore';\n\nimport { loadCoreConfigurations, loadGameConfigurations, xteaRegions } from '@engine/config/config-handler';\nimport { loadPackets } from '@engine/net/inbound-packet-handler';\nimport { watchForChanges, watchSource } from '@engine/util/files';\nimport { activateGameWorld } from '@engine/world';\nimport type { GameServerConfig } from '@server/game/game-server-config';\nimport { GatewayServer } from '@server/gateway/gateway-server';\n\n/**\n * The singleton instance containing the server's active configuration settings.\n */\nexport let serverConfig: GameServerConfig;\n\n/**\n * The singleton instance referencing the game's asset file store.\n */\nexport let filestore: Filestore;\n\nexport const openGatewayServer = (host: string, port: number): void => {\n    SocketServer.launch<GatewayServer>('Game Gateway Server', host, port, socket => new GatewayServer(socket));\n};\n\nexport async function setupConfig(): Promise<boolean> {\n    serverConfig = parseServerConfig<GameServerConfig>();\n\n    if (!serverConfig) {\n        logger.error('Unable to start server due to missing or invalid server configuration.');\n        return false;\n    }\n\n    await loadCoreConfigurations();\n    filestore = new Filestore('cache', { xteas: xteaRegions });\n\n    await loadGameConfigurations();\n    return true;\n}\n\n/**\n * Configures the game server, parses the asset file store, initializes the game world,\n * and finally spins up the game server itself.\n */\nexport async function launchGameServer(): Promise<void> {\n    const config = await setupConfig();\n    if (!config) {\n        return;\n    }\n    await loadPackets();\n    const world = await activateGameWorld();\n\n    if (process.argv.indexOf('-fakePlayers') !== -1) {\n        world.generateFakePlayers();\n    }\n\n    openGatewayServer(serverConfig.host, serverConfig.port);\n\n    watchSource('src/').subscribe(() => world.saveOnlinePlayers());\n    watchForChanges('dist/plugins/', /[/\\\\]plugins[/\\\\]/);\n}\n"
  },
  {
    "path": "src/server/gateway/gateway-server.ts",
    "content": "import type { Socket } from 'net';\nimport { createConnection } from 'net';\n\nimport { logger } from '@runejs/common';\nimport { ByteBuffer } from '@runejs/common';\nimport { SocketServer, parseServerConfig } from '@runejs/common/net';\nimport { LoginResponseCode } from '@runejs/login-server';\n\nimport { Isaac } from '@engine/net/isaac';\nimport { activeWorld } from '@engine/world';\nimport { Player } from '@engine/world/actor/player/player';\nimport type { GameServerConfig } from '@server/game/game-server-config';\nimport { GameServerConnection } from '@server/game/game-server-connection';\n\nconst serverConfig = parseServerConfig<GameServerConfig>();\n\nexport type ServerType = 'game_server' | 'login_server' | 'update_server';\n\nexport class GatewayServer extends SocketServer {\n    private serverType: ServerType;\n    private gameServerConnection: GameServerConnection;\n    private loginServerSocket: Socket;\n    private updateServerSocket: Socket;\n    private serverKey: bigint;\n\n    public constructor(private readonly clientSocket: Socket) {\n        super(clientSocket);\n    }\n\n    public initialHandshake(buffer: ByteBuffer): boolean {\n        if (this.serverType) {\n            this.decodeMessage(buffer);\n            return true;\n        }\n\n        // First communication from the game client to the server gateway\n        // Here we find out what kind of connection the client is making - game, or update server?\n        // If game - they'll need to pass through the login server to authenticate first!\n\n        const packetId = buffer.get('byte', 'u');\n\n        if (packetId === 15) {\n            this.serverType = 'update_server';\n            this.updateServerSocket = createConnection({\n                host: serverConfig.updateServerHost,\n                port: serverConfig.updateServerPort,\n            });\n            this.updateServerSocket.on('data', data => this.clientSocket.write(data));\n            this.updateServerSocket.on('end', () => {\n                logger.info(`Update server connection closed.`);\n            });\n            this.updateServerSocket.on('error', () => {\n                logger.error(`Update server error.`);\n            });\n            this.updateServerSocket.setNoDelay(true);\n            this.updateServerSocket.setKeepAlive(true);\n            this.updateServerSocket.setTimeout(30000);\n        } else if (packetId === 14) {\n            this.serverType = 'login_server';\n            this.loginServerSocket = createConnection({\n                host: serverConfig.loginServerHost,\n                port: serverConfig.loginServerPort,\n            });\n            this.loginServerSocket.on('data', data => {\n                this.parseLoginServerResponse(new ByteBuffer(data));\n            });\n            this.loginServerSocket.on('end', () => {\n                logger.error(`Login server error.`);\n            });\n            this.loginServerSocket.setNoDelay(true);\n            this.loginServerSocket.setKeepAlive(true);\n            this.loginServerSocket.setTimeout(30000);\n        } else {\n            logger.error(`Invalid initial client handshake packet id.`);\n            return false;\n        }\n\n        const data = buffer.getSlice(1, buffer.length);\n        const socket = this.serverType === 'login_server' ? this.loginServerSocket : this.updateServerSocket;\n        socket.write(data);\n\n        return true;\n    }\n\n    public decodeMessage(buffer: ByteBuffer): void | Promise<void> {\n        if (this.serverType === 'login_server') {\n            this.loginServerSocket.write(buffer);\n        } else if (this.serverType === 'update_server') {\n            this.updateServerSocket.write(buffer);\n        } else {\n            this.gameServerConnection?.decodeMessage(buffer);\n        }\n    }\n\n    public connectionDestroyed(): void {\n        this.loginServerSocket?.destroy();\n        this.updateServerSocket?.destroy();\n        this.gameServerConnection?.connectionDestroyed();\n    }\n\n    private async parseLoginServerResponse(buffer: ByteBuffer): Promise<void> {\n        if (!this.serverKey) {\n            // Login handshake response\n            const handshakeResponseCode = buffer.get('byte');\n\n            if (handshakeResponseCode === 0) {\n                this.serverKey = BigInt(buffer.get('long'));\n            }\n        } else {\n            // Login response\n            const loginResponseCode = buffer.get('byte');\n\n            if (loginResponseCode === LoginResponseCode.SUCCESS) {\n                try {\n                    const clientKey1 = buffer.get('int');\n                    const clientKey2 = buffer.get('int');\n                    const gameClientId = buffer.get('int');\n                    const username = buffer.getString();\n                    const passwordHash = buffer.getString();\n                    const lowDetail = buffer.get('byte') === 1;\n\n                    if (activeWorld.playerOnline(username)) {\n                        // Player is already logged in!\n                        // @TODO move to login server\n                        buffer = new ByteBuffer(1);\n                        buffer.put(LoginResponseCode.ALREADY_LOGGED_IN);\n                    } else {\n                        this.serverType = 'game_server';\n                        await this.createPlayer([clientKey1, clientKey2], gameClientId, username, passwordHash, lowDetail ? 'low' : 'high');\n                        return;\n                    }\n                } catch (e) {\n                    this.gameServerConnection?.closeSocket();\n                    logger.error(e);\n                }\n            }\n        }\n\n        // Write the login server response back to the game client\n        this.clientSocket.write(buffer);\n    }\n\n    private async createPlayer(\n        clientKeys: [number, number],\n        gameClientId: number,\n        username: string,\n        passwordHash: string,\n        detail: 'high' | 'low',\n    ): Promise<void> {\n        const sessionKey: number[] = [\n            Number(clientKeys[0]),\n            Number(clientKeys[1]),\n            Number(this.serverKey >> BigInt(32)),\n            Number(this.serverKey),\n        ];\n\n        const inCipher = new Isaac(sessionKey);\n\n        for (let i = 0; i < 4; i++) {\n            sessionKey[i] += 50;\n        }\n\n        const outCipher = new Isaac(sessionKey);\n\n        const player = new Player(this.clientSocket, inCipher, outCipher, gameClientId, username, passwordHash, detail === 'low');\n\n        this.gameServerConnection = new GameServerConnection(this.clientSocket, player);\n\n        activeWorld.registerPlayer(player);\n\n        const outputBuffer = new ByteBuffer(6);\n\n        outputBuffer.put(LoginResponseCode.SUCCESS, 'byte');\n        outputBuffer.put(player.rights.valueOf(), 'byte');\n        outputBuffer.put(0, 'byte'); // account flagged\n        outputBuffer.put(player.worldIndex + 1, 'short');\n        outputBuffer.put(0, 'byte'); // membership status (for friends list count)\n        this.clientSocket.write(outputBuffer);\n\n        await player.init();\n    }\n}\n"
  },
  {
    "path": "src/server/runner.ts",
    "content": "import 'source-map-support/register';\n\nimport { initErrorHandling } from '@engine/util/error-handling';\nimport { activeWorld } from '@engine/world';\nimport { logger } from '@runejs/common';\nimport { launchLoginServer } from '@runejs/login-server';\nimport { launchUpdateServer } from '@runejs/update-server';\nimport { launchGameServer } from '@server/game/game-server';\n\nconst shutdownEvents = [\n    'SIGHUP',\n    'SIGINT',\n    'SIGQUIT',\n    'SIGILL',\n    'SIGTRAP',\n    'SIGABRT',\n    'SIGBUS',\n    'SIGFPE',\n    'SIGUSR1',\n    'SIGSEGV',\n    'SIGUSR2',\n    'SIGTERM',\n];\n\nlet running: boolean = true;\nlet type: 'game' | 'login' | 'update' = 'game';\n\nif (process.argv.indexOf('-login') !== -1) {\n    type = 'login';\n} else if (process.argv.indexOf('-update') !== -1) {\n    type = 'update';\n}\n\nshutdownEvents.forEach(signal =>\n    process.on(signal as any, () => {\n        if (!running) {\n            return;\n        }\n        running = false;\n\n        logger.warn(`${signal} received.`);\n\n        if (type === 'game') {\n            activeWorld?.shutdown();\n        }\n\n        logger.info(`${type.charAt(0).toUpperCase()}${type.substring(1)} Server shutting down...`);\n        process.exit(0);\n    }),\n);\n\ninitErrorHandling();\n\nif (type === 'game') {\n    launchGameServer();\n} else if (type === 'login') {\n    launchLoginServer();\n} else if (type === 'update') {\n    launchUpdateServer();\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"noEmit\": true,\n        \"module\": \"commonjs\",\n        \"target\": \"ESNext\",\n        \"jsx\": \"preserve\",\n        \"importHelpers\": true,\n        \"moduleResolution\": \"node\",\n        \"experimentalDecorators\": true,\n        \"esModuleInterop\": true,\n        \"resolveJsonModule\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"sourceMap\": true,\n        \"strict\": false,\n        \"strictBindCallApply\": true,\n        \"strictFunctionTypes\": false,\n        \"strictNullChecks\": true,\n        \"strictPropertyInitialization\": false,\n        \"baseUrl\": \".\",\n        \"allowJs\": true,\n        \"outDir\": \"./dist\",\n        \"paths\": {\n            \"@engine/*\": [\"src/engine/*\"],\n            \"@server/*\": [\"src/server/*\"],\n            \"@plugins/*\": [\"src/plugins/*\"]\n        },\n        \"types\": [\"jest\", \"node\"],\n        \"lib\": [\"esnext\", \"dom\", \"dom.iterable\", \"scripthost\"]\n    },\n    \"include\": [\"src/**/*.js\", \"src/**/*.ts\", \"tests/**/*.js\", \"tests/**/*.ts\"],\n    \"exclude\": [\"node_modules\"]\n}\n"
  }
]