mirror of
https://github.com/skylord123/node-red-contrib-gamedig.git
synced 2026-05-26 09:03:33 -06:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81a689379f | |||
| c83d719598 | |||
| 617f4d4c51 | |||
| a79c40c41d | |||
| e9b95ac65e | |||
| 12d4a40cf4 | |||
| c0c40910ae | |||
| 350e93479e | |||
| cf94d2a787 | |||
| 035341c386 |
@@ -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']
|
||||
@@ -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."
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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).
|
||||
|
||||
@@ -10,6 +10,12 @@ You can also specify manual GameDig options using `msg.options` as an input. Thi
|
||||
|
||||
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.
|
||||
|
||||
[](https://ko-fi.com/B0B51BM7C)
|
||||
|
||||
### Usage Examples
|
||||
- #### Inserting query data into InfluxDB and using Grafana to view results
|
||||

|
||||
|
||||
Generated
+132
-746
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red-contrib-gamedig",
|
||||
"version": "2.2.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": "^4.0.7"
|
||||
"gamedig": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
+61
-8
@@ -6,6 +6,7 @@
|
||||
name: { value: '' },
|
||||
server_type: { value: '' },
|
||||
host: { value: '' },
|
||||
address: { value: '' },
|
||||
port: { value: '' },
|
||||
halt_if: { value: '' },
|
||||
max_attempts: { value: '' },
|
||||
@@ -14,6 +15,7 @@
|
||||
given_port_only: { value: '' },
|
||||
ip_family: { value: '0' },
|
||||
debug: { value: '' },
|
||||
strip_colors: { type: "checkbox", value: true },
|
||||
request_rules: { value: '' },
|
||||
output_options: { value: '' }
|
||||
},
|
||||
@@ -26,23 +28,30 @@
|
||||
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() {
|
||||
let server_types = null;
|
||||
if(typeof this.strip_colors === "undefined") {
|
||||
this.strip_colors = true;
|
||||
$("#node-input-strip_colors").prop('checked', true);
|
||||
}
|
||||
|
||||
$.getJSON('/gamedig/types', function(data) {
|
||||
if(data.result !== 'ok' || !data.hasOwnProperty("server_types"))
|
||||
{
|
||||
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({
|
||||
@@ -54,7 +63,7 @@
|
||||
if (
|
||||
v.name.toLowerCase().indexOf(val.toLowerCase()) > -1 ||
|
||||
v.type.toLowerCase().indexOf(val.toLowerCase()) > -1 ||
|
||||
v.protocol.toLowerCase().indexOf(val.toLowerCase()) > -1
|
||||
v.options.protocol.toLowerCase().indexOf(val.toLowerCase()) > -1
|
||||
) {
|
||||
matches.push({
|
||||
value: v.type,
|
||||
@@ -65,6 +74,20 @@
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -101,6 +124,14 @@
|
||||
Host without port. Uses <code>msg.host</code> if left blank.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<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" />
|
||||
@@ -171,6 +202,18 @@
|
||||
</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;">
|
||||
@@ -225,6 +268,11 @@
|
||||
</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>
|
||||
@@ -296,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>
|
||||
|
||||
+108
-61
@@ -1,34 +1,84 @@
|
||||
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);
|
||||
let node = this;
|
||||
this.server_type = config.server_type;
|
||||
this.host = config.host;
|
||||
this.port = config.port;
|
||||
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.given_port_only = config.given_port_only || false;
|
||||
this.ip_family = config.ip_family || 0;
|
||||
this.debug = config.debug || false;
|
||||
this.request_rules = config.request_rules || false;
|
||||
this.output_options = config.output_options || false;
|
||||
node.on('input', function(msg) {
|
||||
let options = {
|
||||
'type': node.server_type || msg.server_type || undefined,
|
||||
'host': node.host || msg.host || undefined,
|
||||
'port': node.port || msg.port || undefined,
|
||||
'maxAttempts': node.max_attempts || msg.max_attempts || undefined,
|
||||
'socketTimeout': node.socket_timeout || msg.socket_timeout || undefined,
|
||||
'attemptTimeout': node.attempt_timeout || msg.attempt_timeout || undefined,
|
||||
'givenPortOnly': node.given_port_only || msg.given_port_only || undefined,
|
||||
'ipFamily': node.ip_family || msg.ip_family || undefined,
|
||||
'debug': node.debug || msg.config || undefined,
|
||||
'requestRules': node.request_rules || msg.request_rules || undefined
|
||||
'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)
|
||||
@@ -38,15 +88,20 @@ module.exports = function(RED) {
|
||||
|
||||
// set the things we want to return
|
||||
msg.server_type = options.type;
|
||||
if(options.host) {
|
||||
msg.host = options.host;
|
||||
}
|
||||
if(options.address) {
|
||||
msg.address = options.address;
|
||||
}
|
||||
msg.port = options.port;
|
||||
if(node.output_options)
|
||||
{
|
||||
msg.options = options;
|
||||
}
|
||||
|
||||
if(!options.host) {
|
||||
node.error("host missing from input.");
|
||||
if(!msg.host && !msg.address) {
|
||||
node.error("host/address missing from input.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,65 +110,57 @@ module.exports = function(RED) {
|
||||
return;
|
||||
}
|
||||
|
||||
gamedig.query(options)
|
||||
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 availableTypesContent = fs.readFileSync(require.resolve("gamedig/games.txt"), 'utf-8')
|
||||
server_types = [];
|
||||
|
||||
availableTypesContent
|
||||
.split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
if(
|
||||
line.trim().length === 0
|
||||
|| line.trim().length === 0
|
||||
|| line.trim().startsWith('#')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// examples:
|
||||
// avp2|Aliens versus Predator 2 (2001)|gamespy1|port=27888
|
||||
// avp2010|Aliens vs. Predator (2010)|valve|port=27015
|
||||
|
||||
let [game_type, game_name, game_protocol] = line.split('|');
|
||||
server_types.push({
|
||||
'name': game_name,
|
||||
'type': game_type,
|
||||
'protocol': game_protocol
|
||||
});
|
||||
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
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user