diff --git a/docs/commands.md b/docs/commands.md
index 9dfd6ab22..2c8b51d46 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -347,6 +347,7 @@ In-Game Command | Description
--------------- | -----------
[**afk**](commands.md#afk) | Get the currently afk players in your team.
[**alive**](commands.md#alive) | Get the player with the longest time alive.
+[**calc**](commands.md#calc) | Calculate a mathematical expression.
[**cargo**](commands.md#cargo) | Get information about CargoShip (Location, time till enters egress stage, time since last on map).
[**chinook**](commands.md#chinook) | Get information about Chinook 47 (Location, time since last on map).
[**connection/connections**](commands.md#connectionconnections) | Get recent connection events.
@@ -401,6 +402,12 @@ In-Game Command | Description

+## **calc**
+
+> **Calculate a mathematical expression.**
+
Command: `!calc 1+1`
+
Command: `!calc 17*33`
+
## **cargo**
diff --git a/package-lock.json b/package-lock.json
index 645f969b9..bb74f05b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -700,6 +700,30 @@
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"license": "MIT"
},
+ "node_modules/readable-web-to-node-stream/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
@@ -2109,6 +2133,12 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
+ "node_modules/agent-base/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/prism-media": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
@@ -4835,6 +4865,13 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/core/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
diff --git a/src/handlers/discordCommandHandler.js b/src/handlers/discordCommandHandler.js
index baf9f6bc4..0aed3e440 100644
--- a/src/handlers/discordCommandHandler.js
+++ b/src/handlers/discordCommandHandler.js
@@ -44,6 +44,12 @@ module.exports = {
commandLowerCase.startsWith(`${prefix}${client.intlGet(guildId, 'commandSyntaxAlive')}`)) {
response = rustplus.getCommandAlive(command);
}
+ else if ((commandLowerCase.startsWith(`${prefix}${client.intlGet('en', 'commandSyntaxCalc')} `) ||
+ commandLowerCase === `${prefix}${client.intlGet('en', 'commandSyntaxCalc')}`) ||
+ (commandLowerCase.startsWith(`${prefix}${client.intlGet(guildId, 'commandSyntaxCalc')} `) ||
+ commandLowerCase === `${prefix}${client.intlGet(guildId, 'commandSyntaxCalc')}`)) {
+ response = rustplus.getCommandCalc(client, command);
+ }
else if (commandLowerCase === `${prefix}${client.intlGet('en', 'commandSyntaxCargo')}` ||
commandLowerCase === `${prefix}${client.intlGet(guildId, 'commandSyntaxCargo')}`) {
response = rustplus.getCommandCargo();
diff --git a/src/handlers/inGameCommandHandler.js b/src/handlers/inGameCommandHandler.js
index 8d606ef74..27b895967 100644
--- a/src/handlers/inGameCommandHandler.js
+++ b/src/handlers/inGameCommandHandler.js
@@ -51,6 +51,12 @@ module.exports = {
commandLowerCase.startsWith(`${prefix}${client.intlGet(guildId, 'commandSyntaxAlive')}`)) {
rustplus.sendInGameMessage(rustplus.getCommandAlive(command));
}
+ else if ((commandLowerCase.startsWith(`${prefix}${client.intlGet('en', 'commandSyntaxCalc')} `) ||
+ commandLowerCase === `${prefix}${client.intlGet('en', 'commandSyntaxCalc')}`) ||
+ (commandLowerCase.startsWith(`${prefix}${client.intlGet(guildId, 'commandSyntaxCalc')} `) ||
+ commandLowerCase === `${prefix}${client.intlGet(guildId, 'commandSyntaxCalc')}`)) {
+ rustplus.sendInGameMessage(rustplus.getCommandCalc(client, command));
+ }
else if (commandLowerCase === `${prefix}${client.intlGet('en', 'commandSyntaxCargo')}` ||
commandLowerCase === `${prefix}${client.intlGet(guildId, 'commandSyntaxCargo')}`) {
rustplus.sendInGameMessage(rustplus.getCommandCargo());
diff --git a/src/languages/cs.json b/src/languages/cs.json
index e09ac66f7..49c46e5a4 100644
--- a/src/languages/cs.json
+++ b/src/languages/cs.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe byl detekován!",
"yield": "Úrodnost",
"youAreAlreadyLeader": "Už jsi vůdce.",
- "youAreNotPairedWithServer": "Příkaz na vůdce nefunguje, protože nejsi spárován se serverem."
+ "youAreNotPairedWithServer": "Příkaz na vůdce nefunguje, protože nejsi spárován se serverem.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/de.json b/src/languages/de.json
index 3f6e42a9c..477ca6bc3 100644
--- a/src/languages/de.json
+++ b/src/languages/de.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe erkannt!",
"yield": "Ertrag",
"youAreAlreadyLeader": "Du bist bereits Anführer.",
- "youAreNotPairedWithServer": "Anführer-Befehl funktioniert nicht, weil du nicht mit dem Server gekoppelt bist."
+ "youAreNotPairedWithServer": "Anführer-Befehl funktioniert nicht, weil du nicht mit dem Server gekoppelt bist.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/en.json b/src/languages/en.json
index bb5fb961d..b40b26453 100644
--- a/src/languages/en.json
+++ b/src/languages/en.json
@@ -735,7 +735,7 @@
"travelingVendorHaltedSetting": "When the Traveling Vendor stops moving, send a notification.",
"travelingVendorLeftSetting": "When the Traveling Vendor left the map, send a notification.",
"travelingVendorLocatedAt": "The Traveling Vendor is located at {location}.",
- "travelingVendorLeftMap" : "The Traveling Vendor just left the map at {location}.",
+ "travelingVendorLeftMap": "The Traveling Vendor just left the map at {location}.",
"travelingVendorNotCurrentlyOnMap": "The Traveling Vendor is not currently on the map.",
"travelingVendorResumedAt": "The Traveling Vendor resumed moving at {location}.",
"travelingVendorSpawnedAt": "The Traveling Vendor spawned at {location}.",
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe detected!",
"yield": "Yield",
"youAreAlreadyLeader": "You are already leader.",
- "youAreNotPairedWithServer": "Leader command does not work because you're not paired with the server."
+ "youAreNotPairedWithServer": "Leader command does not work because you're not paired with the server.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/es.json b/src/languages/es.json
index 662e86e30..36f22825f 100644
--- a/src/languages/es.json
+++ b/src/languages/es.json
@@ -786,5 +786,6 @@
"wipeDetected": "¡Wipe detectado!",
"yield": "Yield",
"youAreAlreadyLeader": "Ya eres líder.",
- "youAreNotPairedWithServer": "El comando líder no funciona porque no está emparejado con el servidor."
+ "youAreNotPairedWithServer": "El comando líder no funciona porque no está emparejado con el servidor.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/fr.json b/src/languages/fr.json
index ccc19587c..b1077c1d9 100644
--- a/src/languages/fr.json
+++ b/src/languages/fr.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe detecté!",
"yield": "Rendement",
"youAreAlreadyLeader": "Vous êtes déjà le chef du groupe.",
- "youAreNotPairedWithServer": "La commande Leader ne fonctionne pas car n'est pas associé au serveur."
+ "youAreNotPairedWithServer": "La commande Leader ne fonctionne pas car n'est pas associé au serveur.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/it.json b/src/languages/it.json
index 8c7c6c331..276032da5 100644
--- a/src/languages/it.json
+++ b/src/languages/it.json
@@ -786,5 +786,6 @@
"wipeDetected": "Rilevato wipe!",
"yield": "Yield",
"youAreAlreadyLeader": "Sei già il leader.",
- "youAreNotPairedWithServer": "Il comando del leader non funziona perché non sei associato al server."
+ "youAreNotPairedWithServer": "Il comando del leader non funziona perché non sei associato al server.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/ko.json b/src/languages/ko.json
index 8fcb75144..adf293e15 100644
--- a/src/languages/ko.json
+++ b/src/languages/ko.json
@@ -786,5 +786,6 @@
"wipeDetected": "서버 초기화가 감지되었습니다!",
"yield": "Yield",
"youAreAlreadyLeader": "당신은 이미 팀 리더 입니다.",
- "youAreNotPairedWithServer": "서버와 페어링되지 않았기 때문에 리더 명령어가 작동하지 않습니다."
+ "youAreNotPairedWithServer": "서버와 페어링되지 않았기 때문에 리더 명령어가 작동하지 않습니다.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/pl.json b/src/languages/pl.json
index d23121c4c..fd7fc4ac6 100644
--- a/src/languages/pl.json
+++ b/src/languages/pl.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe detected!",
"yield": "Plon",
"youAreAlreadyLeader": "You are already leader.",
- "youAreNotPairedWithServer": "Leader command does not work because you're not paired with the server."
+ "youAreNotPairedWithServer": "Leader command does not work because you're not paired with the server.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/pt.json b/src/languages/pt.json
index 3598ffffa..9d8c0bcde 100644
--- a/src/languages/pt.json
+++ b/src/languages/pt.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe detetado!",
"yield": "Rendimento",
"youAreAlreadyLeader": "Você já é o líder.",
- "youAreNotPairedWithServer": "O comando Líder não funciona porque você não está emparelhado com o servidor."
+ "youAreNotPairedWithServer": "O comando Líder não funciona porque você não está emparelhado com o servidor.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/ru.json b/src/languages/ru.json
index f1d099369..7943dad24 100644
--- a/src/languages/ru.json
+++ b/src/languages/ru.json
@@ -786,5 +786,6 @@
"wipeDetected": "Обнаружен Wipe!",
"yield": "Урожай",
"youAreAlreadyLeader": "Вы уже лидер.",
- "youAreNotPairedWithServer": "Команда лидера не работает, потому что вы не подключены к серверу."
+ "youAreNotPairedWithServer": "Команда лидера не работает, потому что вы не подключены к серверу.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/sv.json b/src/languages/sv.json
index 8d71ed5bb..7edd4ede7 100644
--- a/src/languages/sv.json
+++ b/src/languages/sv.json
@@ -786,5 +786,6 @@
"wipeDetected": "Rensning upptäcktes!",
"yield": "Yield",
"youAreAlreadyLeader": "Du är redan lagledare.",
- "youAreNotPairedWithServer": "Leader-kommandot fungerar inte eftersom du inte är parad med servern."
+ "youAreNotPairedWithServer": "Leader-kommandot fungerar inte eftersom du inte är parad med servern.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/languages/tr.json b/src/languages/tr.json
index c8b7e925d..8a7c01739 100644
--- a/src/languages/tr.json
+++ b/src/languages/tr.json
@@ -786,5 +786,6 @@
"wipeDetected": "Wipe tespit edildi!",
"yield": "Teslim Ol",
"youAreAlreadyLeader": "Zaten lidersin.",
- "youAreNotPairedWithServer": "Sunucuyla eşleşmediğiniz için lider komutu çalışmıyor."
+ "youAreNotPairedWithServer": "Sunucuyla eşleşmediğiniz için lider komutu çalışmıyor.",
+ "commandSyntaxCalc": "calc"
}
\ No newline at end of file
diff --git a/src/structures/RustPlus.js b/src/structures/RustPlus.js
index 39820fa6a..453a8e011 100644
--- a/src/structures/RustPlus.js
+++ b/src/structures/RustPlus.js
@@ -708,6 +708,34 @@ class RustPlus extends RustPlusLib {
});
}
+ getCommandCalc(client, command) {
+ const prefix = this.generalSettings.prefix;
+ const commandCalc = `${prefix}${client.intlGet(this.guildId, 'commandSyntaxCalc')}`;
+ const commandCalcEn = `${prefix}${client.intlGet('en', 'commandSyntaxCalc')}`;
+
+ let expr = '';
+ if (command.toLowerCase().startsWith(`${commandCalc} `)) {
+ expr = command.substring(commandCalc.length).trim();
+ }
+ else if (command.toLowerCase().startsWith(`${commandCalcEn} `)) {
+ expr = command.substring(commandCalcEn.length).trim();
+ }
+
+ if (expr === '') return client.intlGet(this.guildId, 'missingArguments');
+
+ try {
+ const safeExpr = expr.replace(/[^\d.\+\-\*\/\(\)\s]/g, '');
+ if (safeExpr.length === 0) return client.intlGet(this.guildId, 'errorExecutingCommand');
+
+ const res = new Function(`return (${safeExpr});`)();
+ if (res === undefined || Number.isNaN(res)) return client.intlGet(this.guildId, 'errorExecutingCommand');
+
+ return `${client.intlGet(this.guildId, 'calculated')}: ${expr} = ${res}`;
+ } catch(e) {
+ return client.intlGet(this.guildId, 'errorExecutingCommand');
+ }
+ }
+
getCommandCargo(isInfoChannel = false) {
const strings = [];
let unhandled = this.mapMarkers.cargoShips.map(e => e.id);
diff --git a/ubuntu_install.sh b/ubuntu_install.sh
new file mode 100755
index 000000000..2e538813c
--- /dev/null
+++ b/ubuntu_install.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+# ubuntu_install.sh
+# Automatically clones repository and starts the bot using pm2 interactively
+
+set -e
+
+REPO_URL="https://github.com/abboodnoga176-max/rustplusplus.git"
+INSTALL_DIR="$HOME/rustplusplus"
+
+echo "=== Rustplusplus Ubuntu Installer ==="
+
+echo "Checking for required dependencies..."
+sudo apt-get update
+sudo apt-get install -y git curl build-essential
+
+if ! command -v node >/dev/null 2>&1; then
+ echo "Node.js not found. Installing Node.js..."
+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+ sudo apt-get install -y nodejs
+fi
+
+if ! command -v pm2 >/dev/null 2>&1; then
+ echo "PM2 not found. Installing PM2 globally..."
+ sudo npm install -g pm2
+fi
+
+if [ -d "$INSTALL_DIR" ]; then
+ echo "Directory $INSTALL_DIR already exists."
+else
+ echo "Cloning rustplusplus..."
+ git clone "$REPO_URL" "$INSTALL_DIR"
+fi
+
+cd "$INSTALL_DIR"
+
+echo "Installing NPM dependencies..."
+npm install
+
+ENV_FILE=".env"
+if [ ! -f "$ENV_FILE" ]; then
+ echo ""
+ echo "--- Discord Credentials Setup ---"
+ read -p "Enter your RPP_DISCORD_CLIENT_ID: " DISCORD_CLIENT_ID
+ read -p "Enter your RPP_DISCORD_TOKEN: " DISCORD_TOKEN
+
+ cat < "$ENV_FILE"
+RPP_DISCORD_CLIENT_ID=$DISCORD_CLIENT_ID
+RPP_DISCORD_TOKEN=$DISCORD_TOKEN
+ENV
+ echo "Credentials saved to $ENV_FILE"
+else
+ echo "Credentials already exist in $ENV_FILE"
+fi
+
+UPDATE_SCRIPT="$INSTALL_DIR/ubuntu_update.sh"
+cat << 'UPSCRIPT' > "$UPDATE_SCRIPT"
+#!/bin/bash
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$DIR"
+
+# Fetch latest changes
+git fetch origin
+
+LOCAL=$(git rev-parse HEAD)
+REMOTE=$(git rev-parse @{u})
+
+if [ "$LOCAL" != "$REMOTE" ]; then
+ echo "Updates found. Sending restart notification..."
+
+ if [ -f "notify_restart.js" ]; then
+ node notify_restart.js
+ sleep 5
+ fi
+
+ git reset --hard origin/master
+ npm install
+
+ pm2 restart rustplusplus
+ echo "Update complete."
+fi
+UPSCRIPT
+
+chmod +x "$UPDATE_SCRIPT"
+
+NOTIFY_SCRIPT="$INSTALL_DIR/notify_restart.js"
+cat << 'NJSCRIPT' > "$NOTIFY_SCRIPT"
+require('dotenv').config();
+const { Client, GatewayIntentBits, ChannelType } = require('discord.js');
+const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
+
+client.on('ready', async () => {
+ try {
+ const guilds = client.guilds.cache;
+ for (const [id, guild] of guilds) {
+ const channels = guild.channels.cache.filter(c => c.type === ChannelType.GuildText);
+ const infoChannel = channels.find(c => c.name === 'information' && c.parent && c.parent.name === 'rustplusplus');
+ if (infoChannel) {
+ await infoChannel.send('⚠️ **Notice:** The bot is restarting to apply a new update. It will be back shortly!').catch(() => {});
+ }
+ }
+ } catch (e) {
+ console.error("Error sending restart notifications:", e);
+ }
+ client.destroy();
+});
+client.login(process.env.RPP_DISCORD_TOKEN).catch(()=>console.log("Could not login to notify"));
+NJSCRIPT
+
+CRON_JOB="*/5 * * * * $UPDATE_SCRIPT >> $INSTALL_DIR/logs/auto_update.log 2>&1"
+(crontab -l 2>/dev/null | grep -Fv "ubuntu_update.sh"; echo "$CRON_JOB") | crontab -
+echo "Cron job added to check for updates every 5 minutes."
+
+mkdir -p "$INSTALL_DIR/logs"
+
+echo "Starting bot with PM2..."
+npm install dotenv --no-save
+pm2 start npm --name "rustplusplus" -- run start
+pm2 save
+pm2 startup | grep "sudo" | bash || true
+
+echo "=== Installation Complete ==="
+echo "Bot is now running in the background via PM2."
+echo "Use 'pm2 logs rustplusplus' to view logs."