diff --git a/docs/commands.md b/docs/commands.md
index 9dfd6ab22..16f0f610e 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -376,6 +376,7 @@ In-Game Command | Description
[**timer**](commands.md#timer) | Set custom timers that will notify whenever the timer have expired.
[**tr**](commands.md#tr) | Translate a text to another language.
[**trf**](commands.md#trf) | Translate a text from one language to another.
+[**tracker**](commands.md#tracker) | Manage trackers by adding or removing players directly from the in-game chat.
[**tts**](commands.md#tts) | Send a Text-To-Speech message to the Discord teamchat channel.
[**unmute**](commands.md#unmute) | Unmute the bot from the In-Game Team Chat.
[**upkeep**](commands.md#upkeep) | Get the upkeep time of all connected tool cupboard monitors.
@@ -684,6 +685,22 @@ Subcommand | Description | Required

+
+## **tracker**
+
+> **Manage player trackers directly from the in-game chat.**
+
Use the `tracker` command to create, add, or remove players to/from specific trackers.
+
If no `trackerId` is provided, the most recently created tracker for the current server will be used automatically.
+
+Subcommand | Description | Required
+---------- | ----------- | --------
+`create` | Create a new tracker. You can optionally provide a custom `trackerId`
`!tracker create [trackerId]` | `false`
+`add` | Add a player to a tracker using steamID or BattlemetricsID
`!tracker add [trackerId]` | `false`
+`remove` | Remove a player from a tracker using steamID or BattlemetricsID
`!tracker remove [trackerId]` | `false`
+
+
+
+
## **tts**
> **Send a Text-To-Speech message to the Discord teamchat channel.** To execute a Text-To-Speech command run `!tts `.
diff --git a/docs/images/ingame_commands/tracker_ingame.png b/docs/images/ingame_commands/tracker_ingame.png
new file mode 100644
index 000000000..40d0eb8bc
Binary files /dev/null and b/docs/images/ingame_commands/tracker_ingame.png differ
diff --git a/src/discordTools/discordEmbeds.js b/src/discordTools/discordEmbeds.js
index c47e46359..377d733c1 100644
--- a/src/discordTools/discordEmbeds.js
+++ b/src/discordTools/discordEmbeds.js
@@ -120,7 +120,8 @@ module.exports = {
const serverStatus = !successful ? Constants.NOT_FOUND_EMOJI :
(bmInstance.server_status ? Constants.ONLINE_EMOJI : Constants.OFFLINE_EMOJI);
- let description = `__**Battlemetrics ID:**__ ${battlemetricsLink}\n`;
+ let description = `__**Tracker ID:**__ \`${tracker.trackerId}\`\n`;
+ description += `__**Battlemetrics ID:**__ ${battlemetricsLink}\n`;
description += `__**${Client.client.intlGet(guildId, 'serverId')}:**__ ${tracker.serverId}\n`;
description += `__**${Client.client.intlGet(guildId, 'serverStatus')}:**__ ${serverStatus}\n`;
description += `__**${Client.client.intlGet(guildId, 'streamerMode')}:**__ `;
diff --git a/src/discordTools/discordModals.js b/src/discordTools/discordModals.js
index 1da15cd87..2f5d922e0 100644
--- a/src/discordTools/discordModals.js
+++ b/src/discordTools/discordModals.js
@@ -264,14 +264,14 @@ module.exports = {
const instance = Client.client.getInstance(guildId);
const tracker = instance.trackers[trackerId];
const identifier = JSON.stringify({ "trackerId": trackerId });
-
+
const modal = module.exports.getModal({
customId: `TrackerEdit${identifier}`,
title: Client.client.intlGet(guildId, 'editingOf', {
entity: tracker.name.length > 18 ? `${tracker.name.slice(0, 18)}..` : tracker.name
})
});
-
+
modal.addComponents(
new Discord.ActionRowBuilder().addComponents(TextInput.getTextInput({
customId: 'TrackerName',
@@ -292,11 +292,18 @@ module.exports = {
style: Discord.TextInputStyle.Short,
required: false,
minLength: 0
- }))
+ })),
+ new Discord.ActionRowBuilder().addComponents(TextInput.getTextInput({
+ customId: 'TrackerId',
+ label: Client.client.intlGet(guildId, 'trackerId'),
+ value: String(tracker.trackerId),
+ style: Discord.TextInputStyle.Short
+ }))
);
-
+
return modal;
},
+
getTrackerAddPlayerModal(guildId, trackerId) {
const instance = Client.client.getInstance(guildId);
diff --git a/src/handlers/buttonHandler.js b/src/handlers/buttonHandler.js
index f6bb724f0..969e94ffb 100644
--- a/src/handlers/buttonHandler.js
+++ b/src/handlers/buttonHandler.js
@@ -528,32 +528,16 @@ module.exports = async (client, interaction) => {
}
else if (interaction.customId.startsWith('CreateTracker')) {
const ids = JSON.parse(interaction.customId.replace('CreateTracker', ''));
+ const trackerId = await client.createTrackerInstance(guildId, ids.serverId);
const server = instance.serverList[ids.serverId];
-
- if (!server) {
+
+ if (!server || !trackerId) {
await interaction.message.delete();
return;
}
- interaction.deferUpdate();
-
- /* Find an available tracker id */
- const trackerId = client.findAvailableTrackerId(guildId);
-
- instance.trackers[trackerId] = {
- name: 'Tracker',
- serverId: ids.serverId,
- battlemetricsId: server.battlemetricsId,
- title: server.title,
- img: server.img,
- clanTag: '',
- everyone: false,
- inGame: true,
- players: [],
- messageId: null
- }
- client.setInstance(guildId, instance);
-
+ await interaction.deferUpdate();
+
await DiscordMessages.sendTrackerMessage(guildId, trackerId);
}
else if (interaction.customId.startsWith('CreateGroup')) {
diff --git a/src/handlers/inGameCommandHandler.js b/src/handlers/inGameCommandHandler.js
index 8d606ef74..3ff328bb0 100644
--- a/src/handlers/inGameCommandHandler.js
+++ b/src/handlers/inGameCommandHandler.js
@@ -210,6 +210,9 @@ module.exports = {
else if (commandLowerCase === `${prefix}${client.intlGet('en', 'commandSyntaxTravelingVendor')}` ||
commandLowerCase === `${prefix}${client.intlGet(guildId, 'commandSyntaxTravelingVendor')}`) {
rustplus.sendInGameMessage(rustplus.getCommandTravelingVendor());
+ } else if (commandLowerCase.startsWith(`${prefix}${client.intlGet('en', 'commandSyntaxTracker')}`) ||
+ commandLowerCase.startsWith(`${prefix}${client.intlGet(guildId, 'commandSyntaxTracker')}`)) {
+ rustplus.sendInGameMessage(await rustplus.getCommandTracker(commandLowerCase));
}
else {
/* Maybe a custom command? */
diff --git a/src/handlers/modalHandler.js b/src/handlers/modalHandler.js
index eec65c405..0d6d67094 100644
--- a/src/handlers/modalHandler.js
+++ b/src/handlers/modalHandler.js
@@ -20,6 +20,8 @@
const Discord = require('discord.js');
+const DiscordEmbeds = require('../discordTools/discordEmbeds');
+
const Battlemetrics = require('../structures/Battlemetrics');
const Constants = require('../util/constants.js');
const DiscordMessages = require('../discordTools/discordMessages.js');
@@ -263,21 +265,24 @@ module.exports = async (client, interaction) => {
else if (interaction.customId.startsWith('TrackerEdit')) {
const ids = JSON.parse(interaction.customId.replace('TrackerEdit', ''));
const tracker = instance.trackers[ids.trackerId];
+
const trackerName = interaction.fields.getTextInputValue('TrackerName');
const trackerBattlemetricsId = interaction.fields.getTextInputValue('TrackerBattlemetricsId');
const trackerClanTag = interaction.fields.getTextInputValue('TrackerClanTag');
-
+ const trackerNewId = interaction.fields.getTextInputValue('TrackerId');
+
if (!tracker) {
interaction.deferUpdate();
return;
}
-
+
tracker.name = trackerName;
+
if (trackerClanTag !== tracker.clanTag) {
tracker.clanTag = trackerClanTag;
client.battlemetricsIntervalCounter = 0;
}
-
+
if (trackerBattlemetricsId !== tracker.battlemetricsId) {
if (client.battlemetricsInstances.hasOwnProperty(trackerBattlemetricsId)) {
const bmInstance = client.battlemetricsInstances[trackerBattlemetricsId];
@@ -285,10 +290,10 @@ module.exports = async (client, interaction) => {
tracker.serverId = `${bmInstance.server_ip}-${bmInstance.server_port}`;
tracker.img = Constants.DEFAULT_SERVER_IMG;
tracker.title = bmInstance.server_name;
- }
- else {
+ } else {
const bmInstance = new Battlemetrics(trackerBattlemetricsId);
await bmInstance.setup();
+
if (bmInstance.lastUpdateSuccessful) {
client.battlemetricsInstances[trackerBattlemetricsId] = bmInstance;
tracker.battlemetricsId = trackerBattlemetricsId;
@@ -298,14 +303,33 @@ module.exports = async (client, interaction) => {
}
}
}
+
+ const isTrackerIdChanged = trackerNewId && trackerNewId !== ids.trackerId;
+
+ if (isTrackerIdChanged) {
+ if (instance.trackers.hasOwnProperty(trackerNewId)) {
+ const str = client.intlGet(interaction.guildId, 'trackerIdAlreadyExists', { trackerId: trackerNewId });
+ await client.interactionReply(interaction, DiscordEmbeds.getActionInfoEmbed(1, str));
+ client.log(client.intlGet(null, 'warningCap'), str);
+ return;
+ }
+
+ tracker.trackerId = trackerNewId;
+ instance.trackers[trackerNewId] = tracker;
+
+ delete instance.trackers[ids.trackerId];
+
+ await DiscordMessages.sendTrackerMessage(interaction.guildId, trackerNewId);
+ } else {
+ await DiscordMessages.sendTrackerMessage(interaction.guildId, ids.trackerId);
+ }
+
client.setInstance(guildId, instance);
-
+
client.log(client.intlGet(null, 'infoCap'), client.intlGet(null, 'modalValueChange', {
id: `${verifyId}`,
- value: `${trackerName}, ${tracker.battlemetricsId}, ${tracker.clanTag}`
+ value: `${trackerName}, ${tracker.battlemetricsId}, ${tracker.clanTag}, ${tracker.trackerId}`
}));
-
- await DiscordMessages.sendTrackerMessage(interaction.guildId, ids.trackerId);
}
else if (interaction.customId.startsWith('TrackerAddPlayer')) {
const ids = JSON.parse(interaction.customId.replace('TrackerAddPlayer', ''));
diff --git a/src/languages/en.json b/src/languages/en.json
index bb5fb961d..3c2bc3a8f 100644
--- a/src/languages/en.json
+++ b/src/languages/en.json
@@ -3,6 +3,7 @@
"abandonedCabins": "Abandoned Cabins",
"abandonedMilitaryBase": "Abandoned Military Base",
"abandonedSupermarket": "Abandoned Supermarket",
+ "activeRustServerNotFound": "Could not find an active server.",
"addPlayerCap": "ADD PLAYER",
"addSwitchCap": "ADD SWITCH",
"afkCap": "AFK",
@@ -158,6 +159,7 @@
"commandSyntaxTime": "time",
"commandSyntaxTimer": "timer",
"commandSyntaxTimers": "timers",
+ "commandSyntaxTracker": "tracker",
"commandSyntaxTranslateFromTo": "trf",
"commandSyntaxTranslateTo": "tr",
"commandSyntaxTravelingVendor": "vendor",
@@ -511,6 +513,7 @@
"noRegisteredEvents": "No registered events yet.",
"noRegisteredMarkers": "No registered markers.",
"noSavedNotes": "There are no saved notes.",
+ "noTrackersForServer": "No trackers linked to server",
"noToolCupboardWereFound": "No Tool Cupboard monitors were found.",
"none": "None",
"northEast": "North East",
@@ -553,6 +556,8 @@
"patrolHelicopterTakenDown": "Patrol Helicopter was taken down {location}.",
"percentSign": "Percent Sign",
"pipe": "Pipe",
+ "playerAddedToTracker": "Player {name} ({id}) has been added to tracker {trackerId}.",
+ "playerAlreadyExistsInTracker": "Player ({id}) is already registered in tracker {trackerId}.",
"playerHasBeenAliveFor": "{name} has been alive for {time}.",
"playerId": "Player ID",
"playerJoinedTheTeam": "{name} joined the team.",
@@ -566,7 +571,9 @@
"playerJustReturned": "{name} just returned ({time}).",
"playerJustWentAfk": "{name} just went AFK.",
"playerLeftTheTeam": "{name} left the team.",
+ "playerNotFoundInTracker": "Player ({id}) does not exist in tracker {trackerId}.",
"playerNotPairedWithServer": "Leader command does not work because {name} is not paired with the server.",
+ "playerRemovedFromTracker": "Player ({id}) has been removed from tracker {trackerId}.",
"players": "Players",
"playersSearch": "Players Search",
"plusSign": "Plus Sign",
@@ -725,6 +732,10 @@
"toolCupboard": "Tool Cupboard",
"total": "Total",
"tracker": "Tracker",
+ "trackerCreated": "Tracker created successfully. (ID: {trackerId})",
+ "trackerId": "Tracker ID",
+ "trackerIdAlreadyExists": "Tracker ID {trackerId} already exists!",
+ "trackerIdNotFound": "Tracker ID {trackerId} could not be found.",
"trackerAddPlayerDesc": "Add Player to {tracker}",
"trackerRemovePlayerDesc": "Remove Player from {tracker}",
"trademarkShownBeforeMessage": "{trademark} will be shown before messages.",
diff --git a/src/structures/DiscordBot.js b/src/structures/DiscordBot.js
index 19e50e123..daf82711d 100644
--- a/src/structures/DiscordBot.js
+++ b/src/structures/DiscordBot.js
@@ -362,6 +362,37 @@ class DiscordBot extends Discord.Client {
}
}
+ createTrackerInstance(guildId, serverId, customId = null) {
+ const instance = this.getInstance(guildId);
+ const server = instance?.serverList?.[serverId];
+ if (!server) return null;
+
+ const trackerId = customId || this.findAvailableTrackerId(guildId);
+
+ if (instance.trackers.hasOwnProperty(trackerId)) {
+ return null;
+ }
+
+ instance.trackers[trackerId] = {
+ name: 'Tracker',
+ serverId,
+ battlemetricsId: server.battlemetricsId,
+ title: server.title ?? 'Untitled',
+ img: server.img ?? null,
+ clanTag: '',
+ trackerId,
+ everyone: false,
+ inGame: true,
+ players: [],
+ messageId: null,
+ createdAt: Date.now()
+ };
+
+ this.setInstance(guildId, instance);
+ return trackerId;
+ }
+
+
findAvailableGroupId(guildId, serverId) {
const instance = this.getInstance(guildId);
diff --git a/src/structures/RustPlus.js b/src/structures/RustPlus.js
index 39820fa6a..8c1914fa6 100644
--- a/src/structures/RustPlus.js
+++ b/src/structures/RustPlus.js
@@ -38,6 +38,7 @@ const Map = require('../util/map.js');
const RustPlusLite = require('../structures/RustPlusLite');
const TeamHandler = require('../handlers/teamHandler.js');
const Timer = require('../util/timer.js');
+const Scrape = require('../util/scrape.js');
const TOKENS_LIMIT = 24; /* Per player */
const TOKENS_REPLENISH = 3; /* Per second */
@@ -2746,6 +2747,216 @@ class RustPlus extends RustPlusLib {
return strings;
}
+
+ async getCommandTracker(commandString) {
+ const args = commandString.split(' ');
+ const subCommand = args[1]?.toLowerCase();
+
+ if (subCommand === 'create') {
+ const trackerIdParts = args.slice(2);
+ const customTrackerId = trackerIdParts.length > 0 ? trackerIdParts.join(' ') : null;
+ return await this.createTracker(customTrackerId);
+ }
+
+ if (subCommand === 'add') {
+ const playerId = args[2];
+ const trackerId = args.slice(3).join(' ').trim() || null;
+ if (!playerId) return;
+ return await this.addPlayerToTracker(trackerId, playerId);
+ }
+
+ if (subCommand === 'remove') {
+ const playerId = args[2];
+ const trackerId = args.slice(3).join(' ').trim() || null;
+ if (!playerId) return;
+ return await this.removePlayerFromTracker(trackerId, playerId);
+ }
+
+ if (!args[2]) return;
+ }
+
+
+ async createTracker(inputTrackerId = null) {
+ const instance = Client.client.getInstance(this.guildId);
+ const serverId = instance?.activeServer;
+ if (!serverId) {
+ return Client.client.intlGet(this.guildId, 'activeRustServerNotFound');
+ }
+
+ const trackerId = await Client.client.createTrackerInstance(this.guildId, serverId, inputTrackerId);
+
+ if (!trackerId) {
+ return Client.client.intlGet(this.guildId, 'trackerIdAlreadyExists', { trackerId: inputTrackerId });
+ }
+
+ await DiscordMessages.sendTrackerMessage(this.guildId, trackerId);
+
+ return Client.client.intlGet(this.guildId, 'trackerCreated', { trackerId });
+ }
+
+ async addPlayerToTracker(trackerId, id) {
+ const instance = Client.client.getInstance(this.guildId);
+ if (!instance) {
+ return;
+ }
+
+ let targetTrackerId = trackerId;
+
+ if (!targetTrackerId) {
+ const rustplus = Client.client.rustplusInstances[this.guildId];
+ const currentServerId = rustplus?.serverId;
+
+ if (!currentServerId) {
+ return Client.client.intlGet(this.guildId, 'activeRustServerNotFound');
+ }
+
+ const matchingTrackers = Object.entries(instance.trackers)
+ .filter(([id, tracker]) => tracker.serverId === currentServerId)
+ .sort((a, b) => {
+ const createdA = a[1].createdAt || 0;
+ const createdB = b[1].createdAt || 0;
+ return createdB - createdA;
+ });
+
+
+ if (matchingTrackers.length === 0) {
+ return Client.client.intlGet(this.guildId, 'noTrackersForServer');
+ }
+
+ targetTrackerId = matchingTrackers[0][0];
+ }
+
+ const tracker = instance.trackers[targetTrackerId];
+
+ if (!tracker) {
+ return Client.client.intlGet(this.guildId, 'trackerIdNotFound', { trackerId: targetTrackerId });
+ }
+
+ const isSteamId64 = id.length === Constants.STEAMID64_LENGTH;
+ const bmInstance = Client.client.battlemetricsInstances[tracker.battlemetricsId];
+
+ const playerExists = (isSteamId64 && tracker.players.some(e => e.steamId === id)) ||
+ (!isSteamId64 && tracker.players.some(e => e.playerId === id && e.steamId === null));
+
+ if (playerExists) {
+ return Client.client.intlGet(this.guildId, 'playerAlreadyExistsInTracker', {
+ id,
+ trackerId: targetTrackerId
+ });
+ }
+
+ let name = null;
+ let steamId = null;
+ let playerId = null;
+
+ if (isSteamId64) {
+ steamId = id;
+ name = await Scrape.scrapeSteamProfileName(Client.client, steamId);
+ if (!name) {
+ name = 'Unknown';
+ }
+
+ if (bmInstance) {
+ playerId = Object.keys(bmInstance.players).find(
+ key => bmInstance.players[key]?.name === name
+ );
+ if (!playerId) {
+ playerId = null;
+ }
+ }
+
+ } else {
+ playerId = id;
+
+ if (bmInstance && bmInstance.players.hasOwnProperty(playerId)) {
+ name = bmInstance.players[playerId].name;
+ } else {
+ name = '-';
+ }
+ }
+
+ tracker.players.push({
+ name: name,
+ steamId: steamId,
+ playerId: playerId
+ });
+
+ Client.client.setInstance(this.guildId, instance);
+
+ await DiscordMessages.sendTrackerMessage(this.guildId, targetTrackerId);
+
+ return Client.client.intlGet(this.guildId, 'playerAddedToTracker', {
+ name,
+ id,
+ trackerId: targetTrackerId
+ });
+ }
+
+ async removePlayerFromTracker(trackerId, id) {
+ const instance = Client.client.getInstance(this.guildId);
+ if (!instance) {
+ return;
+ }
+
+ let targetTrackerId = trackerId;
+
+ if (!targetTrackerId) {
+ const rustplus = Client.client.rustplusInstances[this.guildId];
+ const currentServerId = rustplus?.serverId;
+
+ if (!currentServerId) {
+ return Client.client.intlGet(this.guildId, 'activeRustServerNotFound');
+ }
+
+ const matchingTrackers = Object.entries(instance.trackers)
+ .filter(([id, tracker]) => tracker.serverId === currentServerId)
+ .sort((a, b) => {
+ const createdA = a[1].createdAt || 0;
+ const createdB = b[1].createdAt || 0;
+ return createdB - createdA;
+ });
+
+
+ if (matchingTrackers.length === 0) {
+ return Client.client.intlGet(this.guildId, 'noTrackersForServer');
+ }
+
+ targetTrackerId = matchingTrackers[0][0];
+ }
+
+ const tracker = instance.trackers[targetTrackerId];
+ if (!tracker) {
+ return Client.client.intlGet(this.guildId, 'trackerIdNotFound', { trackerId: targetTrackerId });
+ }
+
+ const isSteamId64 = id.length === Constants.STEAMID64_LENGTH;
+
+ const beforeCount = tracker.players.length;
+
+ if (isSteamId64) {
+ tracker.players = tracker.players.filter(e => e.steamId !== id);
+ } else {
+ tracker.players = tracker.players.filter(e => e.playerId !== id || e.steamId !== null);
+ }
+
+ const afterCount = tracker.players.length;
+
+ if (beforeCount === afterCount) {
+ return Client.client.intlGet(this.guildId, 'playerNotFoundInTracker', {
+ id,
+ trackerId: targetTrackerId
+ });
+ }
+
+ Client.client.setInstance(this.guildId, instance);
+
+ await DiscordMessages.sendTrackerMessage(this.guildId, targetTrackerId);
+
+ return Client.client.intlGet(this.guildId, 'playerRemovedFromTracker', {
+ id,
+ trackerId: targetTrackerId
+ });
+ }
}
module.exports = RustPlus;