20 Commits

Author SHA1 Message Date
skylord123 81a689379f Merge pull request #18 from skylord123/dev
Release v3.1.0
2026-05-25 23:11:10 -06:00
skylord123 c83d719598 Merge branch 'master' into dev 2026-05-25 23:09:40 -06:00
skylord123 617f4d4c51 Add GitHub Actions workflow for npm publishing
- Publishes to npm whenever a GitHub Release is published
- Uses npm Trusted Publishing (OIDC) so no NPM_TOKEN secret is required
- Stable tags (vX.Y.Z) go to the latest dist-tag and commit the version
  bump back to master; pre-release tags (vX.Y.Z-beta.N) publish to a
  matching dist-tag without touching master
2026-05-25 23:09:34 -06:00
skylord123 a79c40c41d Update gamedig to 5.3.2 and quiet down offline errors
- Update GameDig from 5.1.4 to 5.3.2
- Server-down errors (timeouts, connection refused, DNS failures, etc.)
  no longer call node.error(). The offline msg still flows out and
  status still shows red, but the debug panel stays clean. Real gamedig
  errors (parse failures, unexpected exceptions) still surface as before
- node.error() now passes msg as the second argument so downstream
  catch nodes can handle gamedig errors
- Removed leftover console.error(error) call from the gamedig catch
  block since node.error already handles logging when appropriate
- Switch the admin /gamedig/types call in the editor from $.getJSON to
  RED.comms.request so it respects Node-RED's auth and httpAdminRoot
  prefix. Adds a .fail() handler so editor load errors get logged
2026-05-25 23:05:01 -06:00
skylord123 e9b95ac65e Merge pull request #17 from skylord123/dev
Release v3.0.1
2024-11-21 11:17:38 -07:00
skylord123 12d4a40cf4 - Remove console.log debug lines 2024-11-21 11:16:04 -07:00
skylord123 c0c40910ae - Update GameDig from 5.1.0 to 5.1.4
- Errors in parsing GameDig response now throw an error instead of returning to output
- Prevents "Class constructor Players cannot be invoked without 'new'" error
  when Node-RED attempts to clone the server response data (this happens when the gamedig node feeds into a function node for example)
2024-11-21 11:08:03 -07:00
skylord123 350e93479e Merge pull request #14 from skylord123/dev
Release v3.0.0
2024-08-06 18:19:19 -06:00
skylord123 cf94d2a787 - Fixed halt if option 2024-08-06 11:34:06 -06:00
skylord123 035341c386 Release v3.0.0
- Update gamedig from 4.0.6 to 5.1.0 (breaking change! various server types renamed)
- Fixed autocomplete for server types and now selecting a server type auto fills the query port with the default
- Added strip colors gamedig option
- Added address gamedig option to skip DNS resolution
- Added github funding links
2024-08-05 20:22:33 -06:00
skylord123 5bacdb685d Release v2.2.1
- Update gamedig from 4.0.6 to 4.0.7
2023-09-05 15:10:50 -06:00
skylord123 2bf5cc5f48 Update deps 2023-05-10 22:41:08 -06:00
skylord123 f0d4b8e269 Set version to 2.2.0 2023-05-10 22:39:57 -06:00
skylord123 5f80a0dbed Merge pull request #12 from skylord123/dev
Release 2.2.0
2023-05-10 22:37:11 -06:00
skylord123 787ee61c4e - You can now pass any option to the underlying GameDig library when querying a server by setting msg.options object on the input. Can be used for example to set msg.options.guildId that is required for querying Discord servers. Note that this will override even the config settings from the UI.
- New option "Output options" that you can enable from config to output `msg.options` object that contains all options we passed to GameDig to query the server. This overrides all other options (whether set from the UI or from `msg.*` variables) so take note of that when chaining multiple server query nodes together with this enabled.
- Breaking Change: This node no longer returns `msg.max_attempts`, `msg.socket_timeout`, and `msg.attempt_timeout`, instead see "Output options" mentioned above if you need this.
2023-05-10 22:12:26 -06:00
skylord123 8bdb043923 Closes #11 - Add autocomplete for server types instead of having to search at bottom of config page 2023-05-10 19:38:10 -06:00
skylord123 a5498a4ba9 #10 - Work on adding all available options (including advanced) from damedig 2023-05-09 23:35:48 -06:00
skylord123 bb5cb45cd9 Updated readme 2023-02-23 23:24:23 -07:00
skylord123 facf77e1fa Merge pull request #9 from skylord123/dev
Update version to 2.1.2
2023-02-23 23:09:44 -07:00
skylord123 9d200b60a1 - update version to 2.1.2
- fixed server type list
- bump gamedig to 4.0.6
2023-02-23 23:06:48 -07:00
7 changed files with 676 additions and 1116 deletions
+14
View File
@@ -0,0 +1,14 @@
# These are supported funding model platforms
github: skylord123 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: SkylarSadlier # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+81
View File
@@ -0,0 +1,81 @@
name: Publish to npm
# Publishes the package to npm whenever a GitHub Release is published.
# It publishes the exact commit the release tag points to, so a pre-release
# can be cut from any branch (e.g. a beta off `dev`) without that branch
# having to be merged into master first.
#
# The release tag is the source of truth for the version:
# - Stable tag (e.g. v1.2.3) -> published to the "latest"
# dist-tag; the version bump is
# committed back to master.
# - Pre-release tag (e.g. v1.2.3-beta.1) -> published to a matching dist-tag
# ("beta", "rc", ...); does NOT
# become "latest" and is NOT
# committed back to master.
#
# Authentication uses npm Trusted Publishing (OIDC) - no token or secret is
# needed. Configure a trusted publisher for this package on npmjs.com:
# Repository: skylord123/node-red-contrib-gamedig
# Workflow: publish.yml
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # commit the version bump back to master
id-token: write # npm Trusted Publishing (OIDC) + provenance
steps:
- name: Check out the released commit
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Update npm
# Trusted Publishing requires npm 11.5.1 or newer; Node 22 ships npm 10.
run: npm install -g npm@latest
- name: Determine version and dist-tag
id: ver
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$VERSION" == *-* ]]; then
# pre-release, e.g. 1.0.0-beta.1 -> dist-tag "beta"
DIST_TAG="${VERSION#*-}"
DIST_TAG="${DIST_TAG%%.*}"
PRERELEASE=true
else
DIST_TAG=latest
PRERELEASE=false
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT"
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
echo "Publishing $VERSION to npm dist-tag '$DIST_TAG' (prerelease=$PRERELEASE)"
- name: Set version
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Publish to npm
run: npm publish --provenance --access public --tag "${{ steps.ver.outputs.dist_tag }}"
- name: Commit version bump back to master
if: steps.ver.outputs.prerelease == 'false'
run: |
if git diff --quiet; then
echo "package.json already at ${{ steps.ver.outputs.version }}; nothing to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Set version to ${{ steps.ver.outputs.version }}"
git push origin HEAD:master \
|| echo "::warning::Could not push the version bump to master (branch protection?). The package was still published."
+19 -6
View File
@@ -1,16 +1,29 @@
# node-red-contrib-gamedig
Query for the status of most game/voice servers using Node-RED.
Query for server information of most game/voice servers using Node-RED.
This package adds the node "Query Game Server" that uses the NPM package [GameDig](https://www.npmjs.com/package/gamedig) to query if a server is online or not and if so returns the data of the server.
This package adds the node `Query Game Server` that uses the NPM package [GameDig](https://www.npmjs.com/package/gamedig) to query if a server is online or not and if so returns the data of the server.
You can pass the server type, host, and port on the input message or define them on the node (settings defined on the node will override msg values).
You can also specify manual GameDig options using `msg.options` as an input. This will override any other options. For example: you can set `msg.options.guildId` that is required for querying Discord servers.
Visit the [GameDig GitLab page](https://github.com/gamedig/node-gamedig#return-value) if you want more information about what this library parses and standardizes from the server response.
### Help fund development
If you use this node and find it helpful please consider donating to help fund future development. All of my software is free and open-source and this helps keep it that way.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B51BM7C)
### Usage Examples
#### Inserting query data into InfluxDB and using Grafana to view results
![Flow Preview](https://skylar.tech/content/images/2019/12/image-2.png)
I created a post on my website about how to use this node to query gameservers and store the results in InfluxDB. I then give a dashboard in Grafana that can be used to display the data. Check it out here:
https://skylar.tech/tracking-game-server-statistics-using-node-red-influxdb-and-grafana/
- #### Inserting query data into InfluxDB and using Grafana to view results
![Flow Preview](https://skylar.tech/content/images/2019/12/image-2.png)
I created a post on my website about how to use this node to query gameservers and store the results in InfluxDB. I then give a dashboard in Grafana that can be used to display the data. Check it out here:
https://skylar.tech/tracking-game-server-statistics-using-node-red-influxdb-and-grafana/
- #### Automatically restarting servers when unavailable
Ever host a server and have it stop responding but the process doesn't crash so it doesn't auto restart? If you pair this with something like [node-red-contrib-dockerode](https://flows.nodered.org/node/node-red-contrib-dockerode) you can automatically restart the container/process if the query fails X times to respond.
### Other Packages
+235 -972
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "node-red-contrib-gamedig",
"version": "2.1.1",
"version": "3.0.1",
"description": "Query for the status of any game server using node-red",
"repository": {
"type": "git",
@@ -23,6 +23,6 @@
}
},
"dependencies": {
"gamedig": "^3.0.0"
"gamedig": "^5.3.2"
}
}
+187 -62
View File
@@ -6,11 +6,18 @@
name: { value: '' },
server_type: { value: '' },
host: { value: '' },
address: { value: '' },
port: { value: '' },
halt_if: { value: '' },
max_attempts: { value: '' },
socket_timeout: { value: '' },
attempt_timeout: { value: '' },
given_port_only: { value: '' },
ip_family: { value: '0' },
debug: { value: '' },
strip_colors: { type: "checkbox", value: true },
request_rules: { value: '' },
output_options: { value: '' }
},
inputs:1,
outputs:1,
@@ -21,26 +28,66 @@
return this.name;
}
if(this.host) {
return (this.server_type ? this.server_type : 'Query') + ': ' + this.host + (this.port ? ":" + this.port : '');
if(this.host || this.address) {
return (this.server_type ? this.server_type : 'Query') + ': ' + (this.host || this.address) + (this.port ? ":" + this.port : '');
}
return 'Query Game Server';
},
oneditprepare: function() {
$.getJSON('/gamedig/types', function(data) {
let html = '<table>' +
'<thead id="query-game-server-types-table"><tr><td><strong>Type</strong></td><td><strong>Name</strong></td></tr></thead>' +
'<tbody id="query-game-server-type-rows">';
for(var type in data) {
html += "<tr class=\"query-game-server-type-row\">" +
"<td>"+type+"</td>" +
"<td>"+data[type]+"</td>" +
"</tr>";
let server_types = null;
if(typeof this.strip_colors === "undefined") {
this.strip_colors = true;
$("#node-input-strip_colors").prop('checked', true);
}
html += '</tbody>' +
'</table>';
$("#query-game-server-types").html(html);
RED.comms.request({
url: 'gamedig/types',
type: 'GET'
}).done(function(data) {
if (data.result !== 'ok' || !data.hasOwnProperty("server_types")) {
console.error("server_types failed to load");
return;
}
server_types = data.server_types;
}).fail(function(err) {
console.error("Error retrieving server_types:", err);
});
$("#node-input-server_type").autoComplete({
search: function(val) {
if(!server_types) return false; // ignore until we have the types loaded
let matches = [];
server_types.forEach(v => {
if (
v.name.toLowerCase().indexOf(val.toLowerCase()) > -1 ||
v.type.toLowerCase().indexOf(val.toLowerCase()) > -1 ||
v.options.protocol.toLowerCase().indexOf(val.toLowerCase()) > -1
) {
matches.push({
value: v.type,
label: `${v.name} (${v.type})`,
protocol: v.protocol
});
}
});
return matches;
}
}).on('change', function () {
if(!server_types) return;
let val = $(this).val();
server_types.forEach(server_type => {
if(server_type['type'] !== val) return;
let query_port = server_type.options.port_query || server_type.options.port || null;
if(query_port && server_type.options.port_query_offset) {
query_port += server_type.options.port_query_offset;
}
$("#node-input-port").val(query_port);
});
});
}
});
@@ -63,10 +110,10 @@
<div class="form-row">
<label for="node-input-server_type"><i class="fa fa-cube"></i> Server Type</label>
<input type="text" id="node-input-server_type">
<input type="text" id="node-input-server_type" placeholder="msg.server_type">
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
View server types <a href="#gamdig-types" style="color:#0000EE;text-decoration: underline;">below</a>.
Recommend visiting the <a href="https://github.com/gamedig/node-gamedig#games-list" target="_blank" style="color:#0000EE;text-decoration: underline;">GameDig GitHub page</a> for more information about the server type you are trying to query. Some types require extra setup.
</div>
<div class="form-row">
@@ -78,8 +125,16 @@
</div>
<div class="form-row">
<label for="node-input-port"><i class="fa fa-ethernet"></i> Port</label>
<input type="text" id="node-input-port" placeholder="msg.host" />
<label for="node-input-address"><i class="fa fa-server"></i> Address</label>
<input type="text" id="node-input-address" placeholder="msg.address" />
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
Override the IP address of the server skipping DNS resolution. When set, host will not be resolved, instead address will be connected to. However, some protocols still use host for other reasons e.g. as part of the query. Uses <code>msg.address</code> if left blank.
</div>
<div class="form-row">
<label for="node-input-port"><i class="fa fa-server"></i> Port</label>
<input type="text" id="node-input-port" placeholder="msg.port" />
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
Query port for the server (join and query port may differ).
@@ -101,7 +156,7 @@
<div class="form-row">
<label for="node-input-max_attempts"><i class="fa fa-cogs"></i> Max Attempts</label>
<input type="text" id="node-input-max_attempts" placeholder="1" />
<input type="text" id="node-input-max_attempts" placeholder="msg.max_attempts (default: 1)" />
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
Number of attempts to query server in case of failure.
@@ -109,52 +164,97 @@
<div class="form-row">
<label for="node-input-socket_timeout"><i class="fa fa-cogs"></i> Socket Timeout</label>
<input type="text" id="node-input-socket_timeout" placeholder="2000" />
<input type="text" id="node-input-socket_timeout" placeholder="msg.socket_timeout (default: 2000)" />
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
Milliseconds to wait for a single packet. Beware that increasing this will cause many queries to take longer even if the server is online.
</div>
<div class="form-row">
<label for="node-input-attempt_timeout"><i class="fas fa-cogs"></i> Attempt Timeout</label>
<input type="text" id="node-input-attempt_timeout" placeholder="10000" />
<label for="node-input-attempt_timeout"><i class="fa fa-cogs"></i> Attempt Timeout</label>
<input type="text" id="node-input-attempt_timeout" placeholder="msg.attempt_timeout (default: 10000)" />
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, as the socketTimeout typically fires first.
</div>
<h3 id="gamdig-types">Server Types</h3>
<p>
Search available types below.<br>
You can also view the list <a href="https://github.com/gamedig/node-gamedig#games-list" target="_blank" style="color:#0000EE;text-decoration: underline;">here</a>.
</p>
<div class="row">
<input type="text" id="query-game-server-types-search" placeholder="Search types.." style="margin-bottom: 10px;" />
<div class="form-row">
<label for="node-input-given_port_only"><i class="fa fa-lock"></i> Lock port</label>
<input
type="checkbox"
id="node-input-given_port_only"
style="width: auto; vertical-align: top"
/>
<span>
Only attempt to query server on given port (default: false).
</span>
</div>
<div id="query-game-server-types"></div>
<script type="text/javascript">
$("#query-game-server-types-search").on("input", function(e) {
let value = $(this).val();
if(value.length) {
$(".query-game-server-type-row").each(function(i, elem){
console.log('yay', $(elem).text(), value, $(elem).text().indexOf(value));
if($(elem).text().toLowerCase().indexOf(value.toLowerCase()) > -1) {
$(elem).show();
} else {
$(elem).hide();
}
});
return;
}
$(".query-game-server-type-row").show();
});
</script>
<div class="form-row">
<label for="node-input-debug"><i class="fa fa-bug"></i> Debug mode</label>
<input
type="checkbox"
id="node-input-debug"
style="width: auto; vertical-align: top"
/>
<span>
Enables massive amounts of debug logging to stdout.
</span>
</div>
<div class="form-row">
<label for="node-input-strip_colors" style="vertical-align: top"><i class="fa fa-server"></i> Strip colors</label>
<div style="width: 50%;display: inline-block;">
<input
type="checkbox"
id="node-input-strip_colors"
style="width: auto; vertical-align: top"
/>
for protocols that strips colors: unreal2, savage2, quake3, nadeo, gamespy2, doom3, armagetron.
</div>
</div>
<div class="form-row">
<label for="node-input-request_rules" style="vertical-align: top"><i class="fa fa-server"></i> Request rules</label>
<div style="width: 50%;display: inline-block;">
<input
type="checkbox"
id="node-input-request_rules"
style="width: auto; vertical-align: top"
/>
For many valve games, additional 'rules' may be fetched into the unstable raw field by setting this to true. Beware that this may increase query time and this is for Valve games only.
</div>
</div>
<div class="form-row">
<label for="node-input-output_options" style="vertical-align: top"><i class="fa fa-server"></i> Output options</label>
<div style="width: 50%;display: inline-block;">
<input
type="checkbox"
id="node-input-output_options"
style="width: auto; vertical-align: top"
/>
Outputs <code style="white-space: normal;">msg.options</code> as an object that contains all the options used to query the server using GameDig. Note: If you pass <code style="white-space: normal;">msg.options</code> as an input it will override all set options so make sure you unset it if chaining multiple server query nodes together unless that is what you want.
</div>
</div>
<div class="form-row">
<label for="node-input-ip_family"><i class="fa fa-server"></i> IP Rules</label>
<select
id="node-input-ip_family"
style="width: auto; vertical-align: top">
<option value="0">IPv4 and IPv6</option>
<option value="4">IPv4</option>
<option value="6">IPv6</option>
</select>
</div>
<div style="margin-left: 105px;width: 50%;margin-bottom: 10px;margin-top: -10px;">
IP family/version returned when looking up hostnames via DNS, can be IPv4 and IPv6, IPv4 only or IPv6 only.
</div>
</script>
<script type="text/html" data-help-name="query-game-server">
<p>Query most Game/Voice server's using the <a href="https://github.com/gamedig/node-gamedig" target="_blank">GameDig</a> library.</p>
<p>Query most Game/Voice server's using the <a href="https://github.com/gamedig/node-gamedig" target="_blank">GameDig</a> library. I recommend visiting the <a href="https://github.com/gamedig/node-gamedig" target="_blank">node-gamedig GitHub page</a> for more documentation.</p>
<h3>Inputs</h3>
<dl class="message-properties">
@@ -168,11 +268,21 @@
</dt>
<dd>Server IP/Hostname. Ignored if configured on the node.</dd>
<dt class="optional">
msg.address <span class="property-type">string | null</span>
</dt>
<dd>Override the IP address of the server skipping DNS resolution. When set, host will not be resolved, instead address will be connected to. However, some protocols still use host for other reasons e.g. as part of the query.</dd>
<dt class="optional">
msg.port <span class="property-type">integer | null</span>
</dt>
<dd>Query port of the server. Ignored if configured on the node. Uses default query port for the server type if left empty.</dd>
<dt class="optional">
msg.options <span class="property-type">object | null</span>
</dt>
<dd>Set additional GameDig options. This overrides all other methods of setting options. Can be used for example to set <code style="white-space: normal;">msg.options.guildId</code> that is required for querying Discord servers.</dd>
<dt class="optional">
msg.max_attempts <span class="property-type">integer | null</span>
</dt>
@@ -187,6 +297,26 @@
msg.attempt_timeout <span class="property-type">integer | null</span>
</dt>
<dd>Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, as the socketTimeout typically fires first. Ignored if configured on the node.</dd>
<dt class="optional">
msg.given_port_only <span class="property-type">boolean</span>
</dt>
<dd>Only attempt to query server on given port. Ignored if configured on the node.</dd>
<dt class="optional">
msg.ip_family <span class="property-type">number</span>
</dt>
<dd>IP family/version returned when looking up hostnames via DNS, can be 0 (IPv4 and IPv6), 4 (IPv4 only) or 6 (IPv6 only). (default 0).</dd>
<dt class="optional">
msg.debug <span class="property-type">boolean</span>
</dt>
<dd>Enables massive amounts of debug logging to stdout. (default false)</dd>
<dt class="optional">
msg.request_rules <span class="property-type">boolean</span>
</dt>
<dd>For many valve games, additional 'rules' may be fetched into the unstable raw field by setting this to true. Beware that this may increase query time and this is for Valve games only.</dd>
</dl>
<h3>Outputs</h3>
@@ -214,7 +344,12 @@
<dt>
msg.host <span class="property-type">string</span>
</dt>
<dd>Server IP/Hostname. Ignored if configured on the node.</dd>
<dd>Server IP/Hostname.</dd>
<dt>
msg.address <span class="property-type">string</span>
</dt>
<dd>Server address used to query.</dd>
<dt>
msg.port <span class="property-type">integer</span>
@@ -222,18 +357,8 @@
<dd>Query port of the server. Ignored if configured on the node.</dd>
<dt>
msg.max_attempts <span class="property-type">integer</span>
msg.options <span class="property-type">object | undefined</span>
</dt>
<dd>Number of attempts to query server in case of failure. Ignored if configured on the node.</dd>
<dt>
msg.socket_timeout <span class="property-type">integer</span>
</dt>
<dd>Milliseconds to wait for a single packet. Beware that increasing this will cause many queries to take longer even if the server is online. Ignored if configured on the node.</dd>
<dt>
msg.attempt_timeout <span class="property-type">integer</span>
</dt>
<dd>Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, as the socketTimeout typically fires first. Ignored if configured on the node.</dd>
<dd>Only set if configured to do so. Will return all the options passed to GameDig to query the server.</dd>
</dl>
</script>
+131 -67
View File
@@ -1,102 +1,166 @@
module.exports = function(RED) {
const gamedig = require('gamedig');
const fs = require('fs');
const { GameDig, games } = require('gamedig');
const SERVER_DOWN_PATTERNS = [
/Timed out/i,
/Failed all \d+ attempts/,
/ECONNREFUSED/,
/ENOTFOUND/,
/EHOSTUNREACH/,
/ENETUNREACH/,
/ETIMEDOUT/,
/ECONNRESET/,
/EAI_AGAIN/
];
function isServerDownError(error) {
const stack = (error && (error.stack || error.message)) || '';
const errorLines = stack.split('\n').filter(line => /^\s*Error:/.test(line));
if (errorLines.length === 0) {
return SERVER_DOWN_PATTERNS.some(re => re.test(stack));
}
return errorLines.every(line => SERVER_DOWN_PATTERNS.some(re => re.test(line)));
}
function deepCloneToPlain(obj) {
// Handle null/undefined
if (!obj) {
return obj;
}
// Handle arrays and array-like objects (including Players collection)
if (Array.isArray(obj) || (typeof obj === 'object' && obj.length >= 0)) {
return Array.from(obj, item => deepCloneToPlain(item));
}
// Handle instances of custom classes (like Player, Results)
if (obj && typeof obj === 'object' && Object.getPrototypeOf(obj) !== Object.prototype) {
// Convert to plain object while preserving enumerable properties
const plainObj = {};
for (const key of Object.keys(obj)) {
plainObj[key] = deepCloneToPlain(obj[key]);
}
return plainObj;
}
// Handle plain objects
if (obj && typeof obj === 'object') {
const result = {};
for (const key of Object.keys(obj)) {
// Skip the Buffer instance
if (key === 'rulesBytes' && Buffer.isBuffer(obj[key])) {
continue;
}
result[key] = deepCloneToPlain(obj[key]);
}
return result;
}
// Return primitive values as-is
return obj;
}
function QueryGameServer(config) {
RED.nodes.createNode(this, config);
var node = this;
this.server_type = config.server_type;
this.host = config.host;
this.port = config.port;
let node = this;
this.halt_if = config.halt_if;
this.max_attempts = config.max_attempts || 1;
this.socket_timeout = config.socket_timeout || 2000;
this.attempt_timeout = config.attempt_timeout || 10000;
this.output_options = config.output_options || false;
node.on('input', function(msg) {
if(node.server_type) {
msg.server_type = node.server_type;
let options = {
'type': config.server_type || msg.server_type || undefined,
'host': config.host || msg.host || undefined,
'address': config.address || msg.address || undefined,
'port': config.port || msg.port || undefined,
'maxAttempts': config.max_attempts || msg.max_attempts || 1,
'socketTimeout': config.socket_timeout || msg.socket_timeout || 2000,
'attemptTimeout': config.attempt_timeout || msg.attempt_timeout || 10000,
'givenPortOnly': config.given_port_only || msg.given_port_only || false,
'ipFamily': config.ip_family || msg.ip_family || undefined,
'debug': config.debug || msg.config || undefined,
'requestRules': config.request_rules || msg.request_rules || undefined,
'strip_colors': typeof config.strip_colors === "undefined" ? true : config.strip_colors
};
if(typeof msg.options === 'object' && msg.options)
{
options = {...options, ...msg.options};
}
if(node.host) {
msg.host = node.host;
// set the things we want to return
msg.server_type = options.type;
if(options.host) {
msg.host = options.host;
}
if(!msg.host) {
node.error("msg.host missing from input.");
if(options.address) {
msg.address = options.address;
}
msg.port = options.port;
if(node.output_options)
{
msg.options = options;
}
if(!msg.host && !msg.address) {
node.error("host/address missing from input.");
return;
}
if(node.port) {
msg.port = node.port;
if(!options.type) {
node.error("server_type missing from input.");
return;
}
if(node.halt_if) {
msg.halt_if = node.halt_if;
}
if(node.max_attempts) {
msg.max_attempts = node.max_attempts;
}
if(node.socket_timeout) {
msg.socket_timeout = node.socket_timeout;
}
if(node.attempt_timeout) {
msg.attempt_timeout = node.attempt_timeout;
}
gamedig.query({
'type': msg.server_type,
'host': msg.host,
'port': msg.port,
'maxAttempts': msg.max_attempts,
'socketTimeout': msg.socket_timeout,
'attemptTimeout': msg.attempt_timeout
})
GameDig.query(options)
.then(function(state) {
try {
msg.payload = 'online';
msg.data = state;
if (msg.payload === msg.halt_if) {
// GameDig returns Results, Players, and Player objects that we need to convert
// to standard Array/Object instances so that Node-RED doesn't error
msg.data = deepCloneToPlain(state);
if (msg.payload === node.halt_if) {
return null;
}
node.status({fill:"green",shape:"dot",text: 'Online ' + msg.data.players.length + ' players' });
node.status({ fill: "green", shape: "dot", text: `Online ${state.players.length} players` });
node.send(msg);
}).catch(function(error) {
} catch(e) {
node.error("Failed returning data: " + e.stack);
}
})
.catch(function(error) {
msg.payload = 'offline';
msg.data = {
'error': error
error,
stack: error.stack,
};
if (msg.payload === msg.halt_if) {
if (msg.payload === node.halt_if) {
return null;
}
node.status({fill:"red", shape:"dot", text: 'Offline'});
node.status({ fill: "red", shape: "dot", text: "Offline" });
node.send(msg);
if (!isServerDownError(error)) {
node.error(`GameDig Error: \n${error.stack}`, msg);
}
});
});
}
RED.nodes.registerType("query-game-server", QueryGameServer);
RED.httpAdmin.get(
"/gamedig/types",
RED.auth.needsPermission('gamedig.types'),
RED.auth.needsPermission('flows.write'),
function(req, res) {
// gamedig has no way of listing available server types
// so we just use regex to parse the info from the README
// this could break so we also reference the gamedig repo
let availableTypes = fs.readFileSync(require.resolve("gamedig/README.md"))
.toString()
.matchAll(/^\| `(.*)` * \| ([a-zA-Z: (0-9)\-'.]*)/gm),
results = {};
for (const match of availableTypes) {
if(match[1].indexOf("`<br>`") >= 0) {
let names = match[1].split("`<br>`");
results[names[0]] = match[2];
results[names[1]] = match[2];
} else {
results[match[1]] = match[2];
}
}
res.json(results);
let server_types = Object.keys(games).map(gameKey => {
let game = games[gameKey];
game["type"] = gameKey;
return game;
});
res.json({
'result': 'ok',
'server_types': server_types
});
}
);
};