├── .eslintrc.json
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE
│ ├── feature.md
│ └── fix.md
├── dependabot.yml
└── workflows
│ ├── bun.yml
│ ├── deno.yml
│ ├── games_list.yml
│ ├── id-tests.yml
│ └── node.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── GAMES_LIST.md
├── LICENSE
├── MIGRATE_IDS.md
├── README.md
├── bin
└── gamedig.js
├── examples
├── import.mjs
└── require.cjs
├── lib
├── DnsResolver.js
├── GlobalUdpSocket.js
├── HexUtil.js
├── Logger.js
├── Promises.js
├── ProtocolResolver.js
├── QueryRunner.js
├── Results.js
├── game-resolver.js
├── gamedig.js
├── games.js
├── index.js
└── reader.js
├── package-lock.json
├── package.json
├── protocols
├── altvmp.js
├── armagetron.js
├── asa.js
├── ase.js
├── assettocorsa.js
├── battlefield.js
├── beammp.js
├── beammpmaster.js
├── brokeprotocol.js
├── brokeprotocolmaster.js
├── buildandshoot.js
├── core.js
├── cs2d.js
├── dayz.js
├── discord.js
├── doom3.js
├── eco.js
├── eldewrito.js
├── epic.js
├── factorio.js
├── farmingsimulator.js
├── ffow.js
├── fivem.js
├── gamespy1.js
├── gamespy2.js
├── gamespy3.js
├── geneshift.js
├── goldsrc.js
├── gtasao.js
├── hawakening.js
├── hawakeningmaster.js
├── hexen2.js
├── index.js
├── jc2mp.js
├── kspdmp.js
├── mafia2mp.js
├── mafia2online.js
├── minecraft.js
├── minecraftbedrock.js
├── minecraftvanilla.js
├── minetest.js
├── mumble.js
├── mumbleping.js
├── nadeo.js
├── openttd.js
├── palworld.js
├── quake1.js
├── quake2.js
├── quake3.js
├── ragemp.js
├── renegadex.js
├── renegadexmaster.js
├── renown.js
├── rfactor.js
├── samp.js
├── satisfactory.js
├── savage2.js
├── sdtd.js
├── soldat.js
├── starmade.js
├── starsiege.js
├── teamspeak2.js
├── teamspeak3.js
├── terraria.js
├── theisleevrima.js
├── toxikk.js
├── tribes1.js
├── tribes1master.js
├── unreal2.js
├── ut3.js
├── valve.js
├── vcmp.js
├── ventrilo.js
├── vintagestory.js
├── vintagestorymaster.js
├── warsow.js
└── xonotic.js
└── tools
├── README.md
├── attempt_protocols.js
├── esbuild.js
├── find-id-changes.js
├── find_id_duplicates.js
├── generate_games_list.js
└── run-id-tests.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": false,
4 | "es2021": true
5 | },
6 | "extends": "standard",
7 | "parserOptions": {
8 | "ecmaVersion": 2021,
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'bug: something is not working'
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Steps To Reproduce**
14 | Please provide the steps to reproduce the behavior, this should include what version of the library you are using, a code example and (if applicable and possible) the server IP you are trying to query.
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots or Data**
20 | If applicable, add screenshots/data to help explain your problem, if this is about a query, run the CLI version with the `--debug` option and put the output in a collapsible section.
21 |
22 | ```
23 | How to do a collapsible section:
24 |
25 | This is hidden until it is not!
26 |
27 | ```
28 |
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'feat: new stuff!'
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What is this feature about?**
11 | Shortly explain what your requested feature is about.
12 |
13 | **Additional context/references**
14 | Add any other context or references about the feature request here.
15 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature
3 | about: Add something new and hopefully useful
4 | title: 'feat: add this game'
5 | labels: feature
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 | **Describe the changes**
12 | - [ ] I have added a line in the [CHANGELOG.md](https://github.com/gamedig/node-gamedig/blob/master/CHANGELOG.md) file.
13 |
14 |
15 |
16 | A clear and concise summary of the changes.
17 |
18 | **Screenshots or Data**
19 | If applicable, add screenshots/data to help explain the changes.
20 |
21 | This contains lots and lots of logs.
22 | ```
23 | How to do a collapsible section:
24 |
25 | This is hidden until it is not!
26 |
27 | ```
28 |
29 |
30 | **Additional context**
31 | Add any other context about the changes here, for example:
32 | * 'Closes issue #T, lore-related to issue #F, mentioned in pr #Two'
33 | * 'To validate the changes, one can test on this address: 127.255.0.256'
34 | * 'I couldn't find any live servers to validate these changes, here are the docs on how to start a local server: www.google.com'
35 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/fix.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Fix
3 | about: Change something broken to make it not broken anymore
4 | title: 'fix: 1 === 1 is false'
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 | **Describe the changes**
12 | - [ ] I have added a line in the [CHANGELOG.md](https://github.com/gamedig/node-gamedig/blob/master/CHANGELOG.md) file.
13 |
14 |
15 |
16 | A clear and concise summary of the changes.
17 |
18 | **Screenshots or Data**
19 | If applicable, add screenshots/data to help explain the changes.
20 |
21 | This contains lots and lots of logs.
22 | ```
23 | How to do a collapsible section:
24 |
25 | This is hidden until it is not!
26 |
27 | ```
28 |
29 |
30 | **Additional context**
31 | Add any other context about the changes here, for example:
32 | * 'Closes issue #T, lore-related to issue #F, mentioned in pr #Two'
33 | * 'To validate the changes, one can test on this address: 127.255.0.256'
34 | * 'I couldn't find any live servers to validate these changes, here are the docs on how to start a local server: www.google.com'
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 |
2 | version: 2
3 | updates:
4 | - package-ecosystem: "npm"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 | versioning-strategy: increase
9 |
--------------------------------------------------------------------------------
/.github/workflows/bun.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Bun
3 |
4 | on:
5 | push:
6 | branches: ["master"]
7 | paths:
8 | - "**.js" # Any JS file
9 | - "package.json"
10 | - "package-lock.json"
11 | - ".github/workflows/bun.yml" # This action
12 | pull_request:
13 | branches: ["master"]
14 | paths:
15 | - "**.js" # Any JS file
16 | - "package.json"
17 | - "package-lock.json"
18 | - ".github/workflows/bun.yml" # This action
19 |
20 | permissions:
21 | contents: read
22 |
23 | jobs:
24 | build:
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Setup repo
29 | uses: actions/checkout@v3
30 |
31 | - uses: oven-sh/setup-bun@v2
32 | with:
33 | bun-version: 1.1.21
34 |
35 | - name: Install Dependencies
36 | run: bun install
37 |
38 | - name: Compile
39 | run: bun build bin/gamedig.js --target=bun
40 |
--------------------------------------------------------------------------------
/.github/workflows/deno.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Deno
3 |
4 | on:
5 | push:
6 | branches: ["master"]
7 | paths:
8 | - "**.js" # Any JS file
9 | - "package.json"
10 | - "package-lock.json"
11 | - ".github/workflows/deno.yml" # This action
12 | pull_request:
13 | branches: ["master"]
14 | paths:
15 | - "**.js" # Any JS file
16 | - "package.json"
17 | - "package-lock.json"
18 | - ".github/workflows/deno.yml" # This action
19 |
20 | permissions:
21 | contents: read
22 |
23 | jobs:
24 | build:
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Setup repo
29 | uses: actions/checkout@v3
30 |
31 | - name: Setup Deno
32 | # uses: denoland/setup-deno@v1
33 | uses: denoland/setup-deno@61fe2df320078202e33d7d5ad347e7dcfa0e8f31 # v1.1.2
34 | with:
35 | deno-version: v1.39.2
36 |
37 | - name: Compile
38 | run: deno compile --allow-net bin/gamedig.js
39 |
--------------------------------------------------------------------------------
/.github/workflows/games_list.yml:
--------------------------------------------------------------------------------
1 | name: Games List Markdown Validation
2 |
3 | on:
4 | push:
5 | paths:
6 | - "lib/games.js"
7 | - "GAMES_LIST.md"
8 | - ".github/workflows/games_list.yml" # This action
9 | pull_request:
10 | paths:
11 | - "lib/games.js"
12 | - "GAMES_LIST.md"
13 | - ".github/workflows/games_list.yml" # This action
14 | workflow_dispatch:
15 |
16 | permissions:
17 | contents: read
18 |
19 | jobs:
20 | check_file:
21 |
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - uses: actions/checkout@v3
26 |
27 | - name: Use Node
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 18.x
31 |
32 | - name: Run games list generation
33 | run: node tools/generate_games_list.js
34 |
35 | - name: Check for changes
36 | run: git diff --exit-code GAMES_LIST.md
37 |
--------------------------------------------------------------------------------
/.github/workflows/id-tests.yml:
--------------------------------------------------------------------------------
1 | name: ID tests
2 |
3 | on:
4 | push:
5 | paths:
6 | - "lib/games.js"
7 | - ".github/workflows/id-tests.yml" # This action
8 | pull_request:
9 | paths:
10 | - "lib/games.js"
11 | - ".github/workflows/id-tests.yml" # This action
12 | workflow_dispatch:
13 |
14 | permissions:
15 | contents: read
16 |
17 | jobs:
18 | test:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 |
25 | - name: Use Node
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: 18.x
29 |
30 | - name: Cache rust dependencies
31 | uses: Swatinem/rust-cache@v2
32 | with:
33 | cache-on-failure: 'true'
34 |
35 | - name: Install ID tester
36 | run: cargo install --git https://github.com/gamedig/rust-gamedig.git gamedig-id-tests
37 |
38 | - name: Run ID tests
39 | run: node tools/run-id-tests.js
40 |
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Node
3 |
4 | on:
5 | push:
6 | branches: [ "master" ]
7 | paths:
8 | - "**.js" # Any JS file
9 | - "package.json"
10 | - "package-lock.json"
11 | - ".github/workflows/node.yml" # This action
12 | pull_request:
13 | branches: [ "master" ]
14 | paths:
15 | - "**.js" # Any JS file
16 | - "package.json"
17 | - "package-lock.json"
18 | - ".github/workflows/node.yml" # This action
19 |
20 | permissions:
21 | contents: read
22 |
23 | jobs:
24 | build:
25 |
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | matrix:
30 | node-version: [16.20.0, 18.x, 20.x, 22.x]
31 |
32 | steps:
33 | - uses: actions/checkout@v3
34 | - name: Use Node ${{ matrix.node-version }}
35 | uses: actions/setup-node@v3
36 | with:
37 | node-version: ${{ matrix.node-version }}
38 | cache: 'npm'
39 | - run: npm ci
40 | - run: npm run build --if-present
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /npm-debug.log
3 | /*.iml
4 | /.idea
5 | /dist
6 | # Deno bin/gamedig executable
7 | gamedig
8 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamedig/node-gamedig/96c8e725d8fd7780166fde30f36c2d2bb0cc90ad/.nvmrc
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to node-GameDig
2 | This project is very open to new suggestions, additions and/or changes, these
3 | can come in the form of *discussions* about the project's state, *proposing a
4 | new feature*, *holding a few points on why we shall do X breaking change* or
5 | *submitting a fix*.
6 |
7 | ## Communications
8 | GitHub is the place we use to track bugs and discuss new features/changes,
9 | although we have a [Discord](https://discord.gg/NVCMn3tnxH) server for the
10 | community, all bugs, suggestions and changes will be reported on GitHub
11 | alongside with their backing points to ensure the transparency of the project's
12 | development.
13 |
14 | ## Issues
15 | Before opening an issue, check if there is an existing relevant issue first,
16 | someone might just have had your issue already, or you might find something
17 | related that could be of help.
18 |
19 | When opening a new issue, make sure to fill the issue template. They are made
20 | to make the subject to be as understandable as possible, not doing so may result
21 | in your issue not being managed right away, if you don't understand something
22 | (be it regarding your own problem/the issue template/the library), please state
23 | so.
24 |
25 | ## Development
26 | Note before contributing that everything done here is under the [MIT](https://opensource.org/license/mit/) license.
27 | ### Naming
28 | Naming is an important matter, and it shouldn't be changed unless necessary.
29 |
30 | Game **names** should be added as they appear on steam (or other storefront
31 | if not listed there) with the release year appended in brackets (except when the
32 | release year is already part of the name).
33 | If there is a mod that needs to be added (or it adds the support for server
34 | queries for the game), its name should be composed of the game name, a separating
35 | **bracket**, the mod name and the release year as specified previously
36 | (e.g. `Grand Theft Auto V - FiveM (2013)`).
37 |
38 | A game's **identification** is a lowercase alphanumeric string will and be forged
39 | following these rules:
40 | 1. Names composed of a maximum of two words (unless #4 applies) will result in an
41 | id where the words are concatenated (`Dead Cells` -> `deadcells`), acronyms in
42 | the name count as a single word (`S.T.A.L.K.E.R.` -> `stalker`).
43 | 2. Names of more than two words shall be made into an acronym made of the
44 | initial letters (`The Binding of Isaac` -> `tboi`), [hypenation composed words](https://prowritingaid.com/hyphenated-words)
45 | don't count as a single word, but of how many parts they are made of
46 | (`Dino D-Day`, 3 words, so `ddd`).
47 | 3. If a game has the exact name as a previously existing id's game
48 | (`Star Wars Battlefront 2`, the 2005 and 2017 one), append the release year to
49 | the newer id (2005 would be `swb2` (suppose we already have this one supported)
50 | and 2017 would be `swb22017`).
51 | 4. If a new id (`Day of Dragons` -> `dod`) results in an id that already exists
52 | (`Day of Defeat` -> `dod`), then the new name should ignore rule #2
53 | (`Day of Dragons` -> `dayofdragons`).
54 | 5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`).
55 | 6. Unless numbers (years included) are at the end of a name, they will be considered
56 | words. If a number is not in the first position, its entire numeric digits will be
57 | used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the
58 | number is in the first position the longhand (words: 5 -> five) representation of the
59 | number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples:
60 | `Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` ->
61 | `unrealtournament2003`.
62 | 7. If a game supports multiple protocols, multiple entries will be done for said game
63 | where the edition/protocol name (first disposable in this order) will be appended to
64 | the base game id's: `` (where the protocol id will follow all
65 | rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock
66 | which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has
67 | legacy versions, which use another protocol, an example would be the one for `1.6`,
68 | so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the
69 | entry of `minecraftlegacy16`). One more entry can be added by the base name of the
70 | game, which queries in a group said supported protocols to make generic queries
71 | easier and disposable.
72 | 8. If its actually about a mod that adds the ability for queries to be performed,
73 | process only the mod name.
74 |
75 | ### Priorities
76 | Game suggestions will be prioritized by maintainers based on whether the game
77 | uses a protocol already implemented in the library (games that use already
78 | implemented protocols will be added first), except in the case where a
79 | contribution is made with the protocol needed to implement the game.
80 |
81 | The same goes for protocols, if 2 were to be requested, the one implemented in
82 | the most games will be prioritized.
83 |
84 | ### Releases
85 | Currently, there is no exact release schedule.
86 | We use following versioning: MAJOR.MINOR.PATCH
87 |
88 | Whereas:
89 | MAJOR: Brings incompatible API changes.
90 | MINOR: Adding functionality in a backward compatible manner.
91 | PATCH: Bug fixes, games support, docs, dependencies patches.
92 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) node-gamedig developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/MIGRATE_IDS.md:
--------------------------------------------------------------------------------
1 | # Migrating game ids from v4 to v5
2 |
3 | **Tip**: Checkout the [changelog](CHANGELOG.md) file to see all changes.
4 |
5 | ## Game IDs
6 | The naming system used to determine the Game IDs have been updated in `v5` and some IDs have been changed.
7 | This means some ids will not work as they could have been changed to something else, see the table below to check which one is which.
8 | (*Don't see your id here? That means it stayed the same.*)
9 |
10 | **Note**: If you are heavily using the old ids, just pass the `checkOldIDs` as true in options (or `--checkOldIDs` via the CLI) to also use them.
11 | **Warning**: We strongly recommend that you update your game id, as these older IDs will eventually not be supported anymore and will be removed.
12 |
13 | ### Old IDs Table
14 | | v4 | | v5 |
15 | |:---------------------|:--|:---------------------|
16 | | americasarmypg | → | aapg |
17 | | 7d2d | → | sdtd |
18 | | americasarmypg | → | aapg |
19 | | as | → | actionsource |
20 | | ageofchivalry | → | aoc |
21 | | arkse | → | ase |
22 | | arcasimracing | → | asr08 |
23 | | arma | → | aaa |
24 | | arma2oa | → | a2oa |
25 | | armacwa | → | acwa |
26 | | armar | → | armaresistance |
27 | | armare | → | armareforger |
28 | | armagetron | → | armagetronadvanced |
29 | | bat1944 | → | battalion1944 |
30 | | bf1942 | → | battlefield1942 |
31 | | bfv | → | battlefieldvietnam |
32 | | bf2 | → | battlefield2 |
33 | | bf2142 | → | battlefield2142 |
34 | | bfbc2 | → | bbc2 |
35 | | bf3 | → | battlefield3 |
36 | | bf4 | → | battlefield4 |
37 | | bfh | → | battlefieldhardline |
38 | | bd | → | basedefense |
39 | | bs | → | bladesymphony |
40 | | buildandshoot | → | bas |
41 | | cod4 | → | cod4mw |
42 | | callofjuarez | → | coj |
43 | | chivalry | → | cmw |
44 | | commandos3 | → | c3db |
45 | | cacrenegade | → | cacr |
46 | | contactjack | → | contractjack |
47 | | cs15 | → | counterstrike15 |
48 | | cs16 | → | counterstrike16 |
49 | | cs2 | → | counterstrike2 |
50 | | crossracing | → | crce |
51 | | darkesthour | → | dhe4445 |
52 | | daysofwar | → | dow |
53 | | deadlydozenpt | → | ddpt |
54 | | dh2005 | → | deerhunter2005 |
55 | | dinodday | → | ddd |
56 | | dirttrackracing2 | → | dtr2 |
57 | | dmc | → | deathmatchclassic |
58 | | dnl | → | dal |
59 | | drakan | → | dootf |
60 | | dys | → | dystopia |
61 | | em | → | empiresmod |
62 | | empyrion | → | egs |
63 | | f12002 | → | formulaone2002 |
64 | | flashpointresistance | → | ofr |
65 | | fivem | → | gta5f |
66 | | forrest | → | theforrest |
67 | | graw | → | tcgraw |
68 | | graw2 | → | tcgraw2 |
69 | | giantscitizenkabuto | → | gck |
70 | | ges | → | goldeneyesource |
71 | | gore | → | gus |
72 | | hldm | → | hld |
73 | | hldms | → | hlds |
74 | | hlopfor | → | hlof |
75 | | hl2dm | → | hl2d |
76 | | hidden | → | thehidden |
77 | | had2 | → | hiddendangerous2 |
78 | | igi2 | → | i2cs |
79 | | il2 | → | il2sturmovik |
80 | | insurgencymic | → | imic |
81 | | isle | → | theisle |
82 | | jamesbondnightfire | → | jb007n |
83 | | jc2mp | → | jc2m |
84 | | jc3mp | → | jc3m |
85 | | kingpin | → | kloc |
86 | | kisspc | → | kpctnc |
87 | | kspdmp | → | kspd |
88 | | kzmod | → | kreedzclimbing |
89 | | left4dead | → | l4d |
90 | | left4dead2 | → | l4d2 |
91 | | m2mp | → | m2m |
92 | | mohsh | → | mohaas |
93 | | mohbt | → | mohaab |
94 | | mohab | → | moha |
95 | | moh2010 | → | moh |
96 | | mohwf | → | mohw |
97 | | minecraftbe | → | mbe |
98 | | mtavc | → | gtavcmta |
99 | | mtasa | → | gtasamta |
100 | | ns | → | naturalselection |
101 | | ns2 | → | naturalselection2 |
102 | | nwn | → | neverwinternights |
103 | | nwn2 | → | neverwinternights2 |
104 | | nolf | → | tonolf |
105 | | nolf2 | → | nolf2asihw |
106 | | pvkii | → | pvak2 |
107 | | ps | → | postscriptum |
108 | | primalcarnage | → | pce |
109 | | pc | → | projectcars |
110 | | pc2 | → | projectcars2 |
111 | | prbf2 | → | prb2 |
112 | | przomboid | → | projectzomboid |
113 | | quake1 | → | quake |
114 | | quake3 | → | q3a |
115 | | ragdollkungfu | → | rdkf |
116 | | r6 | → | rainbowsix |
117 | | r6roguespear | → | rs2rs |
118 | | r6ravenshield | → | rs3rs |
119 | | redorchestraost | → | roo4145 |
120 | | redm | → | rdr2r |
121 | | riseofnations | → | ron |
122 | | rs2 | → | rs2v |
123 | | samp | → | gtasam |
124 | | saomp | → | gtasao |
125 | | savage2 | → | s2ats |
126 | | ss | → | serioussam |
127 | | ss2 | → | serioussam2 |
128 | | ship | → | theship |
129 | | sinep | → | sinepisodes |
130 | | sonsoftheforest | → | sotf |
131 | | swbf | → | swb |
132 | | swbf2 | → | swb2 |
133 | | swjk | → | swjkja |
134 | | swjk2 | → | swjk2jo |
135 | | takeonhelicopters | → | toh |
136 | | tf2 | → | teamfortress2 |
137 | | terraria | → | terrariatshock |
138 | | tribes1 | → | t1s |
139 | | ut | → | unrealtournament |
140 | | ut2003 | → | unrealtournament2003 |
141 | | ut2004 | → | unrealtournament2004 |
142 | | ut3 | → | unrealtournament3 |
143 | | v8supercar | → | v8sc |
144 | | vcmp | → | vcm |
145 | | vs | → | vampireslayer |
146 | | wheeloftime | → | wot |
147 | | wolfenstein2009 | → | wolfenstein |
148 | | wolfensteinet | → | wet |
149 | | wurm | → | wurmunlimited |
150 |
--------------------------------------------------------------------------------
/bin/gamedig.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import * as process from 'node:process'
4 |
5 | import Minimist from 'minimist'
6 | import { GameDig } from './../lib/index.js'
7 |
8 | const argv = Minimist(process.argv.slice(2), {
9 | boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules', 'requestPlayers', 'requestRulesRequired', 'requestPlayersRequired', 'stripColors', 'portCache', 'noBreadthOrder', 'checkOldIDs', 'rejectUnauthorized'],
10 | string: ['guildId', 'serverId', 'listenUdpPort', 'ipFamily', 'token'],
11 | default: {
12 | stripColors: true,
13 | portCache: true,
14 | requestPlayers: true
15 | }
16 | })
17 |
18 | const options = {}
19 | for (const key of Object.keys(argv)) {
20 | const value = argv[key]
21 |
22 | if (key === '_' || key.charAt(0) === '$') { continue }
23 |
24 | options[key] = value
25 | }
26 |
27 | // Separate host and port
28 | if (argv._.length >= 1) {
29 | const target = argv._[0]
30 | const split = target.split(':')
31 | options.host = split[0]
32 | if (split.length > 1) {
33 | options.port = split[1]
34 | }
35 | }
36 |
37 | const { debug, pretty } = options
38 |
39 | const printOnPretty = (object) => {
40 | if (!!pretty || debug) {
41 | console.log(JSON.stringify(object, null, ' '))
42 | } else {
43 | console.log(JSON.stringify(object))
44 | }
45 | }
46 |
47 | const gamedig = new GameDig(options)
48 | gamedig.query(options)
49 | .then(printOnPretty)
50 | .catch((error) => {
51 | if (debug) {
52 | if (error instanceof Error) {
53 | console.log(error.stack)
54 | } else {
55 | console.log(error)
56 | }
57 | } else {
58 | if (error instanceof Error) {
59 | error = error.message
60 | }
61 |
62 | printOnPretty({ error })
63 | }
64 | })
65 |
--------------------------------------------------------------------------------
/examples/import.mjs:
--------------------------------------------------------------------------------
1 | import { GameDig } from '../lib/index.js'
2 | // Instead of '../lib/index.js' you would have here 'gamedig'.
3 |
4 | GameDig.query({
5 | type: 'minecraft',
6 | host: 'mc.hypixel.net',
7 | port: 25565, // lets us explicitly specify the query port of this server
8 | givenPortOnly: true // the library will attempt multiple ports in order to ensure success, to avoid this pass this option
9 | }).then((state) => {
10 | console.log(state)
11 | }).catch((error) => {
12 | console.log(`Server is offline, error: ${error}`)
13 | })
14 |
--------------------------------------------------------------------------------
/examples/require.cjs:
--------------------------------------------------------------------------------
1 | const { GameDig } = require('../dist/index.cjs')
2 | // Instead of '../dist/index.cjs' you would have here 'gamedig'.
3 |
4 | GameDig.query({
5 | type: 'minecraft',
6 | host: 'mc.hypixel.net',
7 | port: 25565, // lets us explicitly specify the query port of this server
8 | givenPortOnly: true // the library will attempt multiple ports in order to ensure success, to avoid this pass this option
9 | }).then((state) => {
10 | console.log(state)
11 | }).catch((error) => {
12 | console.log(`Server is offline, error: ${error}`)
13 | })
14 |
--------------------------------------------------------------------------------
/lib/DnsResolver.js:
--------------------------------------------------------------------------------
1 | import dns from 'node:dns'
2 | import { isIP } from 'node:net'
3 | import { domainToASCII } from 'node:url'
4 |
5 | export default class DnsResolver {
6 | /**
7 | * @param {Logger} logger
8 | */
9 | constructor (logger) {
10 | this.logger = logger
11 | }
12 |
13 | /**
14 | * Resolve a host name to its IP, if the given host name is already
15 | * an IP address no request is made.
16 | *
17 | * If a srvRecordPrefix is provided a SRV request will be made and the
18 | * port returned will be included in the output.
19 | * @param {string} host
20 | * @param {number} ipFamily
21 | * @param {string=} srvRecordPrefix
22 | * @returns {Promise<{address:string, port:number=}>}
23 | */
24 | async resolve (host, ipFamily, srvRecordPrefix) {
25 | this.logger.debug('DNS Lookup: ' + host)
26 |
27 | // Check if host is IPv4 or IPv6
28 | if (isIP(host) === 4 || isIP(host) === 6) {
29 | this.logger.debug('Raw IP Address: ' + host)
30 | return { address: host }
31 | }
32 |
33 | const asciiForm = domainToASCII(host)
34 | if (!asciiForm) {
35 | throw new Error('Invalid domain')
36 | }
37 |
38 | if (asciiForm !== host) {
39 | this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
40 | host = asciiForm
41 | }
42 |
43 | if (srvRecordPrefix) {
44 | this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
45 | let records
46 | try {
47 | records = await dns.promises.resolve(srvRecordPrefix + '.' + host, 'SRV')
48 | if (records.length >= 1) {
49 | this.logger.debug('Found SRV Records: ', records)
50 | const record = records[0]
51 | const srvPort = record.port
52 | const srvHost = record.name
53 | if (srvHost === host) {
54 | throw new Error('Loop in DNS SRV records')
55 | }
56 | return {
57 | port: srvPort,
58 | ...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
59 | }
60 | }
61 | this.logger.debug('No SRV Record')
62 | } catch (e) {
63 | this.logger.debug(e)
64 | }
65 | }
66 |
67 | this.logger.debug('Standard Resolve: ' + host)
68 | const dnsResult = await dns.promises.lookup(host, ipFamily)
69 | // For some reason, this sometimes returns a string address rather than an object.
70 | // I haven't been able to reproduce, but it's been reported on the issue tracker.
71 | let address
72 | if (typeof dnsResult === 'string') {
73 | address = dnsResult
74 | } else {
75 | address = dnsResult.address
76 | }
77 | this.logger.debug('Found address: ' + address)
78 | return { address }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/GlobalUdpSocket.js:
--------------------------------------------------------------------------------
1 | import { createSocket } from 'node:dgram'
2 | import { debugDump } from './HexUtil.js'
3 | import { promisify } from 'node:util'
4 | import Logger from './Logger.js'
5 |
6 | export default class GlobalUdpSocket {
7 | constructor ({ port }) {
8 | this.socket = null
9 | this.callbacks = new Set()
10 | this.debuggingCallbacks = new Set()
11 | this.logger = new Logger()
12 | this.port = port
13 | }
14 |
15 | async _getSocket () {
16 | if (!this.socket) {
17 | const udpSocket = createSocket({
18 | type: 'udp4',
19 | reuseAddr: true
20 | })
21 | udpSocket.unref()
22 | udpSocket.on('message', (buffer, rinfo) => {
23 | const fromAddress = rinfo.address
24 | const fromPort = rinfo.port
25 | this.logger.debug(log => {
26 | log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
27 | log(debugDump(buffer))
28 | })
29 | for (const callback of this.callbacks) {
30 | callback(fromAddress, fromPort, buffer)
31 | }
32 | })
33 | udpSocket.on('error', e => {
34 | this.logger.debug('UDP ERROR:', e)
35 | })
36 | await promisify(udpSocket.bind).bind(udpSocket)(this.port)
37 | this.port = udpSocket.address().port
38 | this.socket = udpSocket
39 | }
40 | return this.socket
41 | }
42 |
43 | async send (buffer, address, port, debug) {
44 | const socket = await this._getSocket()
45 |
46 | if (debug) {
47 | this.logger._print(log => {
48 | log(address + ':' + port + ' UDP(' + this.port + ')-->')
49 | log(debugDump(buffer))
50 | })
51 | }
52 |
53 | await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
54 | }
55 |
56 | addCallback (callback, debug) {
57 | this.callbacks.add(callback)
58 | if (debug) {
59 | this.debuggingCallbacks.add(callback)
60 | this.logger.debugEnabled = true
61 | }
62 | }
63 |
64 | removeCallback (callback) {
65 | this.callbacks.delete(callback)
66 | this.debuggingCallbacks.delete(callback)
67 | this.logger.debugEnabled = this.debuggingCallbacks.size > 0
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/HexUtil.js:
--------------------------------------------------------------------------------
1 | /** @param {Buffer} buffer */
2 | export const debugDump = (buffer) => {
3 | let hexLine = ''
4 | let chrLine = ''
5 | let out = ''
6 | out += 'Buffer length: ' + buffer.length + ' bytes\n'
7 | for (let i = 0; i < buffer.length; i++) {
8 | const sliced = buffer.slice(i, i + 1)
9 | hexLine += sliced.toString('hex') + ' '
10 | let chr = sliced.toString()
11 | if (chr < ' ' || chr > '~') chr = ' '
12 | chrLine += chr + ' '
13 | if (hexLine.length > 60 || i === buffer.length - 1) {
14 | out += hexLine + '\n'
15 | out += chrLine + '\n'
16 | hexLine = chrLine = ''
17 | }
18 | }
19 | return out
20 | }
21 |
--------------------------------------------------------------------------------
/lib/Logger.js:
--------------------------------------------------------------------------------
1 | import { debugDump } from './HexUtil.js'
2 | import { Buffer } from 'node:buffer'
3 |
4 | export default class Logger {
5 | constructor () {
6 | this.debugEnabled = false
7 | this.prefix = ''
8 | }
9 |
10 | debug (...args) {
11 | if (!this.debugEnabled) return
12 | this._print(...args)
13 | }
14 |
15 | _print (...args) {
16 | try {
17 | const strings = this._convertArgsToStrings(...args)
18 | if (strings.length) {
19 | if (this.prefix) {
20 | strings.unshift(this.prefix)
21 | }
22 | console.log(...strings)
23 | }
24 | } catch (e) {
25 | console.log('Error while logging: ' + e)
26 | }
27 | }
28 |
29 | _convertArgsToStrings (...args) {
30 | const out = []
31 | for (const arg of args) {
32 | if (arg instanceof Error) {
33 | out.push(arg.stack)
34 | } else if (arg instanceof Buffer) {
35 | out.push(debugDump(arg))
36 | } else if (typeof arg === 'function') {
37 | const result = arg.call(undefined, (...args) => this._print(...args))
38 | if (result !== undefined) out.push(...this._convertArgsToStrings(result))
39 | } else {
40 | out.push(arg)
41 | }
42 | }
43 | return out
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/Promises.js:
--------------------------------------------------------------------------------
1 | export default class Promises {
2 | static createTimeout (timeoutMs, timeoutMsg) {
3 | let cancel = null
4 | const wrapped = new Promise((resolve, reject) => {
5 | const timeout = setTimeout(
6 | () => {
7 | reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
8 | },
9 | timeoutMs
10 | )
11 | cancel = () => {
12 | clearTimeout(timeout)
13 | }
14 | })
15 | wrapped.cancel = cancel
16 | return wrapped
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/ProtocolResolver.js:
--------------------------------------------------------------------------------
1 | import * as protocols from '../protocols/index.js'
2 |
3 | export const getProtocol = (protocolId) => {
4 | if (!(protocolId in protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
5 |
6 | return new protocols[protocolId]()
7 | }
8 |
--------------------------------------------------------------------------------
/lib/QueryRunner.js:
--------------------------------------------------------------------------------
1 | import { lookup } from './game-resolver.js'
2 | import { getProtocol } from './ProtocolResolver.js'
3 | import GlobalUdpSocket from './GlobalUdpSocket.js'
4 |
5 | const defaultOptions = {
6 | socketTimeout: 2000,
7 | attemptTimeout: 10000,
8 | maxRetries: 1,
9 | stripColors: true,
10 | portCache: true,
11 | noBreadthOrder: false,
12 | ipFamily: 0,
13 | requestPlayers: true
14 | }
15 |
16 | export default class QueryRunner {
17 | constructor (runnerOpts = {}) {
18 | this.udpSocket = new GlobalUdpSocket({
19 | port: runnerOpts.listenUdpPort
20 | })
21 | this.portCache = {}
22 | }
23 |
24 | async run (userOptions) {
25 | for (const key of Object.keys(userOptions)) {
26 | const value = userOptions[key]
27 | if (['port', 'ipFamily'].includes(key)) {
28 | userOptions[key] = parseInt(value)
29 | }
30 | }
31 |
32 | const {
33 | port_query: gameQueryPort,
34 | port_query_offset: gameQueryPortOffset,
35 | ...gameOptions
36 | } = lookup(userOptions)
37 | const attempts = []
38 |
39 | const optionsCollection = {
40 | ...defaultOptions,
41 | ...gameOptions,
42 | ...userOptions
43 | }
44 |
45 | const addAttemptWithPort = port => {
46 | attempts.push({
47 | ...optionsCollection,
48 | port
49 | })
50 | }
51 |
52 | let portOffsetArray = gameQueryPortOffset
53 | if (!Array.isArray(portOffsetArray)) {
54 | gameQueryPortOffset ? portOffsetArray = [gameQueryPortOffset] : portOffsetArray = [0]
55 | }
56 |
57 | const cachedPort = this.portCache[`${userOptions.address}:${userOptions.port}`]
58 |
59 | // Use any cached port if port caching is enabled and user is not explicitly enforcing their given port
60 | if (cachedPort && optionsCollection.portCache && !userOptions.givenPortOnly) {
61 | addAttemptWithPort(cachedPort)
62 | }
63 |
64 | if (userOptions.port) {
65 | if (!userOptions.givenPortOnly) {
66 | portOffsetArray.forEach((portOffset) => { addAttemptWithPort(userOptions.port + portOffset) })
67 | if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
68 | }
69 |
70 | attempts.push(optionsCollection)
71 | } else if (gameQueryPort) {
72 | addAttemptWithPort(gameQueryPort)
73 | } else if (gameOptions.port) {
74 | portOffsetArray.forEach((portOffset) => { addAttemptWithPort(gameOptions.port + portOffset) })
75 | } else {
76 | // Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
77 | attempts.push(optionsCollection)
78 | }
79 |
80 | const numRetries = userOptions.maxRetries || gameOptions.maxRetries || defaultOptions.maxRetries
81 |
82 | const retries = Array.from({ length: numRetries }, (x, i) => i)
83 |
84 | const attemptOrder = []
85 | if (optionsCollection.noBreadthOrder) {
86 | attempts.forEach(attempt => retries.forEach(retry => attemptOrder.push({ attempt, retry })))
87 | } else {
88 | retries.forEach(retry => attempts.forEach(attempt => attemptOrder.push({ attempt, retry })))
89 | }
90 |
91 | let attemptNum = 0
92 | const errors = []
93 | for (const { attempt, retry } of attemptOrder) {
94 | attemptNum++
95 |
96 | try {
97 | const response = await this._attempt(attempt)
98 | if (attempt.portCache) {
99 | this.portCache[`${userOptions.address}:${userOptions.port}`] = attempt.port
100 | }
101 | return response
102 | } catch (e) {
103 | e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
104 | errors.push(e)
105 | }
106 | }
107 |
108 | const err = new Error('Failed all ' + errors.length + ' attempts')
109 | for (const e of errors) {
110 | err.stack += '\n' + e.stack
111 | }
112 |
113 | throw err
114 | }
115 |
116 | async _attempt (options) {
117 | const core = getProtocol(options.protocol)
118 | core.options = options
119 | core.udpSocket = this.udpSocket
120 | return await core.runOnceSafe()
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/Results.js:
--------------------------------------------------------------------------------
1 | export class Player {
2 | name = ''
3 | raw = {}
4 |
5 | constructor (data) {
6 | if (typeof data === 'string') {
7 | this.name = data
8 | } else {
9 | const { name, ...raw } = data
10 | if (name) this.name = name
11 | if (raw) this.raw = raw
12 | }
13 | }
14 | }
15 |
16 | export class Players extends Array {
17 | push (data) {
18 | super.push(new Player(data))
19 | }
20 | }
21 |
22 | export class Results {
23 | name = ''
24 | map = ''
25 | password = false
26 |
27 | raw = {}
28 | version = ''
29 |
30 | maxplayers = 0
31 | numplayers = 0
32 | players = new Players()
33 | bots = new Players()
34 |
35 | queryPort = 0
36 | }
37 |
--------------------------------------------------------------------------------
/lib/game-resolver.js:
--------------------------------------------------------------------------------
1 | import { games } from './games.js'
2 |
3 | export const lookup = (options) => {
4 | const type = options.type
5 |
6 | if (!type) { throw Error('No game specified') }
7 |
8 | if (type.startsWith('protocol-')) {
9 | return {
10 | protocol: type.substring(9)
11 | }
12 | }
13 |
14 | let game = games[type]
15 |
16 | if (options.checkOldIDs) {
17 | Object.keys(games).forEach((id) => {
18 | if (games[id]?.extra?.old_id === type) {
19 | game = games[id]
20 | }
21 | })
22 | }
23 |
24 | if (!game) { throw Error('Invalid game: ' + type) }
25 |
26 | return game.options
27 | }
28 |
--------------------------------------------------------------------------------
/lib/gamedig.js:
--------------------------------------------------------------------------------
1 | import QueryRunner from './QueryRunner.js'
2 |
3 | let singleton = null
4 |
5 | export class GameDig {
6 | constructor (runnerOpts) {
7 | this.queryRunner = new QueryRunner(runnerOpts)
8 | }
9 |
10 | async query (userOptions) {
11 | return await this.queryRunner.run(userOptions)
12 | }
13 |
14 | static getInstance () {
15 | if (!singleton) { singleton = new GameDig() }
16 |
17 | return singleton
18 | }
19 |
20 | static async query (...args) {
21 | return await GameDig.getInstance().query(...args)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import { GameDig } from './gamedig.js'
2 | import { games } from './games.js'
3 | import * as protocols from '../protocols/index.js'
4 |
5 | export { GameDig, games, protocols }
6 |
--------------------------------------------------------------------------------
/lib/reader.js:
--------------------------------------------------------------------------------
1 | import Iconv from 'iconv-lite'
2 | import Long from 'long'
3 | import { Buffer } from 'node:buffer'
4 | import Varint from 'varint'
5 |
6 | function readUInt64BE (buffer, offset) {
7 | const high = buffer.readUInt32BE(offset)
8 | const low = buffer.readUInt32BE(offset + 4)
9 | return new Long(low, high, true)
10 | }
11 | function readUInt64LE (buffer, offset) {
12 | const low = buffer.readUInt32LE(offset)
13 | const high = buffer.readUInt32LE(offset + 4)
14 | return new Long(low, high, true)
15 | }
16 |
17 | export default class Reader {
18 | /**
19 | * @param {Core} query
20 | * @param {Buffer} buffer
21 | **/
22 | constructor (query, buffer) {
23 | this.defaultEncoding = query.options.encoding || query.encoding
24 | this.defaultDelimiter = query.delimiter
25 | this.defaultByteOrder = query.byteorder
26 | this.buffer = buffer
27 | this.i = 0
28 | }
29 |
30 | setOffset (offset) {
31 | this.i = offset
32 | }
33 |
34 | offset () {
35 | return this.i
36 | }
37 |
38 | skip (i) {
39 | this.i += i
40 | }
41 |
42 | pascalString (bytesForSize, adjustment = 0) {
43 | const length = this.uint(bytesForSize) + adjustment
44 | return this.string(length)
45 | }
46 |
47 | string (arg) {
48 | let encoding = this.defaultEncoding
49 | let length = null
50 | let delimiter = this.defaultDelimiter
51 |
52 | if (typeof arg === 'string') delimiter = arg
53 | else if (typeof arg === 'number') length = arg
54 | else if (typeof arg === 'object') {
55 | if ('encoding' in arg) encoding = arg.encoding
56 | if ('length' in arg) length = arg.length
57 | if ('delimiter' in arg) delimiter = arg.delimiter
58 | }
59 |
60 | if (encoding === 'latin1') encoding = 'win1252'
61 |
62 | const start = this.i
63 | let end = start
64 | if (length === null) {
65 | // terminated by the delimiter
66 | let delim = delimiter
67 | if (typeof delim === 'string') delim = delim.charCodeAt(0)
68 | while (true) {
69 | if (end >= this.buffer.length) {
70 | end = this.buffer.length
71 | break
72 | }
73 | if (this.buffer.readUInt8(end) === delim) break
74 | end++
75 | }
76 | this.i = end + 1
77 | } else if (length <= 0) {
78 | return ''
79 | } else {
80 | end = start + length
81 | if (end >= this.buffer.length) {
82 | end = this.buffer.length
83 | }
84 | this.i = end
85 | }
86 |
87 | const slice = this.buffer.slice(start, end)
88 | const enc = encoding
89 | if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
90 | return slice.toString(enc)
91 | } else {
92 | return Iconv.decode(slice, enc)
93 | }
94 | }
95 |
96 | int (bytes) {
97 | let r = 0
98 | if (this.remaining() >= bytes) {
99 | if (this.defaultByteOrder === 'be') {
100 | if (bytes === 1) r = this.buffer.readInt8(this.i)
101 | else if (bytes === 2) r = this.buffer.readInt16BE(this.i)
102 | else if (bytes === 4) r = this.buffer.readInt32BE(this.i)
103 | } else {
104 | if (bytes === 1) r = this.buffer.readInt8(this.i)
105 | else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
106 | else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
107 | }
108 | }
109 | this.i += bytes
110 | return r
111 | }
112 |
113 | /** @returns {number} */
114 | uint (bytes) {
115 | let r = 0
116 | if (this.remaining() >= bytes) {
117 | if (this.defaultByteOrder === 'be') {
118 | if (bytes === 1) r = this.buffer.readUInt8(this.i)
119 | else if (bytes === 2) r = this.buffer.readUInt16BE(this.i)
120 | else if (bytes === 4) r = this.buffer.readUInt32BE(this.i)
121 | else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
122 | } else {
123 | if (bytes === 1) r = this.buffer.readUInt8(this.i)
124 | else if (bytes === 2) r = this.buffer.readUInt16LE(this.i)
125 | else if (bytes === 4) r = this.buffer.readUInt32LE(this.i)
126 | else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
127 | }
128 | }
129 | this.i += bytes
130 | return r
131 | }
132 |
133 | float () {
134 | let r = 0
135 | if (this.remaining() >= 4) {
136 | if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
137 | else r = this.buffer.readFloatLE(this.i)
138 | }
139 | this.i += 4
140 | return r
141 | }
142 |
143 | varint () {
144 | const out = Varint.decode(this.buffer, this.i)
145 | this.i += Varint.decode.bytes
146 | return out
147 | }
148 |
149 | /** @returns Buffer */
150 | part (bytes) {
151 | let r
152 | if (this.remaining() >= bytes) {
153 | r = this.buffer.slice(this.i, this.i + bytes)
154 | } else {
155 | r = Buffer.from([])
156 | }
157 | this.i += bytes
158 | return r
159 | }
160 |
161 | remaining () {
162 | return this.buffer.length - this.i
163 | }
164 |
165 | rest () {
166 | return this.buffer.slice(this.i)
167 | }
168 |
169 | done () {
170 | return this.i >= this.buffer.length
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gamedig",
3 | "description": "Query for the status of any game server in Node.JS",
4 | "scripts": {
5 | "lint:check": "eslint .",
6 | "lint:fix": "eslint --fix .",
7 | "prepare": "npm run build",
8 | "build": "node tools/esbuild.js"
9 | },
10 | "keywords": [
11 | "srcds",
12 | "query",
13 | "game",
14 | "utility",
15 | "util",
16 | "server",
17 | "gameserver",
18 | "game-server-query",
19 | "game server query",
20 | "server query",
21 | "game server",
22 | "gameserverquery",
23 | "serverquery",
24 | "terraria",
25 | "counter strike",
26 | "csgo",
27 | "minecraft"
28 | ],
29 | "type": "module",
30 | "exports": {
31 | "import": "./lib/index.js",
32 | "require": "./dist/index.cjs"
33 | },
34 | "author": "GameDig Contributors",
35 | "version": "5.3.1",
36 | "repository": {
37 | "type": "git",
38 | "url": "https://github.com/gamedig/node-gamedig.git"
39 | },
40 | "bugs": {
41 | "url": "https://github.com/gamedig/node-gamedig/issues"
42 | },
43 | "license": "MIT",
44 | "engines": {
45 | "node": ">=16.20.0"
46 | },
47 | "bin": {
48 | "gamedig": "bin/gamedig.js"
49 | },
50 | "files": [
51 | "dist/index.cjs",
52 | "bin/gamedig.js",
53 | "lib/",
54 | "protocols/",
55 | "LICENSE",
56 | "GAMES_LIST.md",
57 | "README.md"
58 | ],
59 | "dependencies": {
60 | "fast-xml-parser": "5.2.5",
61 | "gbxremote": "0.2.1",
62 | "got": "13.0.0",
63 | "iconv-lite": "0.6.3",
64 | "long": "5.3.2",
65 | "minimist": "1.2.8",
66 | "seek-bzip": "2.0.0",
67 | "telnet-client": "2.2.5",
68 | "varint": "6.0.0"
69 | },
70 | "devDependencies": {
71 | "@types/node": "^16.18.58",
72 | "esbuild": "^0.19.10",
73 | "esbuild-node-externals": "^1.12.0",
74 | "eslint": "^8.49.0",
75 | "eslint-config-standard": "^17.1.0",
76 | "eslint-plugin-import": "^2.28.1",
77 | "eslint-plugin-n": "15.7.0",
78 | "eslint-plugin-promise": "^6.1.1"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/protocols/altvmp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class altvmp extends Core {
4 | constructor() {
5 | super()
6 | this.usedTcp = true
7 | }
8 |
9 | async getServerFromMasterList() {
10 | const targetID = `${this.options.host}:${this.options.port}`
11 |
12 | const results = await this.request({
13 | url: 'https://api.alt-mp.com/servers',
14 | responseType: 'json'
15 | })
16 |
17 | if (results == null) {
18 | throw new Error('Unable to retrieve master server list')
19 | }
20 |
21 | const serverInfo = results.find((server) => {
22 | // If the server uses a CDN, there could be occasional paths in the address, so we are checking for them.
23 | // If the server does not use a CDN, there will be no paths in the address and direct comparison will work.
24 | const address = server.useCdn
25 | ? server.address
26 | : server.address.replace(/(https?:\/\/)?\/?/g, '')
27 | return address === targetID
28 | })
29 |
30 | return serverInfo
31 | }
32 |
33 | async getServerById(targetID) {
34 | const serverInfo = await this.request({
35 | url: `https://api.alt-mp.com/servers/${targetID}`,
36 | responseType: 'json'
37 | })
38 |
39 | if (serverInfo == null) {
40 | throw new Error('Unable to retrieve server info')
41 | }
42 |
43 | return serverInfo
44 | }
45 |
46 | async run(state) {
47 | const serverInfo = this.options.serverId
48 | ? await this.getServerById(this.options.serverId)
49 | : await this.getServerFromMasterList()
50 |
51 | if (!serverInfo) {
52 | throw new Error('No server info was found.')
53 | }
54 |
55 | state.name = serverInfo.name
56 | state.numplayers = serverInfo.playersCount
57 | state.maxplayers = serverInfo.maxPlayersCount
58 | state.password = serverInfo.passworded
59 | state.version = serverInfo.version
60 | state.connect = `altv://${serverInfo.address}`
61 | state.raw = serverInfo
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/protocols/armagetron.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class armagetron extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.byteorder = 'be'
8 | }
9 |
10 | async run (state) {
11 | const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
12 |
13 | const buffer = await this.udpSend(b, b => b)
14 | const reader = this.reader(buffer)
15 |
16 | reader.skip(6)
17 |
18 | state.gamePort = this.readUInt(reader)
19 | state.raw.hostname = this.readString(reader)
20 | state.name = this.stripColorCodes(this.readString(reader))
21 | state.numplayers = this.readUInt(reader)
22 | state.raw.versionmin = this.readUInt(reader)
23 | state.raw.versionmax = this.readUInt(reader)
24 | state.version = this.readString(reader)
25 | state.maxplayers = this.readUInt(reader)
26 |
27 | const players = this.readString(reader)
28 | const list = players.split('\n')
29 | for (const name of list) {
30 | if (!name) continue
31 | state.players.push({
32 | name: this.stripColorCodes(name)
33 | })
34 | }
35 |
36 | state.raw.options = this.stripColorCodes(this.readString(reader))
37 | state.raw.uri = this.readString(reader)
38 | state.raw.globalids = this.readString(reader)
39 | }
40 |
41 | readUInt (reader) {
42 | const a = reader.uint(2)
43 | const b = reader.uint(2)
44 | return (b << 16) + a
45 | }
46 |
47 | readString (reader) {
48 | const len = reader.uint(2)
49 | if (!len) return ''
50 |
51 | let out = ''
52 | for (let i = 0; i < len; i += 2) {
53 | const hi = reader.uint(1)
54 | const lo = reader.uint(1)
55 | if (i + 1 < len) out += String.fromCharCode(lo)
56 | if (i + 2 < len) out += String.fromCharCode(hi)
57 | }
58 |
59 | return out
60 | }
61 |
62 | stripColorCodes (str) {
63 | return this.options.stripColors ? str.replace(/0x[0-9a-f]{6}/g, '') : str
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/protocols/asa.js:
--------------------------------------------------------------------------------
1 | import Epic from './epic.js'
2 |
3 | export default class asa extends Epic {
4 | constructor () {
5 | super()
6 |
7 | // OAuth2 credentials extracted from ARK: Survival Ascended files.
8 | this.clientId = 'xyza7891muomRmynIIHaJB9COBKkwj6n'
9 | this.clientSecret = 'PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s'
10 | this.deploymentId = 'ad9a8feffb3b4b2ca315546f038c3ae2'
11 | }
12 |
13 | async run (state) {
14 | await super.run(state)
15 | state.version = state.raw.attributes.BUILDID_s + '.' + state.raw.attributes.MINORBUILDID_s
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/protocols/ase.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class ase extends Core {
4 | async run (state) {
5 | const buffer = await this.udpSend('s', (buffer) => {
6 | const reader = this.reader(buffer)
7 | const header = reader.string(4)
8 | if (header === 'EYE1') return reader.rest()
9 | })
10 |
11 | const reader = this.reader(buffer)
12 | state.raw.gamename = this.readString(reader)
13 | state.gamePort = parseInt(this.readString(reader))
14 | state.name = this.readString(reader)
15 | state.raw.gametype = this.readString(reader)
16 | state.map = this.readString(reader)
17 | state.version = this.readString(reader)
18 | state.password = this.readString(reader) === '1'
19 | state.numplayers = parseInt(this.readString(reader))
20 | state.maxplayers = parseInt(this.readString(reader))
21 |
22 | while (!reader.done()) {
23 | const key = this.readString(reader)
24 | if (!key) break
25 | const value = this.readString(reader)
26 | state.raw[key] = value
27 | }
28 |
29 | while (!reader.done()) {
30 | const flags = reader.uint(1)
31 | const player = {}
32 | if (flags & 1) player.name = this.readString(reader)
33 | if (flags & 2) player.team = this.readString(reader)
34 | if (flags & 4) player.skin = this.readString(reader)
35 | if (flags & 8) player.score = parseInt(this.readString(reader))
36 | if (flags & 16) player.ping = parseInt(this.readString(reader))
37 | if (flags & 32) player.time = parseInt(this.readString(reader))
38 | state.players.push(player)
39 | }
40 | }
41 |
42 | readString (reader) {
43 | return reader.pascalString(1, -1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/protocols/assettocorsa.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class assettocorsa extends Core {
4 | async run (state) {
5 | const serverInfo = await this.request({
6 | url: `http://${this.options.address}:${this.options.port}/INFO`,
7 | responseType: 'json'
8 | })
9 | const carInfo = await this.request({
10 | url: `http://${this.options.address}:${this.options.port}/JSON|${Math.floor(Math.random() * 999999999999999)}`,
11 | responseType: 'json'
12 | })
13 |
14 | if (!serverInfo || !carInfo || !carInfo.Cars) {
15 | throw new Error('Query not successful')
16 | }
17 |
18 | state.maxplayers = serverInfo.maxclients
19 | state.name = serverInfo.name
20 | state.map = serverInfo.track
21 | state.password = serverInfo.pass
22 | state.gamePort = serverInfo.port
23 | state.numplayers = serverInfo.clients
24 | state.version = serverInfo.poweredBy
25 |
26 | state.raw.carInfo = carInfo.Cars
27 | state.raw.serverInfo = serverInfo
28 |
29 | for (const car of carInfo.Cars) {
30 | if (car.IsConnected) {
31 | state.players.push({
32 | name: car.DriverName,
33 | car: car.Model,
34 | skin: car.Skin,
35 | nation: car.DriverNation,
36 | team: car.DriverTeam
37 | })
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/protocols/battlefield.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class battlefield extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | }
8 |
9 | async run (state) {
10 | await this.withTcp(async socket => {
11 | {
12 | const data = await this.query(socket, ['serverInfo'])
13 | state.name = data.shift()
14 | state.numplayers = parseInt(data.shift())
15 | state.maxplayers = parseInt(data.shift())
16 | state.raw.gametype = data.shift()
17 | state.map = data.shift()
18 | state.raw.roundsplayed = parseInt(data.shift())
19 | state.raw.roundstotal = parseInt(data.shift())
20 |
21 | const teamCount = data.shift()
22 | state.raw.teams = []
23 | for (let i = 0; i < teamCount; i++) {
24 | const tickets = parseFloat(data.shift())
25 | state.raw.teams.push({
26 | tickets
27 | })
28 | }
29 |
30 | state.raw.targetscore = parseInt(data.shift())
31 | state.raw.status = data.shift()
32 |
33 | // Seems like the fields end at random places beyond this point
34 | // depending on the server version
35 |
36 | if (data.length) state.raw.ranked = (data.shift() === 'true')
37 | if (data.length) state.raw.punkbuster = (data.shift() === 'true')
38 | if (data.length) state.password = (data.shift() === 'true')
39 | if (data.length) state.raw.uptime = parseInt(data.shift())
40 | if (data.length) state.raw.roundtime = parseInt(data.shift())
41 |
42 | const isBadCompany2 = data[0] === 'BC2'
43 | if (isBadCompany2) {
44 | if (data.length) data.shift()
45 | if (data.length) data.shift()
46 | }
47 | if (data.length) {
48 | state.raw.ip = data.shift()
49 | const split = state.raw.ip.split(':')
50 | state.gameHost = split[0]
51 | state.gamePort = split[1]
52 | } else {
53 | // best guess if the server doesn't tell us what the server port is
54 | // these are just the default game ports for different default query ports
55 | if (this.options.port === 48888) state.gamePort = 7673
56 | if (this.options.port === 22000) state.gamePort = 25200
57 | }
58 | if (data.length) state.raw.punkbusterversion = data.shift()
59 | if (data.length) state.raw.joinqueue = (data.shift() === 'true')
60 | if (data.length) state.raw.region = data.shift()
61 | if (data.length) state.raw.pingsite = data.shift()
62 | if (data.length) state.raw.country = data.shift()
63 | if (data.length) state.raw.quickmatch = (data.shift() === 'true')
64 | }
65 |
66 | {
67 | const data = await this.query(socket, ['version'])
68 | data.shift()
69 | state.version = data.shift()
70 | }
71 |
72 | {
73 | const data = await this.query(socket, ['listPlayers', 'all'])
74 | const fieldCount = parseInt(data.shift())
75 | const fields = []
76 | for (let i = 0; i < fieldCount; i++) {
77 | fields.push(data.shift())
78 | }
79 | const numplayers = data.shift()
80 | for (let i = 0; i < numplayers; i++) {
81 | const player = {}
82 | for (let key of fields) {
83 | let value = data.shift()
84 |
85 | if (key === 'teamId') key = 'team'
86 | else if (key === 'squadId') key = 'squad'
87 |
88 | if (['kills', 'deaths', 'score', 'rank', 'team', 'squad', 'ping', 'type'].includes(key)) {
89 | value = parseInt(value)
90 | }
91 |
92 | player[key] = value
93 | }
94 | state.players.push(player)
95 | }
96 | }
97 | })
98 | }
99 |
100 | async query (socket, params) {
101 | const outPacket = this.buildPacket(params)
102 | return await this.tcpSend(socket, outPacket, (data) => {
103 | const decoded = this.decodePacket(data)
104 | if (decoded) {
105 | this.logger.debug(decoded)
106 | if (decoded.shift() !== 'OK') throw new Error('Missing OK')
107 | return decoded
108 | }
109 | })
110 | }
111 |
112 | buildPacket (params) {
113 | const paramBuffers = []
114 | for (const param of params) {
115 | paramBuffers.push(Buffer.from(param, 'utf8'))
116 | }
117 |
118 | let totalLength = 12
119 | for (const paramBuffer of paramBuffers) {
120 | totalLength += paramBuffer.length + 1 + 4
121 | }
122 |
123 | const b = Buffer.alloc(totalLength)
124 | b.writeUInt32LE(0, 0)
125 | b.writeUInt32LE(totalLength, 4)
126 | b.writeUInt32LE(params.length, 8)
127 | let offset = 12
128 | for (const paramBuffer of paramBuffers) {
129 | b.writeUInt32LE(paramBuffer.length, offset); offset += 4
130 | paramBuffer.copy(b, offset); offset += paramBuffer.length
131 | b.writeUInt8(0, offset); offset += 1
132 | }
133 |
134 | return b
135 | }
136 |
137 | decodePacket (buffer) {
138 | if (buffer.length < 8) return false
139 | const reader = this.reader(buffer)
140 | const header = reader.uint(4)
141 | const totalLength = reader.uint(4)
142 | // Venice Unleashed servers "broadcast" in-game events to any connected rcon client
143 | // If we get such a non-response packet, skip it and decode any remaining data
144 | // Note: We will receive the broadcast ticket again next time the socket receives data, as data is concatenated
145 | if (!(header & 0x40000000)) {
146 | // Skip total length minus already read bytes (4 header + 4 length)
147 | reader.skip(totalLength - 8)
148 | if (reader.done()) {
149 | this.logger.debug('Skipping packet, type mismatch')
150 | return
151 | } else {
152 | return this.decodePacket(reader.rest())
153 | }
154 | }
155 | if (buffer.length < totalLength) return false
156 | this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
157 |
158 | const paramCount = reader.uint(4)
159 | const params = []
160 | for (let i = 0; i < paramCount; i++) {
161 | params.push(reader.pascalString(4))
162 | reader.uint(1) // strNull
163 | }
164 | return params
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/protocols/beammp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import beammpmaster from './beammpmaster.js'
3 |
4 | export default class beammp extends Core {
5 | async run (state) {
6 | const master = new beammpmaster()
7 | master.options = this.options
8 | const masterState = await master.runOnceSafe()
9 | const servers = masterState.raw.servers
10 | const server = servers.find(s => s.ip === this.options.address)
11 |
12 | if (!server) {
13 | throw new Error('Server not found in the master list')
14 | }
15 |
16 | state.name = server.sname.replace(/\^./g, '')
17 | state.map = server.map
18 | state.password = server.password
19 | state.numplayers = parseInt(server.players)
20 | state.maxplayers = parseInt(server.maxplayers)
21 |
22 | const players = server.playerslist.split(';')
23 | if (players[players.length - 1] === '') {
24 | players.pop()
25 | }
26 | players.forEach(player => {
27 | state.players.push({ name: player })
28 | })
29 |
30 | state.raw = server
31 | if ('version' in state.raw) state.version = state.raw.version
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/protocols/beammpmaster.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class beammpmaster extends Core {
4 | constructor () {
5 | super()
6 |
7 | // Don't use the tcp ping probing
8 | this.usedTcp = true
9 | }
10 |
11 | async run (state) {
12 | state.raw.servers = await this.request({
13 | url: 'https://backend.beammp.com/servers-info',
14 | responseType: 'json'
15 | })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/protocols/brokeprotocol.js:
--------------------------------------------------------------------------------
1 | import brokeprotocolmaster from './brokeprotocolmaster.js'
2 |
3 | /**
4 | * Implements the protocol for BROKE PROTOCOL, a Unity based game
5 | * using a custom master server
6 | */
7 | export default class brokeprotocol extends brokeprotocolmaster {
8 | constructor () {
9 | super()
10 | this.doQuerySingle = true
11 | this.requireToken = true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/protocols/buildandshoot.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | // We are doing some shenanigans here as we are trying to support the stable version and the git master version
4 | // as in the latest (0.75) releases they are mixed up.
5 | export default class buildandshoot extends Core {
6 | async run (state) {
7 | const request = await this.request({
8 | url: 'http://' + this.options.address + ':' + this.options.port + '/json',
9 | responseType: 'json'
10 | })
11 |
12 | state.name = request.serverName
13 | state.map = request.map.name
14 | state.version = request.serverVersion
15 |
16 | const bluePlayers = request.players?.blue || []
17 | const greenPlayers = request.players?.green || []
18 | let players = bluePlayers.concat(greenPlayers)
19 | if (Array.isArray(request.players)) {
20 | players = players.concat(request.players)
21 | }
22 |
23 | state.numplayers = players.length
24 | state.maxplayers = request.maxPlayers || request.players?.maxPlayers
25 |
26 | state.players = []
27 | for (const player of players) {
28 | if (typeof player === 'string') {
29 | state.players.push({
30 | name: player
31 | })
32 | } else {
33 | state.players.push({
34 | ...player,
35 | name: player.name
36 | })
37 | }
38 | }
39 |
40 | state.raw = request
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/protocols/cs2d.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class cs2d extends Core {
4 | async run (state) {
5 | const reader = await this.sendQuery(
6 | Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
7 | Buffer.from('\x01\x00\xFB\x01', 'binary')
8 | )
9 | const flags = reader.uint(1)
10 | state.raw.flags = flags
11 | state.password = this.readFlag(flags, 0)
12 | state.raw.registeredOnly = this.readFlag(flags, 1)
13 | state.raw.fogOfWar = this.readFlag(flags, 2)
14 | state.raw.friendlyFire = this.readFlag(flags, 3)
15 | state.raw.botsEnabled = this.readFlag(flags, 5)
16 | state.raw.luaScripts = this.readFlag(flags, 6)
17 | state.raw.forceLight = this.readFlag(flags, 7)
18 | state.name = this.readString(reader)
19 | state.map = this.readString(reader)
20 | state.numplayers = reader.uint(1)
21 | state.maxplayers = reader.uint(1)
22 | if (flags & 32) {
23 | state.raw.gamemode = reader.uint(1)
24 | } else {
25 | state.raw.gamemode = 0
26 | }
27 | state.raw.numbots = reader.uint(1)
28 | const flags2 = reader.uint(1)
29 | state.raw.flags2 = flags2
30 | state.raw.recoil = this.readFlag(flags2, 0)
31 | state.raw.offScreenDamage = this.readFlag(flags2, 1)
32 | state.raw.hasDownloads = this.readFlag(flags2, 2)
33 | reader.skip(2)
34 | const players = reader.uint(1)
35 | for (let i = 0; i < players; i++) {
36 | const player = {}
37 | player.id = reader.uint(1)
38 | player.name = this.readString(reader)
39 | player.team = reader.uint(1)
40 | player.score = reader.uint(4)
41 | player.deaths = reader.uint(4)
42 | state.players.push(player)
43 | }
44 | }
45 |
46 | async sendQuery (request, expectedHeader) {
47 | // Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
48 | await this.udpSend(request)
49 | await this.udpSend(request)
50 | return await this.udpSend(request, (buffer) => {
51 | const reader = this.reader(buffer)
52 | const header = reader.part(4)
53 | if (!header.equals(expectedHeader)) return
54 | return reader
55 | })
56 | }
57 |
58 | readFlag (flags, offset) {
59 | return !!(flags & (1 << offset))
60 | }
61 |
62 | readString (reader) {
63 | return reader.pascalString(1)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/protocols/dayz.js:
--------------------------------------------------------------------------------
1 | import valve from './valve.js'
2 | import { Buffer } from 'node:buffer'
3 |
4 | export default class dayz extends valve {
5 | async run (state) {
6 | if (!this.options.port) this.options.port = 27016
7 | await super.queryInfo(state)
8 | await super.queryChallenge()
9 | await super.queryPlayers(state)
10 | await this.queryRules(state)
11 |
12 | this.processQueryInfo(state)
13 | await super.cleanup(state)
14 | }
15 |
16 | async queryRules (state) {
17 | if (!this.options.requestRules) {
18 | return
19 | }
20 |
21 | const rules = {}
22 | state.raw.rules = rules
23 | const dayZPayload = []
24 |
25 | this.logger.debug('Requesting rules ...')
26 |
27 | const b = await this.sendPacket(0x56, null, 0x45, true)
28 | if (b === null && !this.options.requestRulesRequired) return // timed out - the server probably has rules disabled
29 |
30 | let dayZPayloadEnded = false
31 |
32 | const reader = this.reader(b)
33 | const num = reader.uint(2)
34 | for (let i = 0; i < num; i++) {
35 | if (!dayZPayloadEnded) {
36 | const one = reader.uint(1)
37 | const two = reader.uint(1)
38 | const three = reader.uint(1)
39 | if (one !== 0 && two !== 0 && three === 0) {
40 | while (true) {
41 | const byte = reader.uint(1)
42 | if (byte === 0) break
43 | dayZPayload.push(byte)
44 | }
45 | continue
46 | } else {
47 | reader.skip(-3)
48 | dayZPayloadEnded = true
49 | }
50 | }
51 |
52 | const key = reader.string()
53 | rules[key] = reader.string()
54 | }
55 |
56 | state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload))
57 | }
58 |
59 | processQueryInfo (state) {
60 | // DayZ embeds some of the server information inside the tags attribute
61 | if (!state.raw.tags) { return }
62 |
63 | state.raw.dlcEnabled = false
64 | state.raw.firstPerson = false
65 | state.raw.privateHive = false
66 | state.raw.external = false
67 | state.raw.official = false
68 |
69 | for (const tag of state.raw.tags) {
70 | if (tag.startsWith('lqs')) {
71 | const value = parseInt(tag.replace('lqs', ''))
72 | if (!isNaN(value)) {
73 | state.raw.queue = value
74 | }
75 | }
76 | if (tag.includes('no3rd')) {
77 | state.raw.firstPerson = true
78 | }
79 | if (tag.includes('isDLC')) {
80 | state.raw.dlcEnabled = true
81 | }
82 | if (tag.includes('privHive')) {
83 | state.raw.privateHive = true
84 | }
85 | if (tag.includes('external')) {
86 | state.raw.external = true
87 | }
88 | if (tag.includes(':')) {
89 | state.raw.time = tag
90 | }
91 | if (tag.startsWith('etm')) {
92 | const value = parseInt(tag.replace('etm', ''))
93 | if (!isNaN(value)) {
94 | state.raw.dayAcceleration = value
95 | }
96 | }
97 | if (tag.startsWith('entm')) {
98 | const value = parseInt(tag.replace('entm', ''))
99 | if (!isNaN(value)) {
100 | state.raw.nightAcceleration = value
101 | }
102 | }
103 | }
104 |
105 | if (!state.raw.external && !state.raw.privateHive) {
106 | state.raw.official = true
107 | }
108 | }
109 |
110 | readDayzMods (/** Buffer */ buffer) {
111 | if (!buffer.length) {
112 | return {}
113 | }
114 |
115 | this.logger.debug('DAYZ BUFFER')
116 | this.logger.debug(buffer)
117 |
118 | const reader = this.reader(buffer)
119 | const version = this.readDayzByte(reader)
120 | const overflow = this.readDayzByte(reader)
121 | const dlc1 = this.readDayzByte(reader)
122 | const dlc2 = this.readDayzByte(reader)
123 | this.logger.debug('version ' + version)
124 | this.logger.debug('overflow ' + overflow)
125 | this.logger.debug('dlc1 ' + dlc1)
126 | this.logger.debug('dlc2 ' + dlc2)
127 | if (dlc1) {
128 | const unknown = this.readDayzUint(reader, 4) // ?
129 | this.logger.debug('unknown ' + unknown)
130 | }
131 | if (dlc2) {
132 | const unknown = this.readDayzUint(reader, 4) // ?
133 | this.logger.debug('unknown ' + unknown)
134 | }
135 | const mods = []
136 | mods.push(...this.readDayzModsSection(reader, true))
137 | mods.push(...this.readDayzModsSection(reader, false))
138 | this.logger.debug('dayz buffer rest:', reader.rest())
139 | return mods
140 | }
141 |
142 | readDayzModsSection (/** Reader */ reader, withHeader) {
143 | const out = []
144 | const count = this.readDayzByte(reader)
145 | this.logger.debug('dayz mod section withHeader:' + withHeader + ' count:' + count)
146 | for (let i = 0; i < count; i++) {
147 | if (reader.done()) break
148 | const mod = {}
149 | if (withHeader) {
150 | mod.unknown = this.readDayzUint(reader, 4) // ?
151 |
152 | // For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes?
153 | const offset = reader.offset()
154 | const flag = this.readDayzByte(reader)
155 | if (flag !== 4) reader.setOffset(offset)
156 |
157 | mod.workshopId = this.readDayzUint(reader, 4)
158 | }
159 | mod.title = this.readDayzString(reader)
160 | this.logger.debug(mod)
161 | out.push(mod)
162 | }
163 | return out
164 | }
165 |
166 | readDayzUint (reader, bytes) {
167 | const out = []
168 | for (let i = 0; i < bytes; i++) {
169 | out.push(this.readDayzByte(reader))
170 | }
171 | const buf = Buffer.from(out)
172 | const r2 = this.reader(buf)
173 | return r2.uint(bytes)
174 | }
175 |
176 | readDayzByte (reader) {
177 | const byte = reader.uint(1)
178 | if (byte === 1) {
179 | const byte2 = reader.uint(1)
180 | if (byte2 === 1) return 1
181 | if (byte2 === 2) return 0
182 | if (byte2 === 3) return 0xff
183 | return 0 // ?
184 | }
185 | return byte
186 | }
187 |
188 | readDayzString (reader) {
189 | const length = this.readDayzByte(reader)
190 | const out = []
191 | for (let i = 0; i < length; i++) {
192 | out.push(this.readDayzByte(reader))
193 | }
194 | return Buffer.from(out).toString('utf8')
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/protocols/discord.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class discord extends Core {
4 | async run (state) {
5 | const guildId = this.options.guildId
6 | if (typeof guildId !== 'string') {
7 | throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
8 | " (It's too large of a number for javascript to store without losing precision)")
9 | }
10 | this.usedTcp = true
11 | const raw = await this.request({
12 | url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
13 | })
14 | const json = JSON.parse(raw)
15 | state.name = json.name
16 | if (json.instant_invite) {
17 | state.connect = json.instant_invite
18 | } else {
19 | state.connect = 'https://discordapp.com/channels/' + guildId
20 | }
21 | for (const member of json.members) {
22 | const { username: name, ...rest } = member
23 | state.players.push({ name, ...rest })
24 | }
25 | delete json.members
26 | state.maxplayers = 500000
27 | state.raw = json
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/protocols/doom3.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class doom3 extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | }
8 |
9 | async run (state) {
10 | const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
11 | const reader = this.reader(packet)
12 | const header = reader.uint(2)
13 | if (header !== 0xffff) return
14 | const header2 = reader.string()
15 | if (header2 !== 'infoResponse') return
16 | const challengePart1 = reader.string(4)
17 | if (challengePart1 !== 'PiNG') return
18 | // some doom3 implementations only return the first 4 bytes of the challenge
19 | const challengePart2 = reader.string(4)
20 | if (challengePart2 !== 'PoNg') reader.skip(-4)
21 | return reader.rest()
22 | })
23 |
24 | let reader = this.reader(body)
25 | const protoVersion = reader.uint(4)
26 | state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
27 | state.version = state.raw.protocolVersion
28 |
29 | // some doom implementations send us a packet size here, some don't (etqw does this)
30 | // we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
31 | reader.skip(2)
32 | const packetContainsSize = (reader.uint(2) === 0)
33 | reader.skip(-4)
34 |
35 | if (packetContainsSize) {
36 | const size = reader.uint(4)
37 | this.logger.debug('Received packet size: ' + size)
38 | }
39 |
40 | while (!reader.done()) {
41 | const key = reader.string()
42 | let value = this.stripColors(reader.string())
43 | if (key === 'si_map') {
44 | value = value.replace('maps/', '')
45 | value = value.replace('.entities', '')
46 | }
47 | if (!key) break
48 | state.raw[key] = value
49 | this.logger.debug(key + '=' + value)
50 | }
51 |
52 | const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
53 |
54 | const rest = reader.rest()
55 | let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
56 | if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
57 | if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
58 | if (!playerResult) {
59 | throw new Error('Unable to find a suitable parse strategy for player list')
60 | }
61 | let players;
62 | [players, reader] = playerResult
63 |
64 | state.numplayers = players.length
65 | for (const player of players) {
66 | if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
67 | }
68 |
69 | state.raw.osmask = reader.uint(4)
70 | if (isEtqw) {
71 | state.raw.ranked = reader.uint(1)
72 | state.raw.timeleft = reader.uint(4)
73 | state.raw.gamestate = reader.uint(1)
74 | state.raw.servertype = reader.uint(1)
75 | // 0 = regular, 1 = tv
76 | if (state.raw.servertype === 0) {
77 | state.raw.interestedClients = reader.uint(1)
78 | } else if (state.raw.servertype === 1) {
79 | state.raw.connectedClients = reader.uint(4)
80 | state.raw.maxClients = reader.uint(4)
81 | }
82 | }
83 |
84 | if (state.raw.si_name) state.name = state.raw.si_name
85 | if (state.raw.si_map) state.map = state.raw.si_map
86 | if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
87 | if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
88 | if (state.raw.si_usepass === '1') state.password = true
89 | if (state.raw.si_needPass === '1') state.password = true
90 | if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
91 | }
92 |
93 | attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
94 | this.logger.debug('starting player parse attempt:')
95 | this.logger.debug('isEtqw: ' + isEtqw)
96 | this.logger.debug('hasClanTag: ' + hasClanTag)
97 | this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
98 | this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
99 | const reader = this.reader(rest)
100 | let lastId = -1
101 | const players = []
102 | while (true) {
103 | this.logger.debug('---')
104 | if (reader.done()) {
105 | this.logger.debug('* aborting attempt, overran buffer *')
106 | return null
107 | }
108 | const player = {}
109 | player.id = reader.uint(1)
110 | this.logger.debug('id: ' + player.id)
111 | if (player.id <= lastId || player.id > 0x20) {
112 | this.logger.debug('* aborting attempt, invalid player id *')
113 | return null
114 | }
115 | lastId = player.id
116 | if (player.id === 0x20) {
117 | this.logger.debug('* player parse successful *')
118 | break
119 | }
120 | player.ping = reader.uint(2)
121 | this.logger.debug('ping: ' + player.ping)
122 | if (!isEtqw) {
123 | player.rate = reader.uint(4)
124 | this.logger.debug('rate: ' + player.rate)
125 | }
126 | player.name = this.stripColors(reader.string())
127 | this.logger.debug('name: ' + player.name)
128 | if (hasClanTag) {
129 | if (hasClanTagPos) {
130 | const clanTagPos = reader.uint(1)
131 | this.logger.debug('clanTagPos: ' + clanTagPos)
132 | }
133 | player.clantag = this.stripColors(reader.string())
134 | this.logger.debug('clan tag: ' + player.clantag)
135 | }
136 | if (hasTypeFlag) {
137 | player.typeflag = reader.uint(1)
138 | this.logger.debug('type flag: ' + player.typeflag)
139 | }
140 | players.push(player)
141 | }
142 | return [players, reader]
143 | }
144 |
145 | stripColors (str) {
146 | // uses quake 3 color codes
147 | return this.options.stripColors ? str.replace(/\^(X.{6}|.)/g, '') : str
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/protocols/eco.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class eco extends Core {
4 | async run (state) {
5 | if (!this.options.port) this.options.port = 3001
6 |
7 | const request = await this.request({
8 | url: `http://${this.options.host}:${this.options.port}/frontpage`,
9 | responseType: 'json'
10 | })
11 | const serverInfo = request.Info
12 |
13 | state.name = serverInfo.Description
14 | state.numplayers = serverInfo.OnlinePlayers
15 | state.maxplayers = serverInfo.TotalPlayers
16 | state.password = serverInfo.HasPassword
17 | state.gamePort = serverInfo.GamePort
18 | state.players = serverInfo.OnlinePlayersNames?.map(name => ({ name, raw: {} })) || []
19 | state.raw = serverInfo
20 | state.version = state.raw.Version
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/protocols/eldewrito.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class eldewrito extends Core {
4 | async run (state) {
5 | const json = await this.request({
6 | url: 'http://' + this.options.address + ':' + this.options.port,
7 | responseType: 'json'
8 | })
9 |
10 | for (const one of json.players) {
11 | state.players.push({ name: one.name, team: one.team })
12 | }
13 |
14 | state.name = json.name
15 | state.map = json.map
16 | state.maxplayers = json.maxPlayers
17 | state.connect = this.options.address + ':' + json.port
18 |
19 | state.raw = json
20 | if ('eldewritoVersion' in state.raw) state.version = state.raw.eldewritoVersion
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/protocols/epic.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class Epic extends Core {
4 | constructor () {
5 | super()
6 |
7 | /**
8 | * To get information about game servers using Epic's EOS, you need some credentials to authenticate using OAuth2.
9 | *
10 | * https://dev.epicgames.com/docs/web-api-ref/authentication
11 | *
12 | * These credentials can be provided by the game developers or extracted from the game's files.
13 | */
14 | this.clientId = null
15 | this.clientSecret = null
16 | this.deploymentId = null
17 | this.epicApi = 'https://api.epicgames.dev'
18 | this.authByExternalToken = false // Some games require a client access token to POST to the matchmaking endpoint.
19 |
20 | this.deviceIdAccessToken = null
21 | this.accessToken = null
22 |
23 | // Don't use the tcp ping probing
24 | this.usedTcp = true
25 | }
26 |
27 | async run (state) {
28 | if (this.authByExternalToken) {
29 | await this.getExternalAccessToken()
30 | } else {
31 | await this.getClientAccessToken()
32 | }
33 |
34 | await this.queryInfo(state)
35 | await this.cleanup(state)
36 | }
37 |
38 | async getClientAccessToken () {
39 | this.logger.debug('Requesting client access token ...')
40 |
41 | const url = `${this.epicApi}/auth/v1/oauth/token`
42 | const body = `grant_type=client_credentials&deployment_id=${this.deploymentId}`
43 | const headers = {
44 | Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`,
45 | 'Content-Type': 'application/x-www-form-urlencoded'
46 | }
47 |
48 | this.logger.debug(`POST: ${url}`)
49 | const response = await this.request({ url, body, headers, method: 'POST', responseType: 'json' })
50 |
51 | this.accessToken = response.access_token
52 | }
53 |
54 | async _getDeviceIdToken () {
55 | this.logger.debug('Requesting deviceId access token ...')
56 |
57 | const url = `${this.epicApi}/auth/v1/accounts/deviceid`
58 | const body = 'deviceModel=PC'
59 | const headers = {
60 | Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`,
61 | 'Content-Type': 'application/x-www-form-urlencoded'
62 | }
63 |
64 | this.logger.debug(`POST: ${url}`)
65 | const response = await this.request({ url, body, headers, method: 'POST', responseType: 'json' })
66 |
67 | return response.access_token
68 | }
69 |
70 | async getExternalAccessToken () {
71 | this.logger.debug('Requesting external access token ...')
72 |
73 | const deviceIdToken = await this._getDeviceIdToken()
74 |
75 | const url = `${this.epicApi}/auth/v1/oauth/token`
76 |
77 | const bodyParts = [
78 | 'grant_type=external_auth',
79 | 'external_auth_type=deviceid_access_token',
80 | `external_auth_token=${deviceIdToken}`,
81 | 'nonce=ABCHFA3qgUCJ1XTPAoGDEF', // This is required but can be set to anything
82 | `deployment_id=${this.deploymentId}`,
83 | 'display_name=User'
84 | ]
85 |
86 | const body = bodyParts.join('&')
87 | const headers = {
88 | Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`,
89 | 'Content-Type': 'application/x-www-form-urlencoded'
90 | }
91 |
92 | this.logger.debug(`POST: ${url}`)
93 | const response = await this.request({ url, body, headers, method: 'POST', responseType: 'json' })
94 |
95 | this.accessToken = response.access_token
96 | }
97 |
98 | async queryInfo (state) {
99 | const url = `${this.epicApi}/matchmaking/v1/${this.deploymentId}/filter`
100 | const body = {
101 | criteria: [
102 | {
103 | key: 'attributes.ADDRESS_s',
104 | op: 'EQUAL',
105 | value: this.options.address
106 | }
107 | ]
108 | }
109 | const headers = {
110 | 'Content-Type': 'application/json',
111 | Accept: 'application/json',
112 | Authorization: `Bearer ${this.accessToken}`
113 | }
114 |
115 | this.logger.debug(`POST: ${url}`)
116 | const response = await this.request({ url, json: body, headers, method: 'POST', responseType: 'json' })
117 |
118 | // Epic returns a list of sessions, we need to find the one with the desired port.
119 | const hasDesiredPort = (session) => session.attributes.ADDRESSBOUND_s === `0.0.0.0:${this.options.port}` ||
120 | session.attributes.ADDRESSBOUND_s === `${this.options.address}:${this.options.port}` ||
121 | session.attributes.GAMESERVER_PORT_l === this.options.port
122 |
123 | const desiredServer = response.sessions.find(hasDesiredPort)
124 |
125 | if (!desiredServer) {
126 | throw new Error('Server not found')
127 | }
128 |
129 | state.name = desiredServer.attributes.CUSTOMSERVERNAME_s
130 | state.map = desiredServer.attributes.MAPNAME_s
131 | state.password = desiredServer.attributes.SERVERPASSWORD_b
132 | state.numplayers = desiredServer.totalPlayers
133 | state.maxplayers = desiredServer.settings.maxPublicPlayers
134 |
135 | for (const player of desiredServer.publicPlayers) {
136 | state.players.push({
137 | name: player.name,
138 | raw: player
139 | })
140 | }
141 |
142 | state.raw = desiredServer
143 | }
144 |
145 | async cleanup (state) {
146 | this.accessToken = null
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/protocols/factorio.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class factorio extends Core {
4 | async run (state) {
5 | if (!this.options.port) this.options.port = 34197
6 | this.usedTcp = true
7 |
8 | const serverInfo = await this.request({
9 | url: `https://multiplayer.factorio.com/get-game-details/${this.options.address}:${this.options.port}`,
10 | responseType: 'json'
11 | })
12 |
13 | const players = serverInfo.players || [] // the 'players' field is undefined if there are no players
14 |
15 | state.name = serverInfo.name
16 | state.password = serverInfo.has_password
17 | state.numplayers = players.length
18 | state.maxplayers = serverInfo.max_players
19 | state.players = players.map(player => ({ name: player, raw: {} }))
20 |
21 | state.raw = serverInfo
22 | state.version = state.raw.application_version.game_version + '.' + state.raw.application_version.build_version
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/protocols/farmingsimulator.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import { XMLParser, XMLValidator } from 'fast-xml-parser'
3 |
4 | export default class farmingsimulator extends Core {
5 | async run (state) {
6 | if (!this.options.port) this.options.port = 8080
7 | if (!this.options.token) throw new Error(`No token provided. You can get it from http://${this.options.host}:${this.options.port}/settings.html`)
8 |
9 | const request = await this.request({
10 | url: `http://${this.options.host}:${this.options.port}/feed/dedicated-server-stats.xml?code=${this.options.token}`,
11 | responseType: 'text'
12 | })
13 |
14 | const isValidXML = XMLValidator.validate(request)
15 | if (!isValidXML) {
16 | throw new Error('Invalid XML received from Farming Simulator Server')
17 | }
18 |
19 | const parser = new XMLParser({ ignoreAttributes: false })
20 | const parsed = parser.parse(request)
21 |
22 | const serverInfo = parsed.Server
23 | const playerInfo = serverInfo.Slots
24 |
25 | // Attributes in fast-xml-parser are prefixed with @_
26 |
27 | state.name = serverInfo['@_name']
28 | state.map = serverInfo['@_mapName']
29 | state.numplayers = parseInt(playerInfo['@_numUsed'], 10) || 0
30 | state.maxplayers = parseInt(playerInfo['@_capacity'], 10) || 0
31 |
32 | const players = playerInfo.Player
33 |
34 | for (const player of players) {
35 | if (player['@_isUsed'] !== 'true') { continue }
36 |
37 | state.players.push({
38 | name: player['#text'],
39 | isUsed: player['@_isUsed'] === 'true',
40 | isAdmin: player['@_isAdmin'] === 'true',
41 | uptime: parseInt(player['@_uptime'], 10),
42 | x: parseFloat(player['@_x']),
43 | y: parseFloat(player['@_y']),
44 | z: parseFloat(player['@_z'])
45 | })
46 | }
47 |
48 | state.raw = {
49 | data: request,
50 | mods: []
51 | }
52 |
53 | const mods = serverInfo.Mods.Mod
54 |
55 | for (const mod of mods) {
56 | if (mod['@_name'] == null) { continue }
57 |
58 | state.raw.mods.push({
59 | name: mod['#text'],
60 | short_name: mod['@_name'],
61 | author: mod['@_author'],
62 | version: mod['@_version'],
63 | hash: mod['@_hash']
64 | })
65 | }
66 |
67 | state.version = serverInfo['@_version']
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/protocols/ffow.js:
--------------------------------------------------------------------------------
1 | import valve from './valve.js'
2 |
3 | export default class ffow extends valve {
4 | constructor () {
5 | super()
6 | this.byteorder = 'be'
7 | this.legacyChallenge = true
8 | }
9 |
10 | async queryInfo (state) {
11 | this.logger.debug('Requesting ffow info ...')
12 | const b = await this.sendPacket(
13 | 0x46,
14 | 'LSQ',
15 | 0x49
16 | )
17 |
18 | const reader = this.reader(b)
19 | state.raw.protocol = reader.uint(1)
20 | state.name = reader.string()
21 | state.map = reader.string()
22 | state.raw.mod = reader.string()
23 | state.raw.gamemode = reader.string()
24 | state.raw.description = reader.string()
25 | state.version = reader.string()
26 | state.gamePort = reader.uint(2)
27 | state.numplayers = reader.uint(1)
28 | state.maxplayers = reader.uint(1)
29 | state.raw.listentype = String.fromCharCode(reader.uint(1))
30 | state.raw.environment = String.fromCharCode(reader.uint(1))
31 | state.password = !!reader.uint(1)
32 | state.raw.secure = reader.uint(1)
33 | state.raw.averagefps = reader.uint(1)
34 | state.raw.round = reader.uint(1)
35 | state.raw.maxrounds = reader.uint(1)
36 | state.raw.timeleft = reader.uint(2)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/protocols/fivem.js:
--------------------------------------------------------------------------------
1 | import quake2 from './quake2.js'
2 |
3 | export default class fivem extends quake2 {
4 | constructor () {
5 | super()
6 | this.sendHeader = 'getinfo xxx'
7 | this.responseHeader = 'infoResponse'
8 | this.encoding = 'utf8'
9 | }
10 |
11 | async run (state) {
12 | await super.run(state)
13 |
14 | {
15 | const json = await this.request({
16 | url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
17 | responseType: 'json'
18 | })
19 | state.raw.info = json
20 | if ('version' in state.raw.info) state.version = state.raw.info.version
21 | }
22 |
23 | try {
24 | // TODO: #674, eventually add `requestPlayers` and `requestPlayersRequired`.
25 | const json = await this.request({
26 | url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
27 | responseType: 'json'
28 | })
29 | state.raw.players = json
30 | for (const player of json) {
31 | state.players.push({ name: player.name, ping: player.ping })
32 | }
33 | } catch (_) {}
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/protocols/gamespy1.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | const stringKeys = new Set([
4 | 'website',
5 | 'gametype',
6 | 'gamemode',
7 | 'player'
8 | ])
9 |
10 | function normalizeEntry ([key, value]) {
11 | key = key.toLowerCase()
12 | const split = key.split('_')
13 | let keyType = key
14 |
15 | if (split.length === 2 && !isNaN(Number(split[1]))) {
16 | keyType = split[0]
17 | }
18 |
19 | if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
20 | if (value.toLowerCase() === 'true') {
21 | value = true
22 | } else if (value.toLowerCase() === 'false') {
23 | value = false
24 | } else if (value.length && !isNaN(Number(value))) {
25 | value = Number(value)
26 | }
27 | }
28 |
29 | return [key, value]
30 | }
31 |
32 | export default class gamespy1 extends Core {
33 | constructor () {
34 | super()
35 | this.encoding = 'latin1'
36 | this.byteorder = 'be'
37 | }
38 |
39 | async run (state) {
40 | const raw = await this.sendPacket('\\status\\xserverquery')
41 | // Convert all keys to lowercase and normalize value types
42 | const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
43 | state.raw = data
44 | if ('hostname' in data) state.name = data.hostname
45 | if ('mapname' in data) state.map = data.mapname
46 | if (this.trueTest(data.password)) state.password = true
47 | if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
48 | if ('hostport' in data) state.gamePort = Number(data.hostport)
49 |
50 | const teamOffByOne = data.gamename === 'bfield1942'
51 | const playersById = {}
52 | const teamNamesById = {}
53 | for (const ident of Object.keys(data)) {
54 | const split = ident.split('_')
55 | if (split.length !== 2) continue
56 | let key = split[0].toLowerCase()
57 | const id = Number(split[1])
58 | if (isNaN(id)) continue
59 | let value = data[ident]
60 |
61 | delete data[ident]
62 |
63 | if (key !== 'team' && key.startsWith('team')) {
64 | // Info about a team
65 | if (key === 'teamname') {
66 | teamNamesById[id] = value
67 | } else {
68 | // other team info which we don't track
69 | }
70 | } else {
71 | // Info about a player
72 | if (!(id in playersById)) playersById[id] = {}
73 |
74 | if (key === 'playername' || key === 'player') {
75 | key = 'name'
76 | }
77 | if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
78 | key = 'teamId'
79 | value += teamOffByOne ? -1 : 0
80 | }
81 |
82 | playersById[id][key] = value
83 | }
84 | }
85 | state.raw.teams = teamNamesById
86 |
87 | const players = Object.values(playersById)
88 |
89 | const seenHashes = new Set()
90 | for (const player of players) {
91 | // Some servers (bf1942) report the same player multiple times (bug?)
92 | // Ignore these duplicates
93 | if (player.keyhash) {
94 | if (seenHashes.has(player.keyhash)) {
95 | this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)')
96 | continue
97 | } else {
98 | seenHashes.add(player.keyhash)
99 | }
100 | }
101 |
102 | // Convert player's team ID to team name if possible
103 | if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
104 | if (Object.keys(teamNamesById).length) {
105 | player.team = teamNamesById[player.teamId] || ''
106 | } else {
107 | player.team = player.teamId
108 | delete player.teamId
109 | }
110 | }
111 |
112 | state.players.push(player)
113 | }
114 |
115 | state.numplayers = state.players.length
116 | state.version = state.raw.gamever
117 | }
118 |
119 | async sendPacket (type) {
120 | let receivedQueryId
121 | const output = {}
122 | const parts = new Set()
123 | let maxPartNum = 0
124 |
125 | return await this.udpSend(type, buffer => {
126 | const reader = this.reader(buffer)
127 | const str = reader.string(buffer.length)
128 | const split = str.split('\\')
129 | split.shift()
130 | const data = {}
131 | while (split.length) {
132 | const key = split.shift()
133 | const value = split.shift() || ''
134 | data[key] = value
135 | }
136 |
137 | let queryId, partNum
138 | const partFinal = ('final' in data)
139 | if (data.queryid) {
140 | const split = data.queryid.split('.')
141 | if (split.length >= 2) {
142 | partNum = Number(split[1])
143 | }
144 | queryId = split[0]
145 | }
146 | delete data.final
147 | delete data.queryid
148 | this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
149 |
150 | if (queryId) {
151 | if (receivedQueryId && receivedQueryId !== queryId) {
152 | this.logger.debug('Rejected packet (Wrong query ID)')
153 | return
154 | } else if (!receivedQueryId) {
155 | receivedQueryId = queryId
156 | }
157 | }
158 | if (!partNum) {
159 | partNum = parts.size
160 | this.logger.debug('No part number received (assigned #' + partNum + ')')
161 | }
162 | if (parts.has(partNum)) {
163 | this.logger.debug('Rejected packet (Duplicate part)')
164 | return
165 | }
166 | parts.add(partNum)
167 | if (partFinal) {
168 | maxPartNum = partNum
169 | }
170 |
171 | this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
172 | for (const i of Object.keys(data)) {
173 | output[i] = data[i]
174 | }
175 | if (maxPartNum && parts.size === maxPartNum) {
176 | this.logger.debug('Received all parts')
177 | this.logger.debug(output)
178 | return output
179 | }
180 | })
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/protocols/gamespy2.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class gamespy2 extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.byteorder = 'be'
8 | }
9 |
10 | async run (state) {
11 | // Parse info
12 | {
13 | const body = await this.sendPacket([0xff, 0, 0])
14 | const reader = this.reader(body)
15 | while (!reader.done()) {
16 | const key = reader.string()
17 | const value = reader.string()
18 | if (!key) break
19 | state.raw[key] = value
20 | }
21 | if ('hostname' in state.raw) state.name = state.raw.hostname
22 | if ('mapname' in state.raw) state.map = state.raw.mapname
23 | if (this.trueTest(state.raw.password)) state.password = true
24 | if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
25 | if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
26 | if ('gamever' in state.raw) state.version = state.raw.gamever
27 | }
28 |
29 | // Parse players
30 | {
31 | const body = await this.sendPacket([0, 0xff, 0])
32 | const reader = this.reader(body)
33 | for (const rawPlayer of this.readFieldData(reader)) {
34 | state.players.push(rawPlayer)
35 | }
36 |
37 | if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
38 | else state.numplayers = state.players.length
39 | }
40 |
41 | // Parse teams
42 | {
43 | const body = await this.sendPacket([0, 0, 0xff])
44 | const reader = this.reader(body)
45 | state.raw.teams = this.readFieldData(reader)
46 | }
47 |
48 | // Special case for america's army 1 and 2
49 | // both use gamename = "armygame"
50 | if (state.raw.gamename === 'armygame') {
51 | const stripColor = (str) => {
52 | // uses unreal 2 color codes
53 | return this.options.stripColors ? str.replace(/\x1b...|[\x00-\x1a]/g, '') : str
54 | }
55 | state.name = stripColor(state.name)
56 | state.map = stripColor(state.map)
57 | for (const key of Object.keys(state.raw)) {
58 | if (typeof state.raw[key] === 'string') {
59 | state.raw[key] = stripColor(state.raw[key])
60 | }
61 | }
62 | for (const player of state.players) {
63 | if (!('name' in player)) continue
64 | player.name = stripColor(player.name)
65 | }
66 | }
67 | }
68 |
69 | async sendPacket (type) {
70 | const request = Buffer.concat([
71 | Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
72 | Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
73 | Buffer.from(type)
74 | ])
75 | return await this.udpSend(request, buffer => {
76 | const reader = this.reader(buffer)
77 | const header = reader.uint(1)
78 | if (header !== 0) return
79 | const pingId = reader.uint(4)
80 | if (pingId !== 1) return
81 | return reader.rest()
82 | })
83 | }
84 |
85 | readFieldData (reader) {
86 | reader.uint(1) // always 0
87 | const count = reader.uint(1) // number of rows in this data
88 |
89 | // some games omit the count byte entirely if it's 0 or at random (like americas army)
90 | // Luckily, count should always be <64, and ascii characters will typically be >64,
91 | // so we can detect this.
92 | if (count > 64) {
93 | reader.skip(-1)
94 | this.logger.debug('Detected missing count byte, rewinding by 1')
95 | } else {
96 | this.logger.debug('Detected row count: ' + count)
97 | }
98 |
99 | this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
100 |
101 | const fields = []
102 | while (!reader.done()) {
103 | const field = reader.string()
104 | if (!field) break
105 | fields.push(field)
106 | this.logger.debug('field:' + field)
107 | }
108 |
109 | if (!fields.length) return []
110 |
111 | const units = []
112 | while (!reader.done()) {
113 | const unit = {}
114 | for (let index = 0; index < fields.length; index++) {
115 | let key = fields[index]
116 | let value = reader.string()
117 | if (!value && index === 0) return units
118 |
119 | this.logger.debug('value:' + value)
120 |
121 | // many fields end with "_"
122 | if (key.endsWith('_')) {
123 | key = key.slice(0, -1)
124 | }
125 |
126 | if (key === 'player') key = 'name'
127 | else if (key === 'team_t') key = 'name'
128 | else if (key === 'tickets_t') key = 'tickets'
129 |
130 | if (['score', 'deaths', 'ping', 'team', 'kills', 'tickets'].includes(key)) {
131 | if (value === '') continue
132 | value = parseInt(value)
133 | }
134 |
135 | unit[key] = value
136 | }
137 | units.push(unit)
138 | }
139 |
140 | return units
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/protocols/gamespy3.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class gamespy3 extends Core {
4 | constructor () {
5 | super()
6 | this.sessionId = 1
7 | this.encoding = 'latin1'
8 | this.byteorder = 'be'
9 | this.useOnlySingleSplit = false
10 | this.isJc2mp = false
11 | }
12 |
13 | async run (state) {
14 | const buffer = await this.sendPacket(9, false, false, false)
15 | const reader = this.reader(buffer)
16 | let challenge = parseInt(reader.string())
17 | this.logger.debug('Received challenge key: ' + challenge)
18 | if (challenge === 0) {
19 | // Some servers send us a 0 if they don't want a challenge key used
20 | // BF2 does this.
21 | challenge = null
22 | }
23 |
24 | let requestPayload
25 | if (this.isJc2mp) {
26 | // they completely alter the protocol. because why not.
27 | requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
28 | } else {
29 | requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
30 | }
31 | /** @type Buffer[] */
32 | const packets = await this.sendPacket(0, challenge, requestPayload, true)
33 |
34 | // iterate over the received packets
35 | // the first packet will start off with k/v pairs, followed with data fields
36 | // the following packets will only have data fields
37 | state.raw.playerTeamInfo = {}
38 |
39 | for (let iPacket = 0; iPacket < packets.length; iPacket++) {
40 | const packet = packets[iPacket]
41 | const reader = this.reader(packet)
42 |
43 | this.logger.debug('Parsing packet #' + iPacket)
44 | this.logger.debug(packet)
45 |
46 | // Parse raw server key/values
47 |
48 | if (iPacket === 0) {
49 | while (!reader.done()) {
50 | const key = reader.string()
51 | if (!key) break
52 |
53 | let value = reader.string()
54 | while (value.match(/^p[0-9]+$/)) {
55 | // fix a weird ut3 bug where some keys don't have values
56 | value = reader.string()
57 | }
58 |
59 | state.raw[key] = value
60 | this.logger.debug(key + ' = ' + value)
61 | }
62 | }
63 |
64 | // Parse player, team, item array state
65 |
66 | if (this.isJc2mp) {
67 | state.raw.numPlayers2 = reader.uint(2)
68 | while (!reader.done()) {
69 | const player = {}
70 | player.name = reader.string()
71 | player.steamid = reader.string()
72 | player.ping = reader.uint(2)
73 | state.players.push(player)
74 | }
75 | } else {
76 | while (!reader.done()) {
77 | if (reader.uint(1) <= 2) continue
78 | reader.skip(-1)
79 | const fieldId = reader.string()
80 | if (!fieldId) continue
81 | const fieldIdSplit = fieldId.split('_')
82 | const fieldName = fieldIdSplit[0]
83 | const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
84 |
85 | if (!(itemType in state.raw.playerTeamInfo)) {
86 | state.raw.playerTeamInfo[itemType] = []
87 | }
88 | const items = state.raw.playerTeamInfo[itemType]
89 |
90 | let offset = reader.uint(1)
91 |
92 | this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
93 |
94 | while (!reader.done()) {
95 | const item = reader.string()
96 | if (!item) break
97 |
98 | while (items.length <= offset) { items.push({}) }
99 | items[offset][fieldName] = item
100 | this.logger.debug('* ' + item)
101 | offset++
102 | }
103 | }
104 | }
105 | }
106 |
107 | // Turn all that raw state into something useful
108 |
109 | if ('hostname' in state.raw) state.name = state.raw.hostname
110 | else if ('servername' in state.raw) state.name = state.raw.servername
111 | if ('mapname' in state.raw) state.map = state.raw.mapname
112 | if (state.raw.password === '1') state.password = true
113 | if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
114 | if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
115 | if ('gamever' in state.raw) state.version = state.raw.gamever
116 |
117 | if ('' in state.raw.playerTeamInfo) {
118 | for (const playerInfo of state.raw.playerTeamInfo['']) {
119 | const player = {}
120 | for (const from of Object.keys(playerInfo)) {
121 | let key = from
122 | let value = playerInfo[from]
123 |
124 | if (key === 'player') key = 'name'
125 | if (['score', 'ping', 'team', 'deaths', 'pid'].includes(key)) value = parseInt(value)
126 | player[key] = value
127 | }
128 | state.players.push(player)
129 | }
130 | }
131 |
132 | if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
133 | else state.numplayers = state.players.length
134 | }
135 |
136 | async sendPacket (type, challenge, payload, assemble) {
137 | const challengeLength = challenge === null ? 0 : 4
138 | const payloadLength = payload ? payload.length : 0
139 |
140 | const b = Buffer.alloc(7 + challengeLength + payloadLength)
141 | b.writeUInt8(0xFE, 0)
142 | b.writeUInt8(0xFD, 1)
143 | b.writeUInt8(type, 2)
144 | b.writeUInt32BE(this.sessionId, 3)
145 | if (challengeLength) b.writeInt32BE(challenge, 7)
146 | if (payloadLength) payload.copy(b, 7 + challengeLength)
147 |
148 | let numPackets = 0
149 | const packets = {}
150 | return await this.udpSend(b, (buffer) => {
151 | const reader = this.reader(buffer)
152 | const iType = reader.uint(1)
153 | if (iType !== type) {
154 | this.logger.debug('Skipping packet, type mismatch')
155 | return
156 | }
157 | const iSessionId = reader.uint(4)
158 | if (iSessionId !== this.sessionId) {
159 | this.logger.debug('Skipping packet, session id mismatch')
160 | return
161 | }
162 |
163 | if (!assemble) {
164 | return reader.rest()
165 | }
166 | if (this.useOnlySingleSplit) {
167 | // has split headers, but they are worthless and only one packet is used
168 | reader.skip(11)
169 | return [reader.rest()]
170 | }
171 |
172 | reader.skip(9) // filler data -- usually set to 'splitnum\0'
173 | let id = reader.uint(1)
174 | const last = (id & 0x80)
175 | id = id & 0x7f
176 | if (last) numPackets = id + 1
177 |
178 | reader.skip(1) // "another 'packet number' byte, but isn't understood."
179 |
180 | packets[id] = reader.rest()
181 | if (this.debug) {
182 | this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
183 | }
184 |
185 | if (!numPackets || Object.keys(packets).length !== numPackets) return
186 |
187 | // assemble the parts
188 | const list = []
189 | for (let i = 0; i < numPackets; i++) {
190 | if (!(i in packets)) {
191 | throw new Error('Missing packet #' + i)
192 | }
193 | list.push(packets[i])
194 | }
195 | return list
196 | })
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/protocols/geneshift.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class geneshift extends Core {
4 | async run (state) {
5 | await this.tcpPing()
6 |
7 | const body = await this.request({
8 | url: 'http://geneshift.net/game/receiveLobby.php'
9 | })
10 |
11 | const split = body.split('
')
12 | let found = null
13 | for (const line of split) {
14 | const fields = line.split('::')
15 | const ip = fields[2]
16 | const port = fields[3]
17 | if (ip === this.options.address && parseInt(port) === this.options.port) {
18 | found = fields
19 | break
20 | }
21 | }
22 |
23 | if (found === null) {
24 | throw new Error('Server not found in list')
25 | }
26 |
27 | state.raw.countrycode = found[0]
28 | state.raw.country = found[1]
29 | state.name = found[4]
30 | state.map = found[5]
31 | state.numplayers = parseInt(found[6])
32 | state.maxplayers = parseInt(found[7])
33 | // fields[8] is unknown?
34 | state.raw.rules = found[9]
35 | state.raw.gamemode = parseInt(found[10])
36 | state.raw.gangsters = parseInt(found[11])
37 | state.raw.cashrate = parseInt(found[12])
38 | state.raw.missions = !!parseInt(found[13])
39 | state.raw.vehicles = !!parseInt(found[14])
40 | state.raw.customweapons = !!parseInt(found[15])
41 | state.raw.friendlyfire = !!parseInt(found[16])
42 | state.raw.mercs = !!parseInt(found[17])
43 | // fields[18] is unknown? listen server?
44 | state.version = found[19]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/protocols/goldsrc.js:
--------------------------------------------------------------------------------
1 | import valve from './valve.js'
2 |
3 | export default class goldsrc extends valve {
4 | constructor () {
5 | super()
6 | this.goldsrcInfo = true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/protocols/gtasao.js:
--------------------------------------------------------------------------------
1 | import samp from './samp.js'
2 |
3 | export default class gtasao extends samp {
4 | constructor () {
5 | super()
6 | this.isOmp = true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/protocols/hawakening.js:
--------------------------------------------------------------------------------
1 | import hawakeningmaster from './hawakeningmaster.js'
2 |
3 | /**
4 | * Implements the protocol for Hawakening, a fan project of the UnrealEngine3 based game HAWKEN
5 | * using a Meteor backend for the master server
6 | */
7 | export default class hawakening extends hawakeningmaster {
8 | constructor () {
9 | super()
10 | this.doQuerySingle = true
11 | this.requireToken = true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/protocols/hexen2.js:
--------------------------------------------------------------------------------
1 | import quake1 from './quake1.js'
2 |
3 | export default class hexen2 extends quake1 {
4 | constructor () {
5 | super()
6 | this.sendHeader = '\xFFstatus\x0a'
7 | this.responseHeader = '\xffn'
8 | }
9 |
10 | async run (state) {
11 | await super.run(state)
12 | state.gamePort = this.options.port - 50
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/protocols/index.js:
--------------------------------------------------------------------------------
1 | import armagetron from './armagetron.js'
2 | import ase from './ase.js'
3 | import asa from './asa.js'
4 | import assettocorsa from './assettocorsa.js'
5 | import battlefield from './battlefield.js'
6 | import brokeprotocol from './brokeprotocol.js'
7 | import brokeprotocolmaster from './brokeprotocolmaster.js'
8 | import buildandshoot from './buildandshoot.js'
9 | import cs2d from './cs2d.js'
10 | import discord from './discord.js'
11 | import doom3 from './doom3.js'
12 | import eco from './eco.js'
13 | import eldewrito from './eldewrito.js'
14 | import epic from './epic.js'
15 | import factorio from './factorio.js'
16 | import farmingsimulator from './farmingsimulator.js'
17 | import ffow from './ffow.js'
18 | import fivem from './fivem.js'
19 | import gamespy1 from './gamespy1.js'
20 | import gamespy2 from './gamespy2.js'
21 | import gamespy3 from './gamespy3.js'
22 | import geneshift from './geneshift.js'
23 | import goldsrc from './goldsrc.js'
24 | import gtasao from './gtasao.js'
25 | import hawakening from './hawakening.js'
26 | import hawakeningmaster from './hawakeningmaster.js'
27 | import hexen2 from './hexen2.js'
28 | import jc2mp from './jc2mp.js'
29 | import kspdmp from './kspdmp.js'
30 | import mafia2mp from './mafia2mp.js'
31 | import mafia2online from './mafia2online.js'
32 | import minecraft from './minecraft.js'
33 | import minecraftbedrock from './minecraftbedrock.js'
34 | import minecraftvanilla from './minecraftvanilla.js'
35 | import minetest from './minetest.js'
36 | import mumble from './mumble.js'
37 | import mumbleping from './mumbleping.js'
38 | import nadeo from './nadeo.js'
39 | import openttd from './openttd.js'
40 | import palworld from './palworld.js'
41 | import quake1 from './quake1.js'
42 | import quake2 from './quake2.js'
43 | import quake3 from './quake3.js'
44 | import renegadex from './renegadex.js'
45 | import renegadexmaster from './renegadexmaster.js'
46 | import renown from './renown.js'
47 | import rfactor from './rfactor.js'
48 | import samp from './samp.js'
49 | import satisfactory from './satisfactory.js'
50 | import savage2 from './savage2.js'
51 | import soldat from './soldat.js'
52 | import starmade from './starmade.js'
53 | import starsiege from './starsiege.js'
54 | import teamspeak2 from './teamspeak2.js'
55 | import teamspeak3 from './teamspeak3.js'
56 | import terraria from './terraria.js'
57 | import toxikk from './toxikk.js'
58 | import tribes1 from './tribes1.js'
59 | import tribes1master from './tribes1master.js'
60 | import unreal2 from './unreal2.js'
61 | import ut3 from './ut3.js'
62 | import valve from './valve.js'
63 | import vcmp from './vcmp.js'
64 | import ventrilo from './ventrilo.js'
65 | import warsow from './warsow.js'
66 | import beammpmaster from './beammpmaster.js'
67 | import beammp from './beammp.js'
68 | import dayz from './dayz.js'
69 | import theisleevrima from './theisleevrima.js'
70 | import ragemp from './ragemp.js'
71 | import xonotic from './xonotic.js'
72 | import altvmp from './altvmp.js'
73 | import vintagestorymaster from './vintagestorymaster.js'
74 | import vintagestory from './vintagestory.js'
75 | import sdtd from './sdtd.js'
76 |
77 | export {
78 | armagetron, ase, asa, assettocorsa, battlefield, brokeprotocol, brokeprotocolmaster, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow,
79 | fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hawakening, hawakeningmaster, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
80 | minecraftbedrock, minecraftvanilla, minetest, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, renegadex, renegadexmaster, renown, rfactor, ragemp, samp,
81 | satisfactory, soldat, savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, toxikk, tribes1, tribes1master, unreal2, ut3, valve,
82 | vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz, theisleevrima, xonotic, altvmp, vintagestorymaster, vintagestory, sdtd
83 | }
84 |
--------------------------------------------------------------------------------
/protocols/jc2mp.js:
--------------------------------------------------------------------------------
1 | import gamespy3 from './gamespy3.js'
2 |
3 | // supposedly, gamespy3 is the "official" query protocol for jcmp,
4 | // but it's broken (requires useOnlySingleSplit), and may not include some player names
5 | export default class jc2mp extends gamespy3 {
6 | constructor () {
7 | super()
8 | this.useOnlySingleSplit = true
9 | this.isJc2mp = true
10 | this.encoding = 'utf8'
11 | }
12 |
13 | async run (state) {
14 | await super.run(state)
15 |
16 | state.version = state.raw.version
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/protocols/kspdmp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class kspdmp extends Core {
4 | async run (state) {
5 | const json = await this.request({
6 | url: 'http://' + this.options.address + ':' + this.options.port,
7 | responseType: 'json'
8 | })
9 |
10 | for (const one of json.players) {
11 | state.players.push({ name: one.nickname, team: one.team })
12 | }
13 |
14 | for (const key of Object.keys(json)) {
15 | state.raw[key] = json[key]
16 | }
17 | state.name = json.server_name
18 | state.maxplayers = json.max_players
19 | state.gamePort = json.port
20 | if (json.players) {
21 | const split = json.players.split(', ')
22 | for (const name of split) {
23 | state.players.push({ name })
24 | }
25 | }
26 | state.numplayers = state.players.length
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/protocols/mafia2mp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class mafia2mp extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.header = 'M2MP'
8 | this.isMafia2Online = false
9 | }
10 |
11 | async run (state) {
12 | const body = await this.udpSend(this.header, (buffer) => {
13 | const reader = this.reader(buffer)
14 | const header = reader.string(this.header.length)
15 | if (header !== this.header) return
16 | return reader.rest()
17 | })
18 |
19 | const reader = this.reader(body)
20 | state.name = this.readString(reader)
21 | state.numplayers = parseInt(this.readString(reader))
22 | state.maxplayers = parseInt(this.readString(reader))
23 | state.raw.gamemode = this.readString(reader)
24 | state.version = state.raw.gamemode
25 | state.password = !!reader.uint(1)
26 | state.gamePort = this.options.port - 1
27 |
28 | while (!reader.done()) {
29 | const player = {}
30 | player.name = this.readString(reader)
31 | if (!player.name) break
32 | if (this.isMafia2Online) {
33 | player.ping = parseInt(this.readString(reader))
34 | }
35 | state.players.push(player)
36 | }
37 | }
38 |
39 | readString (reader) {
40 | return reader.pascalString(1, -1)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/protocols/mafia2online.js:
--------------------------------------------------------------------------------
1 | import mafia2mp from './mafia2mp.js'
2 |
3 | export default class mafia2online extends mafia2mp {
4 | constructor () {
5 | super()
6 | this.header = 'M2Online'
7 | this.isMafia2Online = true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/protocols/minecraft.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import minecraftbedrock from './minecraftbedrock.js'
3 | import minecraftvanilla from './minecraftvanilla.js'
4 | import Gamespy3 from './gamespy3.js'
5 |
6 | /*
7 | Vanilla servers respond to minecraftvanilla only
8 | Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
9 | Some bedrock servers respond to gamespy3 only
10 | Some bedrock servers respond to minecraftbedrock only
11 | Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
12 | */
13 |
14 | export default class minecraft extends Core {
15 | constructor () {
16 | super()
17 | this.srvRecord = '_minecraft._tcp'
18 | }
19 |
20 | async run (state) {
21 | /** @type {Promise[]} */
22 | const promises = []
23 |
24 | const vanillaResolver = new minecraftvanilla()
25 | vanillaResolver.options = this.options
26 | vanillaResolver.udpSocket = this.udpSocket
27 | promises.push(vanillaResolver)
28 |
29 | const gamespyResolver = new Gamespy3()
30 | gamespyResolver.options = {
31 | ...this.options,
32 | encoding: 'utf8'
33 | }
34 | gamespyResolver.udpSocket = this.udpSocket
35 | promises.push(gamespyResolver)
36 |
37 | const bedrockResolver = new minecraftbedrock()
38 | bedrockResolver.options = this.options
39 | bedrockResolver.udpSocket = this.udpSocket
40 | promises.push(bedrockResolver)
41 |
42 | const ranPromises = promises.map(p => p.runOnceSafe().catch(_ => undefined))
43 | const [vanillaState, gamespyState, bedrockState] = await Promise.all(ranPromises)
44 |
45 | state.raw.vanilla = vanillaState
46 | state.raw.gamespy = gamespyState
47 | state.raw.bedrock = bedrockState
48 |
49 | if (!vanillaState && !gamespyState && !bedrockState) {
50 | throw new Error('No protocols succeeded')
51 | }
52 |
53 | // Ordered from least worth to most worth (player names / etc)
54 | if (bedrockState) {
55 | if (bedrockState.players.length) state.players = bedrockState.players
56 | }
57 | if (vanillaState) {
58 | try {
59 | let name = ''
60 | const description = vanillaState.raw.description
61 | if (typeof description === 'string') {
62 | name = description
63 | } else if (typeof description === 'object') {
64 | const stack = [description]
65 |
66 | while (stack.length) {
67 | const current = stack.pop()
68 |
69 | if (current.text) {
70 | name += current.text
71 | }
72 |
73 | if (Array.isArray(current.extra)) {
74 | stack.push(...current.extra.reverse())
75 | }
76 | }
77 | }
78 | state.name = name
79 | } catch (e) {}
80 | if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
81 | if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
82 | if (vanillaState.players.length) state.players = vanillaState.players
83 | if (vanillaState.ping) this.registerRtt(vanillaState.ping)
84 | if (vanillaState.raw.version) state.version = vanillaState.raw.version.name
85 | }
86 | if (gamespyState) {
87 | if (gamespyState.name) state.name = gamespyState.name
88 | if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
89 | if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
90 | if (gamespyState.players.length) state.players = gamespyState.players
91 | else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
92 | if (gamespyState.ping) this.registerRtt(gamespyState.ping)
93 | }
94 | if (bedrockState) {
95 | if (bedrockState.name) state.name = bedrockState.name
96 | if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
97 | if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
98 | if (bedrockState.map) state.map = bedrockState.map
99 | if (bedrockState.ping) this.registerRtt(bedrockState.ping)
100 | if (bedrockState.raw.mcVersion) state.version = bedrockState.raw.mcVersion
101 | }
102 | // remove dupe spaces from name
103 | state.name = state.name.replace(/\s+/g, ' ')
104 | // remove color codes from name
105 | state.name = state.name.replace(/\u00A7./g, '')
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/protocols/minecraftbedrock.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class minecraftbedrock extends Core {
4 | constructor () {
5 | super()
6 | this.byteorder = 'be'
7 | }
8 |
9 | async run (state) {
10 | const bufs = [
11 | Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
12 | Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
13 | Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
14 | Buffer.from('0000000000000000', 'hex') // Cliend GUID
15 | ]
16 |
17 | return await this.udpSend(Buffer.concat(bufs), buffer => {
18 | const reader = this.reader(buffer)
19 |
20 | const messageId = reader.uint(1)
21 | if (messageId !== 0x1c) {
22 | this.logger.debug('Skipping packet, invalid message id')
23 | return
24 | }
25 |
26 | const nonce = reader.part(8).toString('hex') // should match the nonce we sent
27 | this.logger.debug('Nonce: ' + nonce)
28 | if (nonce !== '1122334455667788') {
29 | this.logger.debug('Skipping packet, invalid nonce')
30 | return
31 | }
32 |
33 | // These 8 bytes are identical to the serverId string we receive in decimal below
34 | reader.skip(8)
35 |
36 | const magic = reader.part(16).toString('hex')
37 | this.logger.debug('Magic value: ' + magic)
38 | if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
39 | this.logger.debug('Skipping packet, invalid magic')
40 | return
41 | }
42 |
43 | const statusLen = reader.uint(2)
44 | if (reader.remaining() !== statusLen) {
45 | throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
46 | }
47 |
48 | const statusStr = reader.rest().toString('utf8')
49 | this.logger.debug('Raw status str: ' + statusStr)
50 |
51 | const split = statusStr.split(';')
52 | if (split.length < 6) {
53 | throw new Error('Missing enough chunks in status str')
54 | }
55 |
56 | state.raw.edition = split.shift()
57 | state.name = split.shift()
58 | state.raw.protocolVersion = split.shift()
59 | state.raw.mcVersion = split.shift()
60 | state.version = state.raw.mcVersion
61 | state.numplayers = parseInt(split.shift())
62 | state.maxplayers = parseInt(split.shift())
63 | if (split.length) state.raw.serverId = split.shift()
64 | if (split.length) state.map = split.shift()
65 | if (split.length) state.raw.gameMode = split.shift()
66 | if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
67 | if (split.length) state.raw.ipv4Port = split.shift()
68 | if (split.length) state.raw.ipv6Port = split.shift()
69 |
70 | return true
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/protocols/minecraftvanilla.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import Varint from 'varint'
3 |
4 | export default class minecraftvanilla extends Core {
5 | async run (state) {
6 | const portBuf = Buffer.alloc(2)
7 | portBuf.writeUInt16BE(this.options.port, 0)
8 |
9 | const addressBuf = Buffer.from(this.options.host, 'utf8')
10 |
11 | const bufs = [
12 | this.varIntBuffer(47),
13 | this.varIntBuffer(addressBuf.length),
14 | addressBuf,
15 | portBuf,
16 | this.varIntBuffer(1)
17 | ]
18 |
19 | const outBuffer = Buffer.concat([
20 | this.buildPacket(0, Buffer.concat(bufs)),
21 | this.buildPacket(0)
22 | ])
23 |
24 | const data = await this.withTcp(async socket => {
25 | return await this.tcpSend(socket, outBuffer, data => {
26 | if (data.length < 10) return
27 | const reader = this.reader(data)
28 | const length = reader.varint()
29 | if (data.length < length) return
30 | return reader.rest()
31 | })
32 | })
33 |
34 | const reader = this.reader(data)
35 |
36 | const packetId = reader.varint()
37 | this.logger.debug('Packet ID: ' + packetId)
38 |
39 | const strLen = reader.varint()
40 | this.logger.debug('String Length: ' + strLen)
41 |
42 | const rest = reader.rest()
43 |
44 | const str = rest.toString('utf8', 0, strLen)
45 | this.logger.debug(str)
46 |
47 | const json = JSON.parse(str.substring(0, strLen))
48 |
49 | state.raw = json
50 | state.maxplayers = json.players.max
51 | state.numplayers = json.players.online
52 |
53 | if (json.players.sample) {
54 | for (const player of json.players.sample) {
55 | state.players.push({
56 | id: player.id,
57 | name: player.name
58 | })
59 | }
60 | }
61 |
62 | // Better Compatibility Checker mod support
63 | let bccJson = {}
64 |
65 | if (rest.length > strLen) {
66 | const bccStr = rest.toString('utf8', strLen + 1)
67 | bccJson = JSON.parse(bccStr)
68 | }
69 |
70 | state.raw.bcc = bccJson
71 | }
72 |
73 | varIntBuffer (num) {
74 | return Buffer.from(Varint.encode(num))
75 | }
76 |
77 | buildPacket (id, data) {
78 | if (!data) data = Buffer.from([])
79 | const idBuffer = this.varIntBuffer(id)
80 | return Buffer.concat([
81 | this.varIntBuffer(data.length + idBuffer.length),
82 | idBuffer,
83 | data
84 | ])
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/protocols/minetest.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class minetest extends Core {
4 | constructor () {
5 | super()
6 | this.usedTcp = true
7 | }
8 |
9 | async run (state) {
10 | const servers = await this.request({
11 | url: 'https://servers.minetest.net/list',
12 | responseType: 'json'
13 | })
14 |
15 | if (servers == null) {
16 | throw new Error('Unable to retrieve master server list')
17 | }
18 |
19 | const serverInfo = servers.list.find(
20 | (server) =>
21 | server.address === this.options.address && server.port === this.options.port
22 | )
23 |
24 | if (serverInfo == null) {
25 | throw new Error('Server not found in master server list')
26 | }
27 |
28 | const players = serverInfo.clients_list || [] // the 'players' field is undefined if there are no players
29 |
30 | state.name = serverInfo.name
31 | state.password = serverInfo.password
32 | state.numplayers = serverInfo.clients || players.length
33 | state.maxplayers = serverInfo.clients_max
34 | state.players = players.map((player) => ({ name: player, raw: {} }))
35 |
36 | state.raw = serverInfo
37 | state.version = serverInfo.version
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/protocols/mumble.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class mumble extends Core {
4 | async run (state) {
5 | const json = await this.withTcp(async socket => {
6 | return await this.tcpSend(socket, 'json', (buffer) => {
7 | if (buffer.length < 10) return
8 | const str = buffer.toString()
9 | let json
10 | try {
11 | json = JSON.parse(str)
12 | } catch (e) {
13 | // probably not all here yet
14 | return
15 | }
16 | return json
17 | })
18 | })
19 |
20 | state.raw = json
21 | state.name = json.name
22 | state.gamePort = json.x_gtmurmur_connectport || 64738
23 |
24 | let channelStack = [state.raw.root]
25 | while (channelStack.length) {
26 | const channel = channelStack.shift()
27 | channel.description = this.cleanComment(channel.description)
28 | channelStack = channelStack.concat(channel.channels)
29 | for (const user of channel.users) {
30 | user.comment = this.cleanComment(user.comment)
31 | state.players.push(user)
32 | }
33 | }
34 | }
35 |
36 | cleanComment (str) {
37 | return str.replace(/<.*>/g, '')
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/protocols/mumbleping.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class mumbleping extends Core {
4 | constructor () {
5 | super()
6 | this.byteorder = 'be'
7 | }
8 |
9 | async run (state) {
10 | const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
11 | if (buffer.length >= 24) return buffer
12 | })
13 |
14 | const reader = this.reader(data)
15 | reader.skip(1)
16 | state.raw.versionMajor = reader.uint(1)
17 | state.raw.versionMinor = reader.uint(1)
18 | state.raw.versionPatch = reader.uint(1)
19 | state.version = state.raw.versionMajor + '.' + state.raw.versionMinor + '.' + state.raw.versionPatch
20 | reader.skip(8)
21 | state.numplayers = reader.uint(4)
22 | state.maxplayers = reader.uint(4)
23 | state.raw.allowedbandwidth = reader.uint(4)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/protocols/nadeo.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import Promises from '../lib/Promises.js'
3 | import * as gbxremote from 'gbxremote'
4 |
5 | export default class nadeo extends Core {
6 | async run (state) {
7 | await this.withClient(async client => {
8 | const start = Date.now()
9 | await this.query(client, 'Authenticate', this.options.login, this.options.password)
10 | this.registerRtt(Date.now() - start)
11 |
12 | {
13 | const results = await this.query(client, 'GetServerOptions')
14 | state.name = this.stripColors(results.Name)
15 | state.password = (results.Password !== 'No password')
16 | state.maxplayers = results.CurrentMaxPlayers
17 | state.raw.maxspectators = results.CurrentMaxSpectators
18 | state.raw.GetServerOptions = results
19 | }
20 |
21 | {
22 | const results = await this.query(client, 'GetCurrentChallengeInfo')
23 | state.map = this.stripColors(results.Name)
24 | state.raw.GetCurrentChallengeInfo = results
25 | }
26 |
27 | {
28 | const results = await this.query(client, 'GetCurrentGameInfo')
29 | let gamemode = ''
30 | const igm = results.GameMode
31 | if (igm === 0) gamemode = 'Rounds'
32 | if (igm === 1) gamemode = 'Time Attack'
33 | if (igm === 2) gamemode = 'Team'
34 | if (igm === 3) gamemode = 'Laps'
35 | if (igm === 4) gamemode = 'Stunts'
36 | if (igm === 5) gamemode = 'Cup'
37 | state.raw.gametype = gamemode
38 | state.raw.mapcount = results.NbChallenge
39 | state.raw.GetCurrentGameInfo = results
40 | }
41 |
42 | {
43 | const results = await this.query(client, 'GetVersion')
44 | state.version = results.Version
45 | state.raw.GetVersion = results
46 | }
47 |
48 | if (this.options.port === 5000) {
49 | state.gamePort = 2350
50 | }
51 |
52 | state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
53 | for (const player of state.raw.players) {
54 | state.players.push({
55 | name: this.stripColors(player.Name || player.NickName)
56 | })
57 | }
58 | state.numplayers = state.players.length
59 | })
60 | }
61 |
62 | async withClient (fn) {
63 | const socket = new gbxremote.Client(this.options.port, this.options.host)
64 | try {
65 | const connectPromise = socket.connect()
66 | const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
67 | await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
68 | return await fn(socket)
69 | } finally {
70 | socket.terminate()
71 | }
72 | }
73 |
74 | async query (client, command, ...args) {
75 | const params = args.slice()
76 |
77 | const sentPromise = client.query(command, params)
78 | const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
79 | return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
80 | }
81 |
82 | stripColors (str) {
83 | return this.options.stripColors ? str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '') : str
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/protocols/openttd.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class openttd extends Core {
4 | async run(state) {
5 | {
6 | let [reader, version] = await this.query(7, 6, 1, -1)
7 |
8 | if (version > 7) version = 7 //current version is 7, but this should work for heigher versions too
9 |
10 | switch (version) {
11 | case 7:
12 | state.raw.ticks_playing = reader.uint(8)
13 | case 6:
14 | state.raw.newgrf_serialisation = reader.uint(1)
15 | case 5:
16 | state.raw.gamescript_version = reader.uint(4)
17 | state.raw.gamescript_name = reader.string() // .replace(/\0/g, '')
18 | case 4:
19 | const numGrf = reader.uint(1)
20 | state.raw.grfs = []
21 | for (let i = 0; i < numGrf; i++) {
22 | const grf = {}
23 | grf.id = reader.part(4).toString('hex')
24 | grf.md5 = reader.part(16).toString('hex')
25 | grf.name = reader.string()
26 | state.raw.grfs.push(grf)
27 | }
28 | case 3:
29 | state.raw.date_current = this.readDate(reader)
30 | state.raw.date_start = this.readDate(reader)
31 | case 2:
32 | state.raw.maxcompanies = reader.uint(1)
33 | state.raw.numcompanies = reader.uint(1)
34 | state.raw.maxspectators = reader.uint(1) //deprecated
35 | case 1:
36 | state.name = reader.string()
37 | state.version = reader.string()
38 |
39 | if (version < 6) {
40 | // reader.skip(1)
41 | state.raw.language = this.decode(
42 | reader.uint(1),
43 | ['any', 'en', 'de', 'fr']
44 | )
45 | }
46 |
47 | state.password = !!reader.uint(1)
48 | state.maxplayers = reader.uint(1)
49 | state.numplayers = reader.uint(1)
50 | state.raw.numspectators = reader.uint(1)
51 |
52 | if (version < 3) {
53 | state.raw.date_current = this.readOldDate(reader)
54 | state.raw.date_start = this.readOldDate(reader)
55 | }
56 | if (version < 6) {
57 | state.map = reader.string()
58 | }
59 | state.raw.map_width = reader.uint(2)
60 | state.raw.map_height = reader.uint(2)
61 | state.raw.landscape = this.decode(
62 | reader.uint(1),
63 | ['temperate', 'arctic', 'desert', 'toyland']
64 | )
65 | state.raw.dedicated = !!reader.uint(1)
66 | }
67 |
68 | if (version < 7) {
69 | const DAY_TICKS = 74;
70 | state.raw.ticks_playing = (new Date(state.raw.date_current).getTime() - new Date(state.raw.date_start).getTime()) / 1000 / 3600 / 24 * DAY_TICKS + 1280; //1280 looks like initial ticks after server start
71 | }
72 | }
73 |
74 | /**
75 | * this doesnt work with the current version of openttd and causes timeouts
76 | * leaving it here for the case that it will be fixed in the future
77 | */
78 | // {
79 | // const [reader, version] = await this.query(2, 3, -1, -1)
80 | // // we don't know how to deal with companies outside version 6
81 | // if (version === 6) {
82 | // state.raw.companies = []
83 | // const numCompanies = reader.uint(1)
84 | // for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
85 | // const company = {}
86 | // company.id = reader.uint(1)
87 | // company.name = reader.string()
88 | // company.year_start = reader.uint(4)
89 | // company.value = reader.uint(8).toString()
90 | // company.money = reader.uint(8).toString()
91 | // company.income = reader.uint(8).toString()
92 | // company.performance = reader.uint(2)
93 | // company.password = !!reader.uint(1)
94 |
95 | // const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
96 | // const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
97 |
98 | // company.vehicles = {}
99 | // for (const type of vehicleTypes) {
100 | // company.vehicles[type] = reader.uint(2)
101 | // }
102 | // company.stations = {}
103 | // for (const type of stationTypes) {
104 | // company.stations[type] = reader.uint(2)
105 | // }
106 |
107 | // company.clients = reader.string()
108 | // state.raw.companies.push(company)
109 | // }
110 | // }
111 | // }
112 | }
113 |
114 | async query (type, expected, minver, maxver) {
115 | const b = Buffer.from([0x03, 0x00, type])
116 | return await this.withTcp(async socket => {
117 | return await this.tcpSend(socket, b, (buffer) => {
118 | const reader = this.reader(buffer)
119 | const packetLen = reader.uint(2)
120 | if (packetLen !== buffer.length) {
121 | this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
122 | return
123 | }
124 |
125 | const packetType = reader.uint(1)
126 | if (packetType !== expected) {
127 | this.logger.debug('Unexpected response packet type: ' + packetType)
128 | return
129 | }
130 |
131 | const protocolVersion = reader.uint(1)
132 | if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
133 | throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
134 | }
135 |
136 | return [reader, protocolVersion]
137 | })
138 | })
139 | }
140 |
141 | readDate (reader) {
142 | const daysSinceZero = reader.uint(4)
143 | const temp = new Date(0, 0, 1)
144 | temp.setFullYear(0)
145 | temp.setDate(daysSinceZero + 2) // to show correct date here must be +2
146 | return temp.toISOString().split('T')[0]
147 | }
148 |
149 | readOldDate(reader) {
150 | const DAYS_TILL_ORIGIANAL_BASE_YEAR = 365 * 500 + 125
151 | const daysSinceZero = DAYS_TILL_ORIGIANAL_BASE_YEAR + reader.uint(2)
152 | const temp = new Date(0, 0, 1)
153 | temp.setFullYear(0) //not sure about this - no option to test it
154 | temp.setDate(daysSinceZero + 2)
155 | return temp.toISOString().split('T')[0]
156 | }
157 |
158 | decode (num, arr) {
159 | if (num < 0 || num >= arr.length) {
160 | return num
161 | }
162 | return arr[num]
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/protocols/palworld.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class palworld extends Core {
4 | async makeCall (endpoint) {
5 | const url = `http://${this.options.host}:${this.options.port}/v1/api/${endpoint}`
6 | const headers = {
7 | Authorization: `Basic ${Buffer.from(`${this.options.username}:${this.options.password}`).toString('base64')}`,
8 | Accept: 'application/json'
9 | }
10 |
11 | return await this.request({ url, headers, method: 'GET', responseType: 'json' })
12 | }
13 |
14 | async run (state) {
15 | const serverInfo = await this.makeCall('info')
16 | state.version = serverInfo.version
17 | state.name = serverInfo.servername
18 | state.raw.serverInfo = serverInfo
19 |
20 | const { players } = await this.makeCall('players')
21 | state.numplayers = players.length
22 | state.players = players.map((player) => ({ name: player.name, raw: player }))
23 | state.raw.players = players
24 |
25 | state.raw.settings = await this.makeCall('settings')
26 |
27 | const metrics = await this.makeCall('metrics')
28 | state.numplayers = metrics.currentplayernum
29 | state.maxplayers = metrics.maxplayernum
30 | state.raw.metrics = metrics
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/protocols/quake1.js:
--------------------------------------------------------------------------------
1 | import quake2 from './quake2.js'
2 |
3 | export default class quake1 extends quake2 {
4 | constructor () {
5 | super()
6 | this.responseHeader = 'n'
7 | this.isQuake1 = true
8 | }
9 |
10 | async run (state) {
11 | await super.run(state)
12 | if ('*version' in state.raw) state.version = state.raw['*version']
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/protocols/quake2.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class quake2 extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.delimiter = '\n'
8 | this.sendHeader = 'status'
9 | this.responseHeader = 'print'
10 | this.isQuake1 = false
11 | }
12 |
13 | async run (state) {
14 | const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
15 | const reader = this.reader(packet)
16 | const header = reader.string({ length: 4, encoding: 'latin1' })
17 | if (header !== '\xff\xff\xff\xff') return
18 | let type
19 | if (this.isQuake1) {
20 | type = reader.string(this.responseHeader.length)
21 | } else {
22 | type = reader.string({ encoding: 'latin1' })
23 | }
24 | if (type !== this.responseHeader) return
25 | return reader.rest()
26 | })
27 |
28 | const reader = this.reader(body)
29 | const info = reader.string().split('\\')
30 | if (info[0] === '') info.shift()
31 |
32 | while (true) {
33 | const key = info.shift()
34 | const value = info.shift()
35 | if (typeof value === 'undefined') break
36 | state.raw[key] = value
37 | }
38 |
39 | while (!reader.done()) {
40 | const line = reader.string()
41 | if (!line || line.charAt(0) === '\0') break
42 |
43 | const args = []
44 | const split = line.split('"')
45 | split.forEach((part, i) => {
46 | const inQuote = (i % 2 === 1)
47 | if (inQuote) {
48 | args.push(part)
49 | } else {
50 | const splitSpace = part.split(' ')
51 | for (const subpart of splitSpace) {
52 | if (subpart) args.push(subpart)
53 | }
54 | }
55 | })
56 |
57 | const player = {}
58 | if (this.isQuake1) {
59 | player.id = parseInt(args.shift())
60 | player.score = parseInt(args.shift())
61 | player.time = parseInt(args.shift())
62 | player.ping = parseInt(args.shift())
63 | player.name = args.shift()
64 | player.skin = args.shift()
65 | player.color1 = parseInt(args.shift())
66 | player.color2 = parseInt(args.shift())
67 | } else {
68 | player.frags = parseInt(args.shift())
69 | player.ping = parseInt(args.shift())
70 | player.name = args.shift() || ''
71 | if (!player.name) delete player.name
72 | player.address = args.shift() || ''
73 | if (!player.address) delete player.address
74 | }
75 |
76 | (player.ping ? state.players : state.bots).push(player)
77 | }
78 |
79 | if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
80 | if ('mapname' in state.raw) state.map = state.raw.mapname
81 | if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
82 | if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
83 | if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
84 | if ('hostname' in state.raw) state.name = state.raw.hostname
85 | if ('clients' in state.raw) state.numplayers = state.raw.clients
86 | if ('version' in state.raw) state.version = state.raw.version
87 | if ('iv' in state.raw) state.version = state.raw.iv
88 | else state.numplayers = state.players.length + state.bots.length
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/protocols/quake3.js:
--------------------------------------------------------------------------------
1 | import quake2 from './quake2.js'
2 |
3 | export default class quake3 extends quake2 {
4 | constructor () {
5 | super()
6 | this.sendHeader = 'getstatus'
7 | this.responseHeader = 'statusResponse'
8 | }
9 |
10 | async run (state) {
11 | await super.run(state)
12 | state.name = this.stripColors(state.name)
13 | for (const key of Object.keys(state.raw)) {
14 | state.raw[key] = this.stripColors(state.raw[key])
15 | if ('version' in state.raw) state.version = state.raw.version
16 | }
17 | for (const player of state.players) {
18 | player.name = this.stripColors(player.name)
19 | }
20 | for (const bot of state.bots) {
21 | bot.name = this.stripColors(bot.name)
22 | }
23 | }
24 |
25 | stripColors (str) {
26 | return this.options.stripColors ? str.replace(/\^(X.{6}|.)/g, '') : str
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/protocols/ragemp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class ragemp extends Core {
4 | constructor () {
5 | super()
6 | this.usedTcp = true
7 | }
8 |
9 | async run (state) {
10 | const results = await this.request({
11 | url: 'https://cdn.rage.mp/master/v2/',
12 | responseType: 'json'
13 | })
14 |
15 | if (results == null) {
16 | throw new Error('Unable to retrieve master server list')
17 | }
18 |
19 | const targetID = `${this.options.host}:${this.options.port}`
20 |
21 | let serverResult = null
22 | let serverInfo = null
23 |
24 | for (const entry of results) {
25 | if (entry.id === targetID) {
26 | serverResult = entry
27 | serverInfo = entry.servers.at(0)
28 | break
29 | }
30 |
31 | for (const serverEntry of entry.servers) {
32 | if (serverEntry.id === targetID) {
33 | serverResult = entry
34 | serverInfo = serverEntry
35 | break
36 | }
37 | }
38 | }
39 |
40 | if (serverInfo == null) {
41 | throw new Error('Server not found in master server list.')
42 | }
43 |
44 | state.name = serverInfo.name
45 | state.numplayers = serverInfo.players.amount
46 | state.maxplayers = serverInfo.players.max
47 | state.raw = serverResult
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/protocols/renegadex.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | // import Ajv from 'ajv'
3 | // const ajv = new Ajv()
4 |
5 | export const MasterServerServerInfoSchema = {
6 | type: 'object',
7 | required: [
8 | 'IP',
9 | 'Port',
10 | 'Name',
11 | 'Current Map',
12 | 'Bots',
13 | 'Players',
14 | 'Game Version',
15 | 'Variables'
16 | ],
17 | properties: {
18 | IP: {
19 | type: 'string',
20 | format: 'ipv4',
21 | description: 'IP of the server'
22 | },
23 | Port: {
24 | type: 'integer',
25 | minimum: 0,
26 | maximum: 65535,
27 | description: 'The port of the server instance to connect to for joining'
28 | },
29 | Name: {
30 | type: 'string',
31 | description: 'Name of the server, i.e.: Bob\'s Server.'
32 | },
33 | NamePrefix: {
34 | type: 'string',
35 | description: 'A prefix of the server'
36 | },
37 | 'Current Map': {
38 | type: 'string',
39 | description: 'The current map\'s name the server is running is running'
40 | },
41 | Players: {
42 | type: 'integer',
43 | description: 'The number of players connected to the server',
44 | minimum: 0
45 | },
46 | Bots: {
47 | type: 'integer',
48 | minimum: 0,
49 | description: 'The number of bots'
50 | },
51 | 'Game Version': {
52 | type: 'string',
53 | pattern: '^Open Beta (.*?)?$',
54 | description: 'Version of the build of the server'
55 | },
56 | Variables: {
57 | type: 'object',
58 | properties: {
59 | 'Player Limit': {
60 | type: 'integer',
61 | minimum: 0,
62 | description: 'Maximum number of players allowed by this server'
63 | },
64 | 'Time Limit': {
65 | type: 'integer',
66 | minimum: 0,
67 | description: 'time limit in minutes'
68 | },
69 | 'Team Mode': {
70 | type: 'integer',
71 | description: 'Determines how teams are organized between matches.',
72 | enum: [
73 | 0, // static,
74 | 1, // swap
75 | 2, // random swap
76 | 3, // shuffle
77 | 4, // traditional (assign as players connect)
78 | 5, // traditional + free swap
79 | 6 // ladder rank
80 | ]
81 | },
82 | 'Game Type': {
83 | type: 'integer',
84 | description: 'Type of the game the server is running',
85 | enum: [
86 | 0, // Rx_Game_MainMenu
87 | 1, // Rx_Game
88 | 2, // TS_Game
89 | 3 // SP_Game
90 | // < 3 x < 1000 = RenX Unused/Reserved
91 | // < 1000 < x < 2^31 - 1 = Unassigned / Mod space
92 | ]
93 | },
94 | 'Vehicle Limit': {
95 | type: 'integer',
96 | minimum: 0,
97 | description: 'Maximum number of vehicles allowed by this server'
98 | },
99 | 'Mine Limit': {
100 | type: 'integer',
101 | minimum: 0,
102 | description: 'Maximum number of mines allowed by this server'
103 | },
104 | bPassworded: {
105 | type: 'boolean',
106 | description: 'Whether a password is required to enter the game'
107 | },
108 | bSteamRequired: {
109 | type: 'boolean',
110 | description: 'Whether clients required to be logged into Steam to play on this server'
111 | },
112 | bRanked: {
113 | type: 'boolean',
114 | description: 'Whether the serer is ranked/official'
115 | },
116 | bAllowPrivateMessaging: {
117 | type: 'boolean',
118 | description: 'Whether the server allows non-admin clients to PM each other'
119 | },
120 | bPrivateMessageTeamOnly: {
121 | type: 'boolean',
122 | description: 'whether private messaging is restricted to just teammates'
123 | },
124 | bAutoBalanceTeams: { // alias of 'bSpawnCrates'
125 | type: 'boolean',
126 | description: 'Whether the server will spawn crates in this game for balancing'
127 | },
128 | bSpawnCrates: {
129 | type: 'boolean',
130 | description: 'Whether the server will spawn crates in this game for balancing'
131 | },
132 | CrateRespawnAfterPickup: {
133 | type: 'integer',
134 | minimum: 0,
135 | description: 'interval for crate respawn (after pickup)'
136 | }
137 | },
138 | required: [
139 | 'Player Limit',
140 | 'Time Limit',
141 | 'Team Mode',
142 | 'Game Type',
143 | 'Vehicle Limit',
144 | 'Mine Limit'
145 | ]
146 | }
147 | }
148 | }
149 | export const MasterServerResponseSchema = {
150 | type: 'array',
151 | items: { $ref: '#/$defs/server' },
152 | $defs: {
153 | server: MasterServerServerInfoSchema
154 | }
155 | }
156 |
157 | /**
158 | * Implements the protocol for Renegade X, an UnrealEngine3 based game, using a custom master server
159 | */
160 | export default class renegadex extends Core {
161 | constructor () {
162 | super()
163 | this.usedTcp = true
164 | }
165 |
166 | async run (state) {
167 | // query master list and find specific server
168 | const servers = await this.getMasterServerList()
169 | const serverInfo = servers.find((server) => {
170 | return server.IP === this.options.address && server.Port === this.options.port
171 | })
172 |
173 | if (serverInfo == null) {
174 | throw new Error('Server not found in master server list')
175 | }
176 |
177 | // set state properties based on received server info
178 | this.populateProperties(state, serverInfo)
179 | }
180 |
181 | /**
182 | * Retrieves server list from master server
183 | * @throws {Error} Will throw error when no master list was received
184 | * @returns a list of servers as raw data
185 | */
186 | async getMasterServerList () {
187 | const servers = await this.request({
188 | url: 'https://serverlist-rx.totemarts.services/servers.jsp',
189 | responseType: 'json'
190 | })
191 |
192 | if (servers == null) {
193 | throw new Error('Unable to retrieve master server list')
194 | }
195 | if (!Array.isArray(servers)) {
196 | throw new Error('Invalid data received from master server. Expecting list of data')
197 | }
198 | if (servers.length === 0) {
199 | throw new Error('No data received from master server.')
200 | }
201 |
202 | // TODO: Ajv response validation
203 | // const isDataValid = ajv.validate(MasterServerResponseSchema, servers)
204 | // if (!isDataValid) {
205 | // throw new Error(`Received master server data is unknown/invalid: ${ajv.errorsText(ajv.errors)}`)
206 | // }
207 |
208 | return servers
209 | }
210 |
211 | /**
212 | * Translates raw properties into known properties
213 | * @param {Object} state Parsed data
214 | */
215 | populateProperties (state, serverInfo) {
216 | let emptyPrefix = ''
217 | if (serverInfo.NamePrefix) emptyPrefix = serverInfo.NamePrefix + ' '
218 | const servername = `${emptyPrefix}${serverInfo.Name || ''}`
219 | const numplayers = serverInfo.Players || 0
220 | const variables = serverInfo.Variables || {}
221 |
222 | state.name = servername
223 | state.map = serverInfo['Current Map'] || ''
224 | state.password = !!variables.bPassworded
225 |
226 | state.numplayers = numplayers
227 | state.maxplayers = variables['Player Limit'] || 0
228 |
229 | state.raw = serverInfo
230 | state.version = serverInfo['Game Version'] || ''
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/protocols/renegadexmaster.js:
--------------------------------------------------------------------------------
1 | import renegadex from './renegadex.js'
2 |
3 | /**
4 | * Implements the protocol for retrieving a master list for Renegade X, an UnrealEngine3 based game
5 | */
6 | export default class renegadexmaster extends renegadex {
7 | async run (state) {
8 | const servers = await this.getMasterServerList()
9 |
10 | // pass processed servers as raw list
11 | state.raw.servers = servers.map((serverInfo) => {
12 | // TODO: may use any other deep-copy method like structuredClone() (in Node.js 17+)
13 | // or use a method of Core to retrieve a clean state
14 | const serverState = JSON.parse(JSON.stringify(state))
15 |
16 | // set state properties based on received server info
17 | this.populateProperties(serverState, serverInfo)
18 | return serverState
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/protocols/renown.js:
--------------------------------------------------------------------------------
1 | import Epic from './epic.js'
2 |
3 | export default class renown extends Epic {
4 | constructor () {
5 | super()
6 |
7 | // OAuth2 credentials provided by the game developer.
8 | this.clientId = 'xyza7891XjE03sj3B404z0iQfQ4efjUj'
9 | this.clientSecret = '2YY7STKJ5wJuFIyR8TaXaBPfhHZiAY13YuNPSIn+0WY'
10 | this.deploymentId = '472a8286c61c408ca5ca1ec0401f07b7'
11 | this.authByExternalToken = true
12 | }
13 |
14 | async run (state) {
15 | await super.run(state)
16 | state.name = state.raw.attributes.SERVERNAME_s
17 | state.password = state.raw.attributes.PASSWORD_b
18 | state.version = state.raw.attributes.SERVERVERSION_s
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/protocols/rfactor.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class rfactor extends Core {
4 | async run (state) {
5 | const buffer = await this.udpSend('rF_S', b => b)
6 | const reader = this.reader(buffer)
7 |
8 | state.raw.gamename = this.readString(reader, 8)
9 | state.raw.fullUpdate = reader.uint(1)
10 | state.raw.region = reader.uint(2)
11 | state.raw.ip = reader.part(4)
12 | state.raw.size = reader.uint(2)
13 | state.version = reader.uint(2)
14 | state.raw.versionRaceCast = reader.uint(2)
15 | state.gamePort = reader.uint(2)
16 | state.raw.queryPort = reader.uint(2)
17 | state.raw.game = this.readString(reader, 20)
18 | state.name = this.readString(reader, 28)
19 | state.map = this.readString(reader, 32)
20 | state.raw.motd = this.readString(reader, 96)
21 | state.raw.packedAids = reader.uint(2)
22 | state.raw.ping = reader.uint(2)
23 | state.raw.packedFlags = reader.uint(1)
24 | state.raw.rate = reader.uint(1)
25 | state.numplayers = reader.uint(1)
26 | state.maxplayers = reader.uint(1)
27 | state.raw.bots = reader.uint(1)
28 | state.raw.packedSpecial = reader.uint(1)
29 | state.raw.damage = reader.uint(1)
30 | state.raw.packedRules = reader.uint(2)
31 | state.raw.credits1 = reader.uint(1)
32 | state.raw.credits2 = reader.uint(2)
33 | this.logger.debug(reader.offset())
34 | state.raw.time = reader.uint(2)
35 | state.raw.laps = reader.uint(2) / 16
36 | reader.skip(3)
37 | state.raw.vehicles = reader.string()
38 |
39 | state.password = !!(state.raw.packedSpecial & 2)
40 | state.raw.raceCast = !!(state.raw.packedSpecial & 4)
41 | state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
42 |
43 | const aids = [
44 | 'TractionControl',
45 | 'AntiLockBraking',
46 | 'StabilityControl',
47 | 'AutoShifting',
48 | 'AutoClutch',
49 | 'Invulnerability',
50 | 'OppositeLock',
51 | 'SteeringHelp',
52 | 'BrakingHelp',
53 | 'SpinRecovery',
54 | 'AutoPitstop'
55 | ]
56 | state.raw.aids = []
57 | for (let offset = 0; offset < aids.length; offset++) {
58 | if (state.packedAids && (1 << offset)) {
59 | state.raw.aids.push(aids[offset])
60 | }
61 | }
62 | }
63 |
64 | // Consumes bytesToConsume, but only returns string up to the first null
65 | readString (reader, bytesToConsume) {
66 | const consumed = reader.part(bytesToConsume)
67 | return this.reader(consumed).string()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/protocols/samp.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class samp extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'win1252'
7 | this.magicHeader = 'SAMP'
8 | this.responseMagicHeader = null
9 | this.isVcmp = false
10 | this.isOmp = false
11 | }
12 |
13 | async run (state) {
14 | // read info
15 | {
16 | const reader = await this.sendPacket('i')
17 | if (this.isVcmp) {
18 | const consumed = reader.part(12)
19 | state.version = this.reader(consumed).string()
20 | }
21 | state.password = !!reader.uint(1)
22 | state.numplayers = reader.uint(2)
23 | state.maxplayers = reader.uint(2)
24 | state.name = reader.pascalString(4)
25 | state.raw.gamemode = reader.pascalString(4)
26 | state.raw.map = reader.pascalString(4)
27 | }
28 |
29 | // read rules
30 | if (!this.isVcmp) {
31 | const reader = await this.sendPacket('r')
32 | const ruleCount = reader.uint(2)
33 | state.raw.rules = {}
34 | for (let i = 0; i < ruleCount; i++) {
35 | const key = reader.pascalString(1)
36 | const value = reader.pascalString(1)
37 | state.raw.rules[key] = value
38 | if ('version' in state.raw.rules) state.version = state.raw.rules.version
39 | }
40 | }
41 |
42 | // read players
43 | // don't even bother if > 100 players, because the server won't respond
44 | if (state.numplayers < 100) {
45 | if (this.isVcmp || this.isOmp) {
46 | const reader = await this.sendPacket('c', true)
47 | if (reader !== null) {
48 | const playerCount = reader.uint(2)
49 | for (let i = 0; i < playerCount; i++) {
50 | const player = {}
51 | player.name = reader.pascalString(1)
52 | player.score = reader.int(4)
53 | state.players.push(player)
54 | }
55 | }
56 | } else {
57 | const reader = await this.sendPacket('d', true)
58 | if (reader !== null) {
59 | const playerCount = reader.uint(2)
60 | for (let i = 0; i < playerCount; i++) {
61 | const player = {}
62 | player.id = reader.uint(1)
63 | player.name = reader.pascalString(1)
64 | player.score = reader.int(4)
65 | player.ping = reader.uint(4)
66 | state.players.push(player)
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
73 | async sendPacket (type, allowTimeout) {
74 | const outBuffer = Buffer.alloc(11)
75 | outBuffer.write(this.magicHeader, 0, 4)
76 | const ipSplit = this.options.address.split('.')
77 | outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
78 | outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
79 | outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
80 | outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
81 | outBuffer.writeUInt16LE(this.options.port, 8)
82 | outBuffer.writeUInt8(type.charCodeAt(0), 10)
83 |
84 | const checkBuffer = Buffer.from(outBuffer)
85 | if (this.responseMagicHeader) {
86 | checkBuffer.write(this.responseMagicHeader, 0, 4)
87 | }
88 |
89 | return await this.udpSend(
90 | outBuffer,
91 | (buffer) => {
92 | const reader = this.reader(buffer)
93 | for (let i = 0; i < checkBuffer.length; i++) {
94 | if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
95 | }
96 | return reader
97 | },
98 | () => {
99 | if (allowTimeout) {
100 | return null
101 | }
102 | }
103 | )
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/protocols/satisfactory.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class satisfactory extends Core {
4 | constructor () {
5 | super()
6 |
7 | // Don't use the tcp ping probing
8 | this.usedTcp = true
9 | }
10 |
11 | async run (state) {
12 | const packet = Buffer.from([0xD5, 0xF6, 0, 1, 5, 5, 5, 5, 5, 5, 5, 5, 1])
13 | const response = await this.udpSend(packet, packet => {
14 | const reader = this.reader(packet)
15 | const header = reader.part(4)
16 | if (header.equals(Buffer.from([0xD5, 0xF6, 1, 2]))) return
17 | reader.skip(8) // skip the cookie
18 | return reader
19 | })
20 |
21 | state.raw.serverState = response.int(1)
22 | state.version = response.int(4).toString()
23 | state.raw.serverFlags = response.int(8)
24 |
25 | const subStatesCount = response.int(1)
26 | response.skip(subStatesCount * 3)
27 |
28 | const nameLength = response.int(2)
29 | state.name = response.part(nameLength).toString('utf-8')
30 |
31 | try {
32 | await this.doHttpApiQueries(state)
33 | } catch (e) {
34 | this.logger.debug('HTTP API query failed.')
35 | this.logger.debug(e)
36 | }
37 | }
38 |
39 | async doHttpApiQueries (state) {
40 | const headers = {
41 | 'Content-Type': 'application/json'
42 | }
43 |
44 | /**
45 | * Satisfactory servers unless specified use self-signed certificates for the HTTPS API.
46 | * Because of this we default the `rejectUnauthorized` flag to `false` unless set.
47 | * For more information see GAMES_LIST.md
48 | */
49 | if (!this.options.rejectUnauthorized) this.options.rejectUnauthorized = false
50 |
51 | let token = this.options.token
52 | if (!token) {
53 | const tokenRequestJson = {
54 | function: 'PasswordlessLogin',
55 | data: {
56 | MinimumPrivilegeLevel: 'Client'
57 | }
58 | }
59 |
60 | const response = await this.queryInfo(tokenRequestJson, headers)
61 | token = response.authenticationToken
62 | }
63 |
64 | const queryJson = {
65 | function: 'QueryServerState'
66 | }
67 |
68 | const queryResponse = await this.queryInfo(queryJson, {
69 | ...headers,
70 | Authorization: `Bearer ${token}`
71 | })
72 |
73 | /**
74 | * Satisfactory API cannot pull Server Name at the moment, see QA and vote for fix here
75 | * https://questions.satisfactorygame.com/post/66ebebad772a987f4a8b9ef8
76 | */
77 |
78 | state.numplayers = queryResponse.serverGameState.numConnectedPlayers
79 | state.maxplayers = queryResponse.serverGameState.playerLimit
80 | state.raw.http = queryResponse
81 | }
82 |
83 | async queryInfo (json, headers) {
84 | const url = `https://${this.options.host}:${this.options.port}/api/v1/`
85 |
86 | this.logger.debug(`POST: ${url}`)
87 |
88 | const response = await this.request({
89 | url,
90 | json,
91 | headers,
92 | method: 'POST',
93 | responseType: 'json',
94 | https: {
95 | minVersion: 'TLSv1.2',
96 | rejectUnauthorized: this.options.rejectUnauthorized
97 | }
98 | })
99 |
100 | if (response.data == null) {
101 | throw new Error('Unable to retrieve data from server')
102 | } else {
103 | return response.data
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/protocols/savage2.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class savage2 extends Core {
4 | async run (state) {
5 | const buffer = await this.udpSend('\x01', b => b)
6 | const reader = this.reader(buffer)
7 |
8 | reader.skip(12)
9 | state.name = this.stripColorCodes(reader.string())
10 | state.numplayers = reader.uint(1)
11 | state.maxplayers = reader.uint(1)
12 | state.raw.time = reader.string()
13 | state.map = reader.string()
14 | state.raw.nextmap = reader.string()
15 | state.raw.location = reader.string()
16 | state.raw.minplayers = reader.uint(1)
17 | state.raw.gametype = reader.string()
18 | state.version = reader.string()
19 | state.raw.minlevel = reader.uint(1)
20 | }
21 |
22 | stripColorCodes (str) {
23 | return this.options.stripColors ? str.replace(/\^./g, '') : str
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/protocols/sdtd.js:
--------------------------------------------------------------------------------
1 | import Valve from './valve.js'
2 | import { Players } from '../lib/Results.js'
3 |
4 | const playerRegex = /(?<=id=\d+,\s*)(?\S[^,]*)(?=,)/
5 | const gameVersionsRegex = /V \d+\.\d+(?: \(b\d+\))?/g
6 | const modRegex = /^Mod\s+([^:]+):\s*([\d.]+)$/
7 | const dateTimeRegex = /Day\s+(\d+),\s*(\d{2}:\d{2})/
8 |
9 | const sanitizeTelnetResponse = response => {
10 | return response
11 | .split(/\r?\n/)
12 | .map(l => l.replace(/\r$/, '').trim())
13 | .filter(l => l.length > 0)
14 | }
15 |
16 | export default class sdtd extends Valve {
17 | async run (state) {
18 | await super.run(state)
19 | await this.telnetCalls(state)
20 | }
21 |
22 | async telnetCalls (state) {
23 | const telnetPort = this.options.telnetPort
24 | const telnetPassword = this.options.telnetPassword
25 |
26 | if (!telnetPort || !telnetPassword) {
27 | this.logger.debug('No telnet args given, skipping.')
28 | return
29 | }
30 |
31 | if (!this.options.requestPlayers && !this.options.moreData) {
32 | return
33 | }
34 |
35 | await this.telnetConnect({
36 | port: telnetPort,
37 | password: telnetPassword,
38 | passwordPrompt: /Please enter password:/i,
39 | shellPrompt: /\r\n$/,
40 | echoLines: 0
41 | })
42 |
43 | if (this.options.requestPlayers) {
44 | await this.telnetCallPlayers(state)
45 | }
46 |
47 | if (this.options.moreData) {
48 | await this.telnetMoreData(state)
49 | }
50 |
51 | await this.telnetClose()
52 | }
53 |
54 | async telnetCallPlayers (state) {
55 | const playersResponse = await this.telnetExecute('listplayers')
56 | state.players = new Players()
57 | for (const possiblePlayerLine of sanitizeTelnetResponse(playersResponse)) {
58 | const match = possiblePlayerLine.match(playerRegex)
59 |
60 | const name = match?.groups?.name
61 | if (name) {
62 | state.players.push({
63 | name,
64 | responseLine: possiblePlayerLine
65 | })
66 | }
67 | }
68 |
69 | state.raw.telnetPlayersResponse = playersResponse
70 | }
71 |
72 | async telnetMoreData (state) {
73 | const gettimeResponse = await this.telnetExecute('gettime')
74 | const dateTime = sanitizeTelnetResponse(gettimeResponse)[0] || ''
75 | const match = dateTime.match(dateTimeRegex)
76 | if (match) {
77 | state.raw.day = Number(match[1])
78 | state.raw.time = match[2]
79 | state.raw.hordeDay = state.raw.day % 7 === 0
80 | } else {
81 | state.raw.hordeDay = false
82 | }
83 |
84 | state.raw.telnetGettimeResponse = gettimeResponse
85 |
86 | const versionResponse = await this.telnetExecute('version')
87 | const versions = sanitizeTelnetResponse(versionResponse)
88 | const gameVersions = versions[0] || ''
89 | const gameVersionsMatch = gameVersions.match(gameVersionsRegex)
90 | if (gameVersionsMatch) {
91 | state.raw.gameVersion = gameVersionsMatch[0]
92 | state.raw.compatibilityVersion = gameVersionsMatch[1]
93 | }
94 |
95 | const mods = []
96 | for (const possibleMod of versions.slice(1)) {
97 | const match = possibleMod.match(modRegex)
98 | if (match) {
99 | mods.push({
100 | name: match[1],
101 | version: match[2]
102 | })
103 | }
104 | }
105 |
106 | state.raw.mods = mods
107 | state.raw.telnetVersionResponse = versionResponse
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/protocols/soldat.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | const extractValue = (text, regex, defaultValue, parser = (val) => val) => {
4 | const match = text.match(regex)
5 | return match ? parser(match[1] || defaultValue) : defaultValue
6 | }
7 |
8 | export default class soldat extends Core {
9 | async run (state) {
10 | const data = await this.withTcp(async socket => {
11 | return await this.tcpSend(socket, 'STARTFILES\r\nlogs/gamestat.txt\r\nENDFILES\r\n', (data) => {
12 | const asString = data.toString()
13 | if (asString.endsWith('\r\n') && !asString.endsWith('ENDFILES\r\n')) {
14 | return undefined
15 | }
16 | return data
17 | })
18 | })
19 |
20 | const string = data.toString()
21 |
22 | state.numplayers = extractValue(string, /Players:\s*(\d+)/, 0, Number)
23 | state.map = extractValue(string, /Map:\s*(.+)/, '')
24 |
25 | const lines = string.trim().split('\n')
26 | const playersIndex = lines.findIndex(line => line.startsWith('Players list'))
27 |
28 | if (playersIndex > -1) {
29 | for (let i = playersIndex + 1; i < lines.length - 1; i += 5) {
30 | state.players.push({
31 | name: lines[i].trim(),
32 | raw: {
33 | kills: parseInt(lines[i + 1].trim()),
34 | deaths: parseInt(lines[i + 2].trim()),
35 | team: parseInt(lines[i + 3].trim()),
36 | ping: parseInt(lines[i + 4].trim())
37 | }
38 | })
39 | }
40 | }
41 |
42 | state.raw.response = string
43 | state.raw.gamemode = extractValue(string, /Gamemode:\s*(.+)/, '')
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/protocols/starmade.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class starmade extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.byteorder = 'be'
8 | }
9 |
10 | async run (state) {
11 | const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
12 |
13 | const payload = await this.withTcp(async socket => {
14 | return await this.tcpSend(socket, b, buffer => {
15 | if (buffer.length < 12) return
16 | const reader = this.reader(buffer)
17 | const packetLength = reader.uint(4)
18 | this.logger.debug('Received packet length: ' + packetLength)
19 | const timestamp = reader.uint(8).toString()
20 | this.logger.debug('Received timestamp: ' + timestamp)
21 | if (reader.remaining() < packetLength || reader.remaining() < 5) return
22 |
23 | const checkId = reader.uint(1)
24 | const packetId = reader.uint(2)
25 | const commandId = reader.uint(1)
26 | const type = reader.uint(1)
27 |
28 | this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
29 | if (checkId !== 0x2a) return
30 |
31 | return reader.rest()
32 | })
33 | })
34 |
35 | const reader = this.reader(payload)
36 |
37 | const data = []
38 | state.raw.data = data
39 |
40 | while (!reader.done()) {
41 | const mark = reader.uint(1)
42 | if (mark === 1) {
43 | // signed int
44 | data.push(reader.int(4))
45 | } else if (mark === 3) {
46 | // float
47 | data.push(reader.float())
48 | } else if (mark === 4) {
49 | // string
50 | data.push(reader.pascalString(2))
51 | } else if (mark === 6) {
52 | // byte
53 | data.push(reader.uint(1))
54 | }
55 | }
56 |
57 | this.logger.debug('Received raw data array', data)
58 |
59 | if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
60 | if (typeof data[1] === 'string') state.version = data[1]
61 | if (typeof data[2] === 'string') state.name = data[2]
62 | if (typeof data[3] === 'string') state.raw.description = data[3]
63 | if (typeof data[4] === 'number') state.raw.startTime = data[4]
64 | if (typeof data[5] === 'number') state.numplayers = data[5]
65 | if (typeof data[6] === 'number') state.maxplayers = data[6]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/protocols/starsiege.js:
--------------------------------------------------------------------------------
1 | import tribes1 from './tribes1.js'
2 |
3 | export default class starsiege extends tribes1 {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.requestByte = 0x72
8 | this.responseByte = 0x73
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/protocols/teamspeak2.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class teamspeak2 extends Core {
4 | async run (state) {
5 | const queryPort = this.options.teamspeakQueryPort || 51234
6 |
7 | await this.withTcp(async socket => {
8 | {
9 | const data = await this.sendCommand(socket, 'sel ' + this.options.port)
10 | if (data !== '[TS]') throw new Error('Invalid header')
11 | }
12 |
13 | {
14 | const data = await this.sendCommand(socket, 'si')
15 | for (const line of data.split('\r\n')) {
16 | const equals = line.indexOf('=')
17 | const key = equals === -1 ? line : line.substring(0, equals)
18 | const value = equals === -1 ? '' : line.substring(equals + 1)
19 | state.raw[key] = value
20 | }
21 | if ('server_name' in state.raw) state.name = state.raw.server_name
22 | }
23 |
24 | {
25 | const data = await this.sendCommand(socket, 'pl')
26 | const split = data.split('\r\n')
27 | const fields = split.shift().split('\t')
28 | for (const line of split) {
29 | const split2 = line.split('\t')
30 | const player = {}
31 | split2.forEach((value, i) => {
32 | let key = fields[i]
33 | if (!key) return
34 | if (key === 'nick') key = 'name'
35 | const m = value.match(/^"(.*)"$/)
36 | if (m) value = m[1]
37 | player[key] = value
38 | })
39 | state.players.push(player)
40 | }
41 | state.numplayers = state.players.length
42 | }
43 |
44 | {
45 | const data = await this.sendCommand(socket, 'cl')
46 | const split = data.split('\r\n')
47 | const fields = split.shift().split('\t')
48 | state.raw.channels = []
49 | for (const line of split) {
50 | const split2 = line.split('\t')
51 | const channel = {}
52 | split2.forEach((value, i) => {
53 | const key = fields[i]
54 | if (!key) return
55 | const m = value.match(/^"(.*)"$/)
56 | if (m) value = m[1]
57 | channel[key] = value
58 | })
59 | state.raw.channels.push(channel)
60 | }
61 | }
62 | }, queryPort)
63 | }
64 |
65 | async sendCommand (socket, cmd) {
66 | return await this.tcpSend(socket, cmd + '\x0A', buffer => {
67 | if (buffer.length < 6) return
68 | if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
69 | return buffer.slice(0, -6).toString()
70 | })
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/protocols/teamspeak3.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class teamspeak3 extends Core {
4 | async run (state) {
5 | const queryPort = this.options.teamspeakQueryPort || 10011
6 |
7 | await this.withTcp(async socket => {
8 | {
9 | const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
10 | const split = data.split('\n\r')
11 | if (split[0] !== 'TS3') throw new Error('Invalid header')
12 | }
13 |
14 | {
15 | const data = await this.sendCommand(socket, 'serverinfo')
16 | state.raw = data[0]
17 | if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
18 | if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
19 | if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline
20 | if ('virtualserver_version' in state.raw) state.version = state.raw.virtualserver_version
21 | }
22 |
23 | {
24 | const list = await this.sendCommand(socket, 'clientlist')
25 | for (const client of list) {
26 | client.name = client.client_nickname
27 | delete client.client_nickname
28 | if (client.client_type === '0') {
29 | state.players.push(client)
30 | }
31 | }
32 | }
33 |
34 | {
35 | const data = await this.sendCommand(socket, 'channellist -topic')
36 | state.raw.channels = data
37 | }
38 | }, queryPort)
39 | }
40 |
41 | async sendCommand (socket, cmd, raw) {
42 | const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
43 | if (buffer.length < 21) return
44 | if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
45 | return buffer.slice(0, -21).toString()
46 | })
47 |
48 | if (raw) {
49 | return body
50 | } else {
51 | const segments = body.split('|')
52 | const out = []
53 | for (const line of segments) {
54 | const split = line.split(' ')
55 | const unit = {}
56 | for (const field of split) {
57 | const equals = field.indexOf('=')
58 | const key = equals === -1 ? field : field.substring(0, equals)
59 | const value = equals === -1
60 | ? ''
61 | : field.substring(equals + 1)
62 | .replace(/\\s/g, ' ').replace(/\\\//g, '/')
63 | unit[key] = value
64 | }
65 | out.push(unit)
66 | }
67 | return out
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/protocols/terraria.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class terraria extends Core {
4 | async run (state) {
5 | const json = await this.request({
6 | url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status',
7 | searchParams: {
8 | players: 'true',
9 | token: this.options.token
10 | },
11 | responseType: 'json'
12 | })
13 |
14 | if (json.status !== '200') throw new Error('Invalid status')
15 |
16 | state.raw = json
17 |
18 | for (const one of json.players) {
19 | state.players.push({ name: one.nickname, team: one.team })
20 | }
21 |
22 | state.name = json.name
23 | state.gamePort = json.port
24 | state.numplayers = json.playercount
25 | state.maxplayers = json.maxplayers
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/protocols/theisleevrima.js:
--------------------------------------------------------------------------------
1 | import Epic from './epic.js'
2 |
3 | export default class theisleevrima extends Epic {
4 | constructor () {
5 | super()
6 |
7 | // OAuth2 credentials extracted from The Isle Evrima files.
8 | this.clientId = 'xyza7891gk5PRo3J7G9puCJGFJjmEguW'
9 | this.clientSecret = 'pKWl6t5i9NJK8gTpVlAxzENZ65P8hYzodV8Dqe5Rlc8'
10 | this.deploymentId = '6db6bea492f94b1bbdfcdfe3e4f898dc'
11 | }
12 |
13 | async run (state) {
14 | await super.run(state)
15 | state.name = state.raw.attributes.SERVERNAME_s
16 | state.map = state.raw.attributes.MAP_NAME_s
17 | state.version = state.raw.attributes.SERVER_VERSION_s
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/protocols/toxikk.js:
--------------------------------------------------------------------------------
1 | import valve from './valve.js'
2 | // import { TranslateMapUT3 } from './ut3.js'
3 |
4 | export const TranslateMapToxikk = Object.freeze({
5 | // add UT3 values
6 | // Toxikk is using UDK which includes basic implementation of UT3
7 | // ...TranslateMapUT3,
8 |
9 | // Old UT3/UDK properties
10 | p1073741825: 'map', // UC Source='CRZMapName'
11 | p1073741826: 'game', // UC Source='CustomGameMode'
12 | p268435704: 'frag_limit', // UC Source='TimeLimit'
13 | p268435705: 'time_limit', // UC Source='TimeLimit'
14 | p268435703: 'numbots',
15 | p1073741827: 'servername', // UC Source='ServerDescription'
16 | p268435717: false, // 'stock_mutators' // UC Source='OfficialMutators' // Note: "EpicMutators" are bit-masked and require Full UE3 license (C++) to flag mutators, stock mutators could be always 0, ignore for now
17 | p1073741828: 'custom_mutators', // UC Source='CustomMutators'
18 |
19 | // Toxikk specific localized settings (commented name is based on source code)
20 | s32779: 'GameMode', // 8=Custom, anything else is UT3/UDK
21 | s0: 'bot_skill', // UC Source='BotSkill', // 0=Ridiculous 1=Novice 2=Average 3=Experienced 4=Adept 5=Masterful 6=Inhuman 7=Godlike
22 | s1: false, // UC Source='Map' // Note: set as '0' mostly, generally the index will state the official map, ignore for now
23 | s6: 'pure_server', // UC Source='PureServer', // bool
24 | s7: 'password', // UC Source='LockedServer', // bool
25 | s8: 'vs_bots', // UC Source='VsBots', // 0=Disabled 1="2:1" 2="1:1" 3="2:3" 4="1:2" 5="1:3" 6="1:4"
26 | s9: 'Campaign', // bool
27 | s10: 'force_respawn', // UC Source='ForceRespawn', // bool
28 | s11: 'AllowKeyboard', // bool
29 | s12: 'IsFullServer', // bool
30 | s13: 'IsEmptyServer', // bool
31 | s14: 'IsDedicated', // bool
32 | s15: 'RankedServer', // 0=UnRanked 1=Ranked
33 | s16: 'OnlyFullGamePlayers', // bool
34 | s17: 'IgnoredByMatchmaking', // bool
35 | s18: 'OfficialServer', // 0=Community 1=Official
36 | s19: 'ModdingLevel', // 0=Unmodded 1=Server Modded 2=Client Modded
37 |
38 | // Toxikk properties
39 | p268435706: 'MaxPlayers',
40 | p268435707: 'MinNetPlayers',
41 | p268435708: 'MinSkillClass',
42 | p268435709: 'MaxSkillClass',
43 | p1073741829: 'PLAYERIDS1',
44 | p1073741830: 'PLAYERIDS2',
45 | p1073741831: 'PLAYERIDS3',
46 | p1073741832: 'PLAYERNAMES1',
47 | p1073741833: 'PLAYERNAMES2',
48 | p1073741834: 'PLAYERNAMES3',
49 | p1073741837: 'PLAYERSCS',
50 | p1073741838: 'PLAYERBadgeRanks',
51 | p1073741839: 'GameVersion',
52 | p1073741840: 'GameVoteList'
53 | })
54 |
55 | /**
56 | * Implements the protocol for Toxikk, an UnrealEngine3 based game,
57 | * using Valve protocol for query with additional UE3 properties/settings parsing
58 | */
59 | export default class toxikk extends valve {
60 | async run (state) {
61 | if (!this.options.port) this.options.port = 27015
62 | await this.queryInfo(state)
63 | await this.queryChallenge()
64 | await this.queryPlayers(state)
65 | await this.queryRules(state)
66 |
67 | this.processQueryInfo(state)
68 | await this.cleanup(state)
69 | }
70 |
71 | /** @override */
72 | async cleanup (state) {
73 | // valve protocol attempts to put "hidden players" into player/bot array
74 | // the bot data is not properly queried, therefore prevent push players into bots-array
75 | const originalNumBots = state.raw.numbots
76 | state.raw.numbots = null
77 | super.cleanup(state)
78 | state.raw.numbots = originalNumBots
79 | }
80 |
81 | async queryRules (state) {
82 | if (!this.options.requestRules) {
83 | return
84 | }
85 |
86 | const rules = {}
87 | this.logger.debug('Requesting rules ...')
88 |
89 | const b = await this.sendPacket(0x56, null, 0x45, true)
90 | if (b === null && !this.options.requestRulesRequired) return // timed out - the server probably has rules disabled
91 |
92 | const reader = this.reader(b)
93 | const num = reader.uint(2)
94 | for (let i = num - 1; i > 0 && !reader.done(); i--) {
95 | const key = reader.string()
96 | const value = reader.string()
97 | if (reader.remaining() <= 0) {
98 | // data might be corrupt in this case, keep existing rules
99 | break
100 | }
101 |
102 | rules[key] = value
103 | }
104 |
105 | state.raw.rules = rules
106 | }
107 |
108 | processQueryInfo (state) {
109 | // move raw rules into root-raw object and attempt to translate properties
110 | Object.assign(state.raw, state.raw.rules)
111 | this.translate(state.raw, TranslateMapToxikk)
112 |
113 | const split = (a) => {
114 | let s = a.split('\x1c')
115 | s = s.filter((e) => { return e })
116 | return s
117 | }
118 | if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
119 | if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
120 | if ('map' in state.raw) state.map ??= state.raw.map
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/protocols/tribes1.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class tribes1 extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | this.requestByte = 0x62
8 | this.responseByte = 0x63
9 | this.challenge = 0x01
10 | }
11 |
12 | async run (state) {
13 | const query = Buffer.alloc(3)
14 | query.writeUInt8(this.requestByte, 0)
15 | query.writeUInt16LE(this.challenge, 1)
16 | const reader = await this.udpSend(query, (buffer) => {
17 | const reader = this.reader(buffer)
18 | const responseByte = reader.uint(1)
19 | if (responseByte !== this.responseByte) {
20 | this.logger.debug('Unexpected response byte')
21 | return
22 | }
23 | const challenge = reader.uint(2)
24 | if (challenge !== this.challenge) {
25 | this.logger.debug('Unexpected challenge')
26 | return
27 | }
28 | const requestByte = reader.uint(1)
29 | if (requestByte !== this.requestByte) {
30 | this.logger.debug('Unexpected request byte')
31 | return
32 | }
33 | return reader
34 | })
35 |
36 | state.raw.gametype = this.readString(reader)
37 | const isStarsiege2009 = state.raw.gametype === 'Starsiege'
38 | state.version = this.readString(reader)
39 | state.name = this.readString(reader)
40 |
41 | if (isStarsiege2009) {
42 | state.password = !!reader.uint(1)
43 | state.raw.dedicated = !!reader.uint(1)
44 | state.raw.dropInProgress = !!reader.uint(1)
45 | state.raw.gameInProgress = !!reader.uint(1)
46 | state.numplayers = reader.uint(4)
47 | state.maxplayers = reader.uint(4)
48 | state.raw.teamPlay = reader.uint(1)
49 | state.map = this.readString(reader)
50 | state.raw.cpuSpeed = reader.uint(2)
51 | state.raw.factoryVeh = reader.uint(1)
52 | state.raw.allowTecmix = reader.uint(1)
53 | state.raw.spawnLimit = reader.uint(4)
54 | state.raw.fragLimit = reader.uint(4)
55 | state.raw.timeLimit = reader.uint(4)
56 | state.raw.techLimit = reader.uint(4)
57 | state.raw.combatLimit = reader.uint(4)
58 | state.raw.massLimit = reader.uint(4)
59 | state.raw.playersSent = reader.uint(4)
60 | const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
61 | while (!reader.done()) {
62 | const player = {}
63 | player.name = this.readString(reader)
64 | const teamId = reader.uint(1)
65 | const team = teams[teamId]
66 | if (team) player.team = teams[teamId]
67 | }
68 | return
69 | }
70 |
71 | state.raw.dedicated = !!reader.uint(1)
72 | state.password = !!reader.uint(1)
73 | state.raw.playerCount = reader.uint(1)
74 | state.maxplayers = reader.uint(1)
75 | state.raw.cpuSpeed = reader.uint(2)
76 | state.raw.mod = this.readString(reader)
77 | state.raw.type = this.readString(reader)
78 | state.map = this.readString(reader)
79 | state.raw.motd = this.readString(reader)
80 | state.raw.teamCount = reader.uint(1)
81 |
82 | const teamFields = this.readFieldList(reader)
83 | const playerFields = this.readFieldList(reader)
84 |
85 | state.raw.teams = []
86 | for (let i = 0; i < state.raw.teamCount; i++) {
87 | const teamName = this.readString(reader)
88 | const teamValues = this.readValues(reader)
89 |
90 | const teamInfo = {}
91 | for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
92 | let key = teamFields[i]
93 | let value = teamValues[i]
94 | if (key === 'ultra_base') key = 'name'
95 | if (value === '%t') value = teamName
96 | if (['score', 'players'].includes(key)) value = parseInt(value)
97 | teamInfo[key] = value
98 | }
99 | state.raw.teams.push(teamInfo)
100 | }
101 |
102 | for (let i = 0; i < state.raw.playerCount; i++) {
103 | const ping = reader.uint(1) * 4
104 | const packetLoss = reader.uint(1)
105 | const teamNum = reader.uint(1)
106 | const name = this.readString(reader)
107 | const playerValues = this.readValues(reader)
108 |
109 | const playerInfo = {}
110 | for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
111 | const key = playerFields[i]
112 | let value = playerValues[i]
113 | if (value === '%p') value = ping
114 | if (value === '%l') value = packetLoss
115 | if (value === '%t') value = teamNum
116 | if (value === '%n') value = name
117 | if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
118 | if (key === 'team') {
119 | const teamId = parseInt(value)
120 | if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
121 | value = state.raw.teams[teamId].name
122 | } else {
123 | continue
124 | }
125 | }
126 | playerInfo[key] = value
127 | }
128 | state.players.push(playerInfo)
129 | }
130 | }
131 |
132 | readFieldList (reader) {
133 | const str = this.readString(reader)
134 | if (!str) return []
135 | return ('?' + str)
136 | .split('\t')
137 | .map((a) => a.substring(1).trim().toLowerCase())
138 | .map((a) => a === 'team name' ? 'name' : a)
139 | .map((a) => a === 'player name' ? 'name' : a)
140 | }
141 |
142 | readValues (reader) {
143 | const str = this.readString(reader)
144 | if (!str) return []
145 | return str
146 | .split('\t')
147 | .map((a) => a.trim())
148 | }
149 |
150 | readString (reader) {
151 | return reader.pascalString(1)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/protocols/tribes1master.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | /** Unsupported -- use at your own risk!! */
4 |
5 | export default class tribes1master extends Core {
6 | constructor () {
7 | super()
8 | this.encoding = 'latin1'
9 | }
10 |
11 | async run (state) {
12 | const queryBuffer = Buffer.from([
13 | 0x10, // standard header
14 | 0x03, // dump servers
15 | 0xff, // ask for all packets
16 | 0x00, // junk
17 | 0x01, 0x02 // challenge
18 | ])
19 |
20 | const parts = new Map()
21 | let total = 0
22 | const full = await this.udpSend(queryBuffer, (buffer) => {
23 | const reader = this.reader(buffer)
24 | const header = reader.uint(2)
25 | if (header !== 0x0610) {
26 | this.logger.debug('Header response does not match: ' + header.toString(16))
27 | return
28 | }
29 | const num = reader.uint(1)
30 | const t = reader.uint(1)
31 | if (t <= 0 || (total > 0 && t !== total)) {
32 | throw new Error('Conflicting packet total: ' + t)
33 | }
34 | total = t
35 |
36 | if (num < 1 || num > total) {
37 | this.logger.debug('Invalid packet number: ' + num + ' ' + total)
38 | return
39 | }
40 | if (parts.has(num)) {
41 | this.logger.debug('Duplicate part: ' + num)
42 | return
43 | }
44 |
45 | reader.skip(2) // challenge (0x0201)
46 | reader.skip(2) // always 0x6600
47 | parts.set(num, reader.rest())
48 |
49 | if (parts.size === total) {
50 | const ordered = []
51 | for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
52 | return Buffer.concat(ordered)
53 | }
54 | })
55 |
56 | const fullReader = this.reader(full)
57 | state.raw.name = this.readString(fullReader)
58 | state.raw.motd = this.readString(fullReader)
59 |
60 | state.raw.servers = []
61 | while (!fullReader.done()) {
62 | fullReader.skip(1) // junk ?
63 | const count = fullReader.uint(1)
64 | for (let i = 0; i < count; i++) {
65 | const six = fullReader.uint(1)
66 | if (six !== 6) {
67 | throw new Error('Expecting 6')
68 | }
69 | const ip = fullReader.uint(4)
70 | const port = fullReader.uint(2)
71 | const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
72 | state.raw.servers.push(ipStr + ':' + port)
73 | }
74 | }
75 | }
76 |
77 | readString (reader) {
78 | return reader.pascalString(1)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/protocols/unreal2.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class unreal2 extends Core {
4 | constructor () {
5 | super()
6 | this.encoding = 'latin1'
7 | }
8 |
9 | async run (state) {
10 | let extraInfoReader
11 | {
12 | const b = await this.sendPacket(0, true)
13 | const reader = this.reader(b)
14 | state.raw.serverid = reader.uint(4)
15 | state.raw.ip = this.readUnrealString(reader)
16 | state.gamePort = reader.uint(4)
17 | state.raw.queryport = reader.uint(4)
18 | state.name = this.readUnrealString(reader, true)
19 | state.map = this.readUnrealString(reader, true)
20 | state.raw.gametype = this.readUnrealString(reader, true)
21 | state.numplayers = reader.uint(4)
22 | state.maxplayers = reader.uint(4)
23 | this.logger.debug(log => {
24 | log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i))
25 | })
26 | extraInfoReader = reader
27 | }
28 |
29 | {
30 | const b = await this.sendPacket(1, true)
31 | const reader = this.reader(b)
32 | state.raw.mutators = []
33 | state.raw.rules = {}
34 | while (!reader.done()) {
35 | const key = this.readUnrealString(reader, true)
36 | const value = this.readUnrealString(reader, true)
37 | this.logger.debug(key + '=' + value)
38 | if (key === 'Mutator' || key === 'mutator') {
39 | state.raw.mutators.push(value)
40 | } else if (key || value) {
41 | if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) {
42 | state.raw.rules[key] += ',' + value
43 | } else {
44 | state.raw.rules[key] = value
45 | }
46 | }
47 | }
48 | if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' }
49 | if ('UTComp_Version' in state.raw.rules) { state.version = state.raw.rules.UTComp_Version }
50 | }
51 |
52 | if (state.raw.mutators.includes('KillingFloorMut') ||
53 | state.raw.rules['Num trader weapons'] ||
54 | state.raw.rules['Server Version'] === '1065'
55 | ) {
56 | // Killing Floor
57 | state.raw.wavecurrent = extraInfoReader.uint(4)
58 | state.raw.wavetotal = extraInfoReader.uint(4)
59 | state.raw.ping = extraInfoReader.uint(4)
60 | state.raw.flags = extraInfoReader.uint(4)
61 | state.raw.skillLevel = this.readUnrealString(extraInfoReader, true)
62 | } else {
63 | state.raw.ping = extraInfoReader.uint(4)
64 | // These fields were added in later revisions of unreal engine
65 | if (extraInfoReader.remaining() >= 8) {
66 | state.raw.flags = extraInfoReader.uint(4)
67 | state.raw.skill = this.readUnrealString(extraInfoReader, true)
68 | }
69 | }
70 |
71 | {
72 | const b = await this.sendPacket(2, false)
73 | const reader = this.reader(b)
74 |
75 | state.raw.scoreboard = {}
76 | while (!reader.done()) {
77 | const player = {}
78 | player.id = reader.uint(4)
79 | player.name = this.readUnrealString(reader, true)
80 | player.ping = reader.uint(4)
81 | player.score = reader.int(4)
82 | player.statsId = reader.uint(4)
83 | this.logger.debug(player)
84 |
85 | if (!player.id) {
86 | state.raw.scoreboard[player.name] = player.score
87 | } else if (!player.ping) {
88 | state.bots.push(player)
89 | } else {
90 | state.players.push(player)
91 | }
92 | }
93 | }
94 | }
95 |
96 | readUnrealString (reader, stripColor) {
97 | let length = reader.uint(1); let ucs2 = false
98 | if (length >= 0x80) {
99 | // This is flagged as a UCS-2 String
100 | length = (length & 0x7f) * 2
101 | ucs2 = true
102 |
103 | // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
104 | // not included in the length. Skip it if present (hopefully this never happens legitimately)
105 | const peek = reader.uint(1)
106 | if (peek !== 1) reader.skip(-1)
107 |
108 | this.logger.debug(log => {
109 | log('UCS2 STRING')
110 | log('UCS2 Length: ' + length)
111 | log(reader.buffer.slice(reader.i, reader.i + length))
112 | })
113 | }
114 |
115 | let out = ''
116 | if (ucs2) {
117 | out = reader.string({ encoding: 'ucs2', length })
118 | this.logger.debug('UCS2 String decoded: ' + out)
119 | } else if (length > 0) {
120 | out = reader.string()
121 | }
122 |
123 | // Sometimes the string has a null at the end (included with the length)
124 | // Strip it if present
125 | if (out.charCodeAt(out.length - 1) === 0) {
126 | out = out.substring(0, out.length - 1)
127 | }
128 |
129 | if (stripColor && this.options.stripColors) {
130 | out = out.replace(/\x1b...|[\x00-\x1a]/gus, '')
131 | }
132 |
133 | return out
134 | }
135 |
136 | async sendPacket (type, required) {
137 | const outbuffer = Buffer.from([0x79, 0, 0, 0, type])
138 |
139 | const packets = []
140 | return await this.udpSend(outbuffer, (buffer) => {
141 | const reader = this.reader(buffer)
142 | reader.uint(4) // header
143 | const iType = reader.uint(1)
144 | if (iType !== type) return
145 | packets.push(reader.rest())
146 | }, () => {
147 | if (!packets.length && required) return
148 | return Buffer.concat(packets)
149 | })
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/protocols/ut3.js:
--------------------------------------------------------------------------------
1 | import gamespy3 from './gamespy3.js'
2 |
3 | export default class ut3 extends gamespy3 {
4 | async run (state) {
5 | await super.run(state)
6 |
7 | this.translate(state.raw, {
8 | mapname: false,
9 | p1073741825: 'map',
10 | p1073741826: 'gametype',
11 | p1073741827: 'servername',
12 | p1073741828: 'custom_mutators',
13 | gamemode: 'joininprogress',
14 | s32779: 'gamemode',
15 | s0: 'bot_skill',
16 | s6: 'pure_server',
17 | s7: 'password',
18 | s8: 'vs_bots',
19 | s10: 'force_respawn',
20 | p268435704: 'frag_limit',
21 | p268435705: 'time_limit',
22 | p268435703: 'numbots',
23 | p268435717: 'stock_mutators',
24 | p1073741829: 'stock_mutators',
25 | s1: false,
26 | s9: false,
27 | s11: false,
28 | s12: false,
29 | s13: false,
30 | s14: false,
31 | p268435706: false,
32 | p268435968: false,
33 | p268435969: false
34 | })
35 |
36 | const split = (a) => {
37 | let s = a.split('\x1c')
38 | s = s.filter((e) => { return e })
39 | return s
40 | }
41 | if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
42 | if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
43 | if ('map' in state.raw) state.map = state.raw.map
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/protocols/vcmp.js:
--------------------------------------------------------------------------------
1 | import samp from './samp.js'
2 |
3 | export default class vcmp extends samp {
4 | constructor () {
5 | super()
6 | this.magicHeader = 'VCMP'
7 | this.responseMagicHeader = 'MP04'
8 | this.isVcmp = true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/protocols/vintagestory.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 | import vintagestorymaster from './vintagestorymaster.js'
3 |
4 | export default class vintagestory extends Core {
5 | async run (state) {
6 | const master = new vintagestorymaster()
7 | master.options = this.options
8 | const masterState = await master.runOnceSafe()
9 | const servers = masterState.raw.servers
10 | const server = servers.find(s => s.serverIP === `${this.options.address}:${this.options.port}`)
11 |
12 | if (!server) {
13 | throw new Error('Server not found in the master list')
14 | }
15 |
16 | state.name = server.serverName
17 | state.password = server.hasPassword
18 | state.numplayers = parseInt(server.players)
19 | state.maxplayers = parseInt(server.maxPlayers)
20 | state.version = server.gameVersion
21 |
22 | state.raw = server
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/protocols/vintagestorymaster.js:
--------------------------------------------------------------------------------
1 | import Core from './core.js'
2 |
3 | export default class vintagestorymaster extends Core {
4 | constructor () {
5 | super()
6 | this.usedTcp = true
7 | }
8 |
9 | async run (state) {
10 | const response = await this.request({
11 | url: 'https://masterserver.vintagestory.at/api/v1/servers/list',
12 | responseType: 'json'
13 | })
14 |
15 | state.raw.servers = response?.data || []
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/protocols/warsow.js:
--------------------------------------------------------------------------------
1 | import quake3 from './quake3.js'
2 |
3 | export default class warsow extends quake3 {
4 | async run (state) {
5 | await super.run(state)
6 | if (state.players) {
7 | for (const player of state.players) {
8 | player.team = player.address
9 | delete player.address
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/protocols/xonotic.js:
--------------------------------------------------------------------------------
1 | import quake3 from './quake3.js'
2 |
3 | export default class xonotic extends quake3 {
4 | async run (state) {
5 | await super.run(state)
6 |
7 | // Sometimes, the server returns a player's name as a number (which seems to be the team?) and the name in
8 | // an extra field called "address", we are not sure of this behaviour nor if this is a good enough solution
9 | for (const player of state.players) {
10 | if (!isNaN(player.name) && player.raw.address) {
11 | player.raw.team = player.name
12 | player.name = player.raw.address
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tools/README.md:
--------------------------------------------------------------------------------
1 | This folder is not really intended to be used by users.
2 |
3 | Contains helpers for certain stuff (such as generating the games list Markdown table).
4 |
--------------------------------------------------------------------------------
/tools/attempt_protocols.js:
--------------------------------------------------------------------------------
1 | import Minimist from 'minimist'
2 | import { GameDig } from './../lib/index.js'
3 | import * as protocols from './../protocols/index.js'
4 |
5 | const argv = Minimist(process.argv.slice(2), {})
6 |
7 | const options = {}
8 | if (argv._.length >= 1) {
9 | const target = argv._[0]
10 | const split = target.split(':')
11 | options.host = split[0]
12 | if (split.length >= 2) {
13 | options.port = split[1]
14 | }
15 | options.debug = argv._[1] === 'debug'
16 | }
17 |
18 | const gamedig = new GameDig(options)
19 |
20 | const protocolList = []
21 | Object.keys(protocols).forEach((key) => protocolList.push(key))
22 |
23 | const ignoredProtocols = ['discord', 'beammpmaster', 'beammp', 'teamspeak2', 'teamspeak3', 'vintagestorymaster', 'renegadexmaster', 'hawakeningmaster', 'brokeprotocolmaster']
24 | const protocolListFiltered = protocolList.filter((protocol) => !ignoredProtocols.includes(protocol))
25 |
26 | const run = async () => {
27 | for (const protocol of protocolListFiltered) {
28 | try {
29 | const response = await gamedig.query({
30 | ...options,
31 | type: `protocol-${protocol}`
32 | })
33 | console.log(`Success on '${protocol}':`, response)
34 | process.exit()
35 | } catch (e) {
36 | console.log(`Error on '${protocol}': ${e}`)
37 | }
38 | }
39 | }
40 |
41 | run().then(() => {})
42 |
--------------------------------------------------------------------------------
/tools/esbuild.js:
--------------------------------------------------------------------------------
1 | import { build } from 'esbuild'
2 | import { nodeExternalsPlugin } from 'esbuild-node-externals'
3 |
4 | const buildConfig = {
5 | entryPoints: ['lib/index.js'],
6 | platform: 'node',
7 | target: 'node16',
8 | bundle: true,
9 | outfile: 'dist/index.cjs',
10 | format: 'cjs',
11 | // got is a esm module, so we need to add it to the allowList, to include it in the bundle.
12 | plugins: [nodeExternalsPlugin({ allowList: ['got'] })]
13 | }
14 |
15 | build(buildConfig).then(() => { }).catch(() => { process.exit(1) })
16 |
--------------------------------------------------------------------------------
/tools/find-id-changes.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawnSync } from 'node:child_process'
4 | import assert from 'node:assert'
5 | import { mkdirSync, copyFileSync } from 'node:fs'
6 | import path from 'node:path'
7 | import { fileURLToPath } from 'node:url'
8 | import process from 'node:process'
9 |
10 | // Generate a list of changes to "lib/games.js" where game IDs have been changed via the git history.
11 | // Requires git to be installed.
12 | // Make sure you don't have any local un-committed changes to lib/games.js
13 |
14 | // Usage: node tools/find-id-changes.js > id-changes.json
15 | // Output is an array of
16 | // {
17 | // "hash": "git commit hash",
18 | // "changes": [ ["oldid", "newid"], ... ],
19 | // "removed": [ "removedid", ... ],
20 | // "added": [ "addedid", ... ]
21 | // }
22 |
23 | // The output can be converted to a map of { "oldid": "newid" } using a jq command:
24 | // cat id-changes.json | jq ".[].changes | map({ (.[0]): .[1] } ) | add" | jq -s "add"
25 |
26 | const main = async (rootDir) => {
27 | // Make sure CWD is the root of the repo
28 | process.chdir(rootDir)
29 |
30 | // Get list of commits that have modified lib/games.js
31 | const gitLog = spawnSync(
32 | 'git',
33 | [
34 | 'log',
35 | '--follow',
36 | '--format=%H',
37 | '--diff-filter=M',
38 | '--reverse',
39 | '--',
40 | 'lib/games.js'
41 | ],
42 | { encoding: 'utf-8' }
43 | )
44 |
45 | // Make a directory to store files in
46 | mkdirSync('game_changes', { recursive: true })
47 |
48 | const output = []
49 |
50 | for (const commitHash of gitLog.stdout.split('\n')) {
51 | if (commitHash.length === 0) continue
52 |
53 | // Checkout lib/games.js before the commit that changed it
54 | assert(
55 | spawnSync('git', ['checkout', `${commitHash}^1`, '--', 'lib/games.js'])
56 | .status === 0
57 | )
58 |
59 | // We have to copy each state of the file to its own file because node caches imports
60 | const beforeName = `game_changes/${commitHash}-before.js`
61 | copyFileSync('lib/games.js', beforeName)
62 |
63 | const before = await import(path.join('../', beforeName))
64 |
65 | // Checkout lib/games.js after the commit that changed it
66 | assert(
67 | spawnSync('git', ['checkout', `${commitHash}`, '--', 'lib/games.js'])
68 | .status === 0
69 | )
70 |
71 | const afterName = `game_changes/${commitHash}-after.js`
72 | copyFileSync('lib/games.js', afterName)
73 |
74 | const after = await import(path.join('../', afterName))
75 |
76 | // Find game IDs that were removed and added
77 | let removed = Object.keys(before.games).filter(
78 | (key) => !(key in after.games)
79 | )
80 | let added = Object.keys(after.games).filter(
81 | (key) => !(key in before.games)
82 | )
83 |
84 | const changes = []
85 |
86 | for (const rm of removed) {
87 | for (const add of added) {
88 | const beforeGame = before.games[rm]
89 | const afterGame = after.games[add]
90 |
91 | // Modify game names to ignore case, spaces, and punctuation
92 | const beforeName = beforeGame.name.toLowerCase().replace(/[^a-z]/g, '')
93 | const afterName = afterGame.name.toLowerCase().replace(/[^a-z]/g, '')
94 |
95 | if (
96 | beforeGame.options.protocol === afterGame.options.protocol &&
97 | (beforeName.includes(afterName) || afterName.includes(beforeName))
98 | ) {
99 | changes.push([rm, add])
100 | removed = removed.filter((r) => r !== rm)
101 | added = added.filter((a) => a !== add)
102 | break
103 | }
104 | }
105 | }
106 |
107 | output.push({
108 | hash: commitHash,
109 | changes,
110 | removed,
111 | added
112 | })
113 | }
114 |
115 | // Reset the contents of lib/games.js
116 | spawnSync('git', ['checkout', '--', 'lib/games.js'])
117 |
118 | return output
119 | }
120 |
121 | main(
122 | // Get the root of the repo:
123 | // dir of bin/find-id-changes.js -> /../
124 | path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
125 | ).then((o) => console.log(JSON.stringify(o)), console.error)
126 |
--------------------------------------------------------------------------------
/tools/find_id_duplicates.js:
--------------------------------------------------------------------------------
1 | import { games } from '../lib/games.js'
2 |
3 | const ids = Object.keys(games)
4 |
5 | Object.keys(games).forEach((key) => {
6 | if (games[key].extra && games[key].extra.old_id) {
7 | const idOld = games[key].extra.old_id
8 | ids.push(idOld)
9 | }
10 | })
11 |
12 | function hasDuplicates (obj) {
13 | const uniqueSet = new Set()
14 |
15 | for (const item of obj) {
16 | if (uniqueSet.has(item)) {
17 | console.log('Duplicate:', item)
18 | return true
19 | }
20 | uniqueSet.add(item)
21 | }
22 |
23 | return false
24 | }
25 |
26 | if (hasDuplicates(ids)) {
27 | console.log('Duplicates found.')
28 | } else {
29 | console.log('No duplicates found.')
30 | }
31 |
--------------------------------------------------------------------------------
/tools/generate_games_list.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import * as fs from 'node:fs'
4 | import { games } from '../lib/games.js'
5 | import { fileURLToPath } from 'node:url'
6 | import { dirname } from 'node:path'
7 |
8 | const __filename = fileURLToPath(import.meta.url)
9 | const __dirname = dirname(__filename)
10 |
11 | const readmeFilename = __dirname + '/../GAMES_LIST.md'
12 | const readme = fs.readFileSync(readmeFilename, { encoding: 'utf8' })
13 |
14 | const markerTop = ''
15 | const markerBottom = ''
16 |
17 | const sortedGamesIds = Object.keys(games).sort()
18 | const sortedGames = {}
19 | sortedGamesIds.forEach(key => {
20 | sortedGames[key] = games[key]
21 | })
22 |
23 | const columnDelimiter = '|'
24 | const columnPadLeft = 1
25 | const columnPadRight = 1
26 |
27 | const HeaderType = {
28 | ID: 0,
29 | GameName: 1,
30 | Notes: 2
31 | }
32 | const HeaderNames = {
33 | [HeaderType.ID]: { Name: 'GameDig Type ID' },
34 | [HeaderType.GameName]: { Name: 'Name' },
35 | [HeaderType.Notes]: { Name: 'See Also' }
36 | }
37 | // defines the order of columns
38 | const HeaderDefinition = [
39 | HeaderType.ID,
40 | HeaderType.GameName,
41 | HeaderType.Notes
42 | ]
43 |
44 | const headerMap = HeaderDefinition.map(idx => Object.values(HeaderType)[idx])
45 | const headers = Object.keys(HeaderType).map((x, idx) => HeaderNames[idx].Name)
46 |
47 | const matrix = []
48 | const maxLength = headers.map(x => x?.length ?? 0)
49 | Object.entries(sortedGames).forEach(([id, game]) => {
50 | const lineArray = Array(headerMap.length).fill('')
51 | lineArray[HeaderType.ID] = id
52 | lineArray[HeaderType.GameName] = game.name
53 |
54 | const notes = []
55 | if (game?.extra?.doc_notes) {
56 | notes.push('[Notes](#' + game.extra.doc_notes + ')')
57 | }
58 | if (['valve', 'dayz', 'sdtd'].includes(game.options.protocol)) {
59 | notes.push('[Valve Protocol](#valve)')
60 | }
61 | if (['epic', 'asa', 'theisleevrima', 'renown'].includes(game.options.protocol)) {
62 | notes.push('[EOS Protocol](#epic)')
63 | }
64 | lineArray[HeaderType.Notes] = notes.join(', ')
65 |
66 | lineArray.forEach((x, index) => {
67 | maxLength[index] = Math.max(maxLength[index], x?.length ?? 0)
68 | })
69 | matrix.push(lineArray)
70 | })
71 |
72 | matrix.splice(0, 0, headers)
73 | const padLeft = ' '.repeat(columnPadLeft)
74 | const padRight = ' '.repeat(columnPadRight)
75 | const lines = matrix.map(row => {
76 | const values = headerMap.map((x, idx) => {
77 | return padLeft + row[x].padEnd(maxLength[x], ' ') + padRight
78 | })
79 | return `${columnDelimiter}${''}${values.join(columnDelimiter)}${columnDelimiter}`
80 | })
81 | const headerSeps = ['', ...headerMap.map(x => '-'.repeat(maxLength[x] + columnPadLeft + columnPadRight)), '']
82 | const headerSep = `${headerSeps.join(columnDelimiter)}`
83 | lines.splice(1, 0, headerSep)
84 | const generated = lines.join('\n')
85 |
86 | let start = readme.indexOf(markerTop)
87 | const end = start >= 0 ? readme.indexOf(markerBottom) : 0
88 | start = Math.max(0, start) + (start >= 0 ? markerTop.length : 0)
89 |
90 | const updated = readme.substring(0, start) + '\n' + generated + '\n' + readme.substring(end)
91 | fs.writeFileSync(readmeFilename, updated)
92 |
--------------------------------------------------------------------------------
/tools/run-id-tests.js:
--------------------------------------------------------------------------------
1 | import { spawnSync } from 'node:child_process'
2 | import process from 'node:process'
3 |
4 | // Import directly from file so that this script works without dependencies installed.
5 | import { games } from './../lib/games.js'
6 |
7 | const ID_TEST_BIN = process.env.GAMEDIG_ID_TESTER || 'gamedig-id-tests'
8 |
9 | const result = spawnSync(ID_TEST_BIN, {
10 | input: JSON.stringify(games),
11 | stdio: ['pipe', 'inherit', 'inherit']
12 | })
13 |
14 | if (result.error) {
15 | throw result.error
16 | }
17 |
18 | process.exit(result.status)
19 |
--------------------------------------------------------------------------------