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 ![In-Game Command translateFrom Image](images/ingame_commands/translateFrom_ingame.png) + +## **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` + +![In-Game Command tracker Image](images/ingame_commands/tracker_ingame.png) + + ## **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;